Packages

phoenix_kit

1.7.49
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
phoenix_kit lib modules shop import shopify_csv.ex
Raw

lib/modules/shop/import/shopify_csv.ex

defmodule PhoenixKit.Modules.Shop.Import.ShopifyCSV do
@moduledoc """
Main orchestrator for Shopify CSV import.
Coordinates CSV parsing, validation, filtering, transformation, and product creation.
## Usage
# Dry run - see what would be imported
ShopifyCSV.import("/path/to/products.csv", dry_run: true)
# Full import
ShopifyCSV.import("/path/to/products.csv")
# Import with custom config
config = Shop.get_import_config!(config_id)
ShopifyCSV.import("/path/to/products.csv", config: config)
# Import to specific category
category = Shop.get_category_by_slug("shelves")
ShopifyCSV.import("/path/to/products.csv", category_id: category.id)
"""
alias PhoenixKit.Modules.Shop
alias PhoenixKit.Modules.Shop.Import.{CSVParser, CSVValidator, Filter, ProductTransformer}
alias PhoenixKit.Modules.Shop.ImportConfig
alias PhoenixKit.Modules.Shop.Translations
require Logger
@doc """
Import products from Shopify CSV file.
## Options
- `:dry_run` - If true, don't create products, just return what would be created
- `:category_id` - Override category for all products
- `:skip_existing` - If true, skip products with existing slugs (default: true)
- `:update_existing` - If true, update existing products instead of skipping (default: false)
- `:config` - ImportConfig struct for filtering/categorization (nil = use defaults)
- `:validate` - If true, validate CSV before import (default: true)
Note: When `update_existing: true`, `skip_existing` is ignored.
## Returns
Summary map with:
- `:imported` - count of newly created products
- `:updated` - count of updated existing products
- `:skipped` - count of skipped (existing or filtered out)
- `:errors` - count of failed imports
- `:dry_run` - count of products in dry run
- `:error_details` - list of error tuples
- `:validation_report` - CSV validation report (if validate: true)
"""
def import(file_path, opts \\ []) do
dry_run = Keyword.get(opts, :dry_run, false)
category_id = Keyword.get(opts, :category_id)
skip_existing = Keyword.get(opts, :skip_existing, true)
update_existing = Keyword.get(opts, :update_existing, false)
config = Keyword.get(opts, :config)
validate = Keyword.get(opts, :validate, true)
language = Keyword.get(opts, :language)
# Get required columns from config if provided
required_columns = get_required_columns(config)
# Validate CSV first if requested
validation_result =
if validate do
case CSVValidator.validate_headers(file_path, required_columns) do
{:ok, _headers} ->
{:ok,
CSVValidator.get_validation_report(file_path, required_columns: required_columns)}
{:error, reason} ->
{:error, reason}
end
else
{:ok, nil}
end
case validation_result do
{:error, reason} ->
%{
imported: 0,
updated: 0,
dry_run: 0,
skipped: 0,
errors: 1,
error_details: [{:validation_failed, format_validation_error(reason)}],
validation_report: nil
}
{:ok, validation_report} ->
do_import(file_path, %{
dry_run: dry_run,
category_id: category_id,
skip_existing: skip_existing,
update_existing: update_existing,
config: config,
validation_report: validation_report,
language: language
})
end
end
defp get_required_columns(%ImportConfig{required_columns: cols}) when is_list(cols), do: cols
defp get_required_columns(_), do: ImportConfig.default_required_columns()
defp format_validation_error({:missing_columns, cols}),
do: "Missing columns: #{Enum.join(cols, ", ")}"
defp format_validation_error(:file_not_found), do: "File not found"
defp format_validation_error(:empty_file), do: "File is empty"
defp format_validation_error({:parse_error, msg}), do: "CSV parse error: #{msg}"
defp format_validation_error(other), do: inspect(other)
defp do_import(file_path, opts) do
%{
dry_run: dry_run,
category_id: category_id,
skip_existing: skip_existing,
update_existing: update_existing,
config: config,
validation_report: validation_report,
language: language
} = opts
# Build categories map for auto-assignment
categories_map = build_categories_map()
# Parse and group CSV
Logger.info("Parsing CSV: #{file_path}")
grouped = CSVParser.parse_and_group(file_path)
Logger.info("Found #{map_size(grouped)} unique handles")
# Filter and import
results =
grouped
|> Enum.map(fn {handle, rows} ->
process_product(handle, rows, %{
dry_run: dry_run,
category_id: category_id,
categories_map: categories_map,
skip_existing: skip_existing,
update_existing: update_existing,
config: config,
language: language
})
end)
summary = summarize(results)
Map.put(summary, :validation_report, validation_report)
end
@doc """
Quick dry run - just parse and filter, show what would be imported.
"""
def preview(file_path, opts \\ []) do
config = Keyword.get(opts, :config)
grouped = CSVParser.parse_and_group(file_path)
filtered =
grouped
|> Enum.filter(fn {_handle, rows} -> Filter.should_include?(rows, config) end)
Logger.info("Total products in CSV: #{map_size(grouped)}")
Logger.info("Would import (matching filter): #{length(filtered)}")
Logger.info("Would skip (filtered out): #{map_size(grouped) - length(filtered)}")
# Show sample
filtered
|> Enum.take(5)
|> Enum.each(fn {handle, rows} ->
first = List.first(rows)
category = Filter.categorize(first["Title"] || "", config)
Logger.info(" #{handle} -> #{category}")
end)
%{
total: map_size(grouped),
would_import: length(filtered),
would_skip: map_size(grouped) - length(filtered)
}
end
@doc """
Validates CSV file without importing.
Returns validation report with headers, row count, and any warnings.
"""
def validate(file_path, opts \\ []) do
config = Keyword.get(opts, :config)
required_columns = get_required_columns(config)
CSVValidator.get_validation_report(file_path, required_columns: required_columns)
end
# Private helpers
defp build_categories_map do
lang = Translations.default_language()
Shop.list_categories()
|> Enum.reduce(%{}, fn cat, acc ->
# Extract string slug from JSONB map for map key
slug = Translations.get(cat, :slug, lang)
if slug && slug != "" do
Map.put(acc, slug, cat.id)
else
acc
end
end)
end
defp process_product(handle, rows, opts) do
config = opts.config
if Filter.should_include?(rows, config) do
do_process_product(handle, rows, opts)
else
{:skipped, handle, :filtered}
end
end
defp do_process_product(handle, rows, opts) do
%{
dry_run: dry_run,
category_id: override_category_id,
categories_map: categories_map,
config: config,
language: language
} = opts
# Transform with config and language
transform_opts = if language, do: [language: language], else: []
attrs = ProductTransformer.transform(handle, rows, categories_map, config, transform_opts)
# Override category if specified
attrs =
if override_category_id do
Map.put(attrs, :category_id, override_category_id)
else
attrs
end
if dry_run do
{:dry_run, handle, attrs}
else
save_product(handle, attrs, opts)
end
end
defp save_product(handle, attrs, opts) do
%{skip_existing: skip_existing, update_existing: update_existing} = opts
cond do
update_existing ->
upsert_product(handle, attrs)
skip_existing && product_exists?(handle) ->
{:skipped, handle, :exists}
true ->
create_product(handle, attrs)
end
end
defp product_exists?(slug) do
case Shop.get_product_by_slug(slug) do
nil -> false
_ -> true
end
end
defp create_product(handle, attrs) do
case Shop.create_product(attrs) do
{:ok, product} ->
Logger.debug("Created: #{handle}")
{:ok, product}
{:error, changeset} ->
Logger.warning("Failed: #{handle} - #{inspect(changeset.errors)}")
{:error, handle, changeset}
end
end
defp upsert_product(handle, attrs) do
case Shop.upsert_product(attrs) do
{:ok, product, :inserted} ->
Logger.debug("Created: #{handle}")
{:ok, product}
{:ok, product, :updated} ->
Logger.debug("Updated: #{handle}")
{:updated, product}
{:error, changeset} ->
Logger.warning("Failed: #{handle} - #{inspect(changeset.errors)}")
{:error, handle, changeset}
end
end
defp summarize(results) do
ok_count = Enum.count(results, &match?({:ok, _}, &1))
updated_count = Enum.count(results, &match?({:updated, _}, &1))
dry_count = Enum.count(results, &match?({:dry_run, _, _}, &1))
skipped_count = Enum.count(results, &match?({:skipped, _, _}, &1))
errors = Enum.filter(results, &match?({:error, _, _}, &1))
summary = %{
imported: ok_count,
updated: updated_count,
dry_run: dry_count,
skipped: skipped_count,
errors: length(errors),
error_details: errors
}
Logger.info(
"Import complete: #{ok_count} imported, #{updated_count} updated, #{dry_count} dry run, #{skipped_count} skipped, #{length(errors)} errors"
)
summary
end
end