Packages
phoenix_kit
1.7.78
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/shop/import/option_builder.ex
defmodule PhoenixKit.Modules.Shop.Import.OptionBuilder do
@moduledoc """
Build option values and price modifiers from Shopify variant rows.
Extracts Option1..Option10 names and values from CSV rows,
calculates base price (minimum) and price modifiers (deltas from base).
## Extended Support
- Supports Option1 through Option10 (Shopify standard)
- Accepts option_mappings for slot-based options
- Builds _option_slots structure for products using global options
"""
@max_options 10
@doc """
Build options data from variant rows (legacy format).
Returns a map with:
- base_price: minimum variant price (Decimal)
- option1_name: name of first option (e.g., "Size")
- option1_values: list of unique values for option1
- option1_modifiers: map of value => price delta from base
- option2_name: name of second option (e.g., "Color")
- option2_values: list of unique values for option2
## Examples
OptionBuilder.build_from_variants(rows)
# => %{
# base_price: Decimal.new("22.80"),
# option1_name: "Size",
# option1_values: ["4 inches (10 cm)", "5 inches (13 cm)", ...],
# option1_modifiers: %{"4 inches (10 cm)" => "0", "5 inches (13 cm)" => "5.00", ...},
# option2_name: "Color",
# option2_values: ["Black", "White", ...]
# }
"""
def build_from_variants(rows) when is_list(rows) do
# Get option names from first row
first_row = List.first(rows)
option1_name = get_non_empty(first_row, "Option1 Name")
option2_name = get_non_empty(first_row, "Option2 Name")
# Extract variants with prices
variants =
rows
|> Enum.map(fn row ->
%{
option1_value: get_non_empty(row, "Option1 Value"),
option2_value: get_non_empty(row, "Option2 Value"),
price: parse_price(row["Variant Price"])
}
end)
|> Enum.filter(& &1.price)
# Calculate base price (minimum)
base_price =
variants
|> Enum.map(& &1.price)
|> Enum.min(fn -> Decimal.new("0") end)
# Build option1 data (typically Size - affects price)
{option1_values, option1_modifiers} = build_option_data(variants, :option1_value, base_price)
# Build option2 values (typically Color - no price impact, just values)
option2_values = get_unique_values(variants, :option2_value)
%{
base_price: base_price,
option1_name: option1_name,
option1_values: option1_values,
option1_modifiers: option1_modifiers,
option2_name: option2_name,
option2_values: option2_values
}
end
@doc """
Build extended options data from variant rows.
Supports Option1 through Option10 and optional slot mappings.
## Arguments
- `rows` - List of CSV row maps for a single product
- `opts` - Keyword options:
- `:option_mappings` - List of mapping configs from ImportConfig
## Returns
Map with:
- `base_price` - Minimum variant price
- `options` - List of option data for each option found
- `option_slots` - Slot definitions if mappings provided
## Examples
# Without mappings (standard import)
OptionBuilder.build_extended(rows)
# => %{
# base_price: Decimal.new("22.80"),
# options: [
# %{position: 1, name: "Size", values: [...], modifiers: %{...}},
# %{position: 2, name: "Cup Color", values: [...]},
# %{position: 3, name: "Liquid Color", values: [...]}
# ],
# option_slots: []
# }
# With mappings (slot-based import)
mappings = [
%{"csv_name" => "Cup Color", "slot_key" => "cup_color", "source_key" => "color"},
%{"csv_name" => "Liquid Color", "slot_key" => "liquid_color", "source_key" => "color"}
]
OptionBuilder.build_extended(rows, option_mappings: mappings)
# => %{
# base_price: Decimal.new("22.80"),
# options: [...],
# option_slots: [
# %{slot: "cup_color", source_key: "color", label: "Cup Color", values: [...]},
# %{slot: "liquid_color", source_key: "color", label: "Liquid Color", values: [...]}
# ]
# }
"""
def build_extended(rows, opts \\ []) when is_list(rows) do
option_mappings = Keyword.get(opts, :option_mappings, [])
first_row = List.first(rows)
# Extract variants with prices and all option values
variants = extract_all_variants(rows)
# Calculate base price (minimum)
base_price =
variants
|> Enum.map(& &1.price)
|> Enum.filter(& &1)
|> Enum.min(fn -> Decimal.new("0") end)
# Build option data for each option position
options =
for i <- 1..@max_options,
name = get_option_name(first_row, i),
name != nil do
field = String.to_atom("option#{i}_value")
{values, modifiers} = build_option_data(variants, field, base_price)
# Only include modifiers if they have non-zero values
has_price_impact = Enum.any?(modifiers, fn {_k, v} -> v != "0" end)
%{
position: i,
name: name,
values: values,
modifiers: if(has_price_impact, do: modifiers, else: %{})
}
end
# Build option slots from mappings
option_slots = build_option_slots_from_mappings(options, option_mappings)
%{
base_price: base_price,
options: options,
option_slots: option_slots
}
end
# Extract all option values (Option1..Option10) from variant rows
defp extract_all_variants(rows) do
Enum.map(rows, fn row ->
base = %{price: parse_price(row["Variant Price"])}
# Add option values for each position
Enum.reduce(1..@max_options, base, fn i, acc ->
key = String.to_atom("option#{i}_value")
value = get_non_empty(row, "Option#{i} Value")
Map.put(acc, key, value)
end)
end)
|> Enum.filter(& &1.price)
end
# Get option name for a position
defp get_option_name(row, position) do
get_non_empty(row, "Option#{position} Name")
end
# Build option slots from mappings
defp build_option_slots_from_mappings(options, mappings) when is_list(mappings) do
mappings
|> Enum.map(fn mapping ->
csv_name = mapping["csv_name"]
slot_key = mapping["slot_key"]
source_key = mapping["source_key"]
label = mapping["label"] || csv_name
# Find the option with matching name
option = Enum.find(options, fn opt -> opt.name == csv_name end)
if option && slot_key do
%{
slot: slot_key,
source_key: source_key,
label: label,
values: option.values,
position: option.position
}
else
nil
end
end)
|> Enum.reject(&is_nil/1)
end
defp build_option_slots_from_mappings(_, _), do: []
# Private helpers
defp get_non_empty(row, key) do
case row[key] do
nil -> nil
"" -> nil
value -> String.trim(value)
end
end
defp parse_price(nil), do: nil
defp parse_price(""), do: nil
defp parse_price(str) when is_binary(str) do
str = String.trim(str)
case Decimal.parse(str) do
{decimal, _} -> decimal
:error -> nil
end
end
defp build_option_data(variants, field, base_price) do
# Get unique values preserving order of first appearance
values =
variants
|> Enum.map(&Map.get(&1, field))
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
# Build price modifiers for each value
# Group by value and take the first price for each
price_by_value =
variants
|> Enum.reduce(%{}, fn v, acc ->
value = Map.get(v, field)
if value && !Map.has_key?(acc, value) do
Map.put(acc, value, v.price)
else
acc
end
end)
# Calculate modifiers as delta from base price
modifiers =
price_by_value
|> Enum.reduce(%{}, fn {value, price}, acc ->
modifier = Decimal.sub(price, base_price)
Map.put(acc, value, Decimal.to_string(modifier))
end)
{values, modifiers}
end
defp get_unique_values(variants, field) do
variants
|> Enum.map(&Map.get(&1, field))
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
end
end