PhoenixKit.Modules.Shop (phoenix_kit v1.7.71)

Copy Markdown View Source

E-commerce Shop Module for PhoenixKit.

Provides comprehensive e-commerce functionality including products, categories, options-based pricing, and cart management.

Features

  • Products: Physical and digital products with JSONB flexibility
  • Categories: Hierarchical product categories
  • Options: Product options with dynamic pricing (fixed or percent modifiers)
  • Inventory: Stock tracking with reservation system
  • Cart: Persistent shopping cart (DB-backed for cross-device support)

System Enable/Disable

# Check if shop is enabled
PhoenixKit.Modules.Shop.enabled?()

# Enable/disable shop system
PhoenixKit.Modules.Shop.enable_system()
PhoenixKit.Modules.Shop.disable_system()

Integration with Billing

Shop integrates with the Billing module for orders and payments. Order line_items include shop metadata for product tracking.

Summary

Functions

Aggregates filter values for sidebar display.

Auto-selects payment option if only one is available.

Auto-selects the cheapest available shipping method for a cart.

Bulk delete categories. Returns count of deleted categories. Nullifies category references on orphaned products.

Bulk delete products. Returns count of deleted products.

Bulk update category parent. Returns count of updated categories. Excludes the target parent from the update set to prevent self-reference. Uses a single UPDATE with subquery to resolve parent_uuid.

Bulk update category status. Returns count of updated categories.

Bulk update product category. Returns count of updated products.

Bulk update product status. Returns count of updated products.

Calculates the final price for a product based on selected specifications.

Generates a localized URL for the cart page.

Generates a localized URL for the shop catalog.

Returns categories as options for select input. Returns list of {localized_name, id} tuples.

Checks if a category slug exists for a language.

Generates a localized URL for a category.

Returns a changeset for category form.

Returns a changeset for tracking import config changes.

Returns a changeset for product form.

Returns a changeset for shipping method form.

Generates a localized URL for the checkout page.

Clears all items from cart.

Collects all storage file UUIDs associated with a single product.

Collects all storage file UUIDs for a list of product UUIDs.

Marks import as completed.

Converts a cart to a Billing.Order.

Counts active carts.

Creates a new cart.

Creates a new category.

Creates an import config.

Creates a new import log entry.

Creates a new product.

Creates a new shipping method.

Returns the default storefront filter configuration.

Deletes a category.

Deletes an import config.

Deletes an import log.

Deletes a product.

Deletes a shipping method.

Disables the shop system.

Discovers filterable option keys from product metadata.

Enables the shop system.

Checks if the shop system is enabled.

Creates the legacy default import config if no configs exist.

Ensures a category has a featured_product_uuid set.

Ensures a default Prom.ua import config exists. Creates one if no config with name "prom_ua_default" is found.

Expires old guest carts.

Marks import as failed.

Finds active cart by user_uuid or session_id.

Finds an existing product by any slug in the provided slug map.

Formats the product price for catalog display.

Gets available shipping methods for a cart. Filters by weight, subtotal, and country.

Gets a cart by ID or UUID with items preloaded.

Gets a cart by ID or UUID, raises if not found.

Gets a category by ID or UUID.

Gets a category by ID or UUID, raises if not found.

Finds a category by slug in any language.

Gets a category by slug.

Gets a category by slug with language awareness.

Gets the localized slug for a category.

Returns the current shop configuration.

Returns dashboard statistics for the shop.

Gets the default currency struct from Billing module.

Gets the default currency code from Billing module. Falls back to "USD" if Billing has no default currency configured.

Gets the default import config, if one exists.

Gets the default language code (base code, e.g., "en").

Returns only enabled storefront filters, sorted by position.

Gets an import config by ID.

Gets an import config by ID, raises if not found.

Gets an import config by name.

Gets an import log by ID.

Gets an import log by ID, raises if not found.

Gets or creates a cart for the current user/session.

Gets price-affecting options for a product.

Gets the price range for a product based on option modifiers.

Gets a product by ID or UUID.

Gets a product by ID or UUID, raises if not found.

Finds a product by slug in any language.

Gets a product by slug.

Gets a product by slug with language awareness.

Gets the localized slug for a product.

Gets all selectable options for a product (for UI display).

Gets a shipping method by ID or UUID.

Gets a shipping method by ID or UUID, raises if not found.

Gets a shipping method by slug.

Gets storefront filter configuration from shop_config.

Lists active categories only (for storefront display).

Lists carts with filters for admin.

Lists all categories.

Lists categories with translated fields for a specific language.

Lists categories with count for pagination.

Returns a list of {name, id} tuples for products in a category that have images. Used for the featured product dropdown in the admin category form.

Lists categories that have no products assigned.

Lists all active import configs.

Lists recent import logs.

Lists categories visible in storefront navigation/menu. Only active categories appear in menus. Semantic alias for list_active_categories/1.

Lists all products with optional filters.

Lists products by their IDs.

Lists products with translated fields for a specific language.

Lists products with count for pagination.

Lists root categories (no parent).

Lists all shipping methods.

Lists categories whose products are visible in storefront. Includes both active and unlisted categories. Use for product filtering, not for navigation menus.

Marks abandoned carts (no activity for X days).

Merges guest cart into user cart after login. Accepts a user struct or user_uuid (string).

Merges localized fields from new attributes into existing product.

Returns a map of category_uuid => product_count for all categories.

Checks if a product slug exists for a language.

Generates a localized URL for a product.

Removes item from cart.

Sets payment option for cart.

Sets shipping method for cart.

Sets the shipping country for the cart.

Marks import as started.

Returns translation helpers module for direct access.

Updates item quantity in cart.

Updates a category.

Updates translation for a specific language on a category.

Updates an import config.

Updates an import log.

Updates a product.

Updates translation for a specific language on a product.

Updates a shipping method.

Saves storefront filter configuration.

Creates or updates a product by slug.

Validates selected_specs against product's option schema.

Functions

add_to_cart(cart, product, quantity \\ 1, opts \\ [])

Adds item to cart.

Options

  • :selected_specs - Map of selected specifications (for dynamic pricing)

Examples

# Add simple product
add_to_cart(cart, product, 2)

# Add product with specification-based pricing
add_to_cart(cart, product, 1, selected_specs: %{"material" => "PETG", "color" => "Gold"})

aggregate_filter_values(opts \\ [])

Aggregates filter values for sidebar display.

Returns a map of filter_key => aggregated data. For price_range: %{min: Decimal, max: Decimal} For vendor: [%{value: "Vendor", count: 5}, ...] For metadata_option: [%{value: "8 inches", count: 3}, ...]

Options:

  • :category_uuid - Scope aggregation to a specific category by UUID

auto_select_payment_option(cart, payment_options)

Auto-selects payment option if only one is available.

If cart already has a payment option selected, does nothing. If only one option is available, selects it.

auto_select_shipping_method(cart, shipping_methods)

Auto-selects the cheapest available shipping method for a cart.

If cart already has a shipping method selected, does nothing. If only one method is available, selects it. If multiple methods are available, selects the cheapest one.

bulk_delete_categories(ids)

Bulk delete categories. Returns count of deleted categories. Nullifies category references on orphaned products.

bulk_delete_products(ids)

Bulk delete products. Returns count of deleted products.

bulk_update_category_parent(ids, parent_uuid)

Bulk update category parent. Returns count of updated categories. Excludes the target parent from the update set to prevent self-reference. Uses a single UPDATE with subquery to resolve parent_uuid.

bulk_update_category_status(ids, status)

Bulk update category status. Returns count of updated categories.

bulk_update_product_category(uuids, category_uuid)

Bulk update product category. Returns count of updated products.

bulk_update_product_status(ids, status)

Bulk update product status. Returns count of updated products.

calculate_product_price(product, selected_specs)

Calculates the final price for a product based on selected specifications.

Applies option price modifiers (fixed and percent) to the base price. Fixed modifiers are applied first, then percent modifiers.

Example

product = %Product{price: Decimal.new("20.00")}
selected_specs = %{"material" => "PETG", "finish" => "Premium"}

# If PETG has +$10 fixed and Premium has +20% percent:
calculate_product_price(product, selected_specs)
# => Decimal.new("36.00")  # ($20 + $10) * 1.20

cart_url(language)

@spec cart_url(String.t()) :: String.t()

Generates a localized URL for the cart page.

Examples

iex> Shop.cart_url("ru")
"/ru/cart"

iex> Shop.cart_url("en")
"/cart"

catalog_url(language)

@spec catalog_url(String.t()) :: String.t()

Generates a localized URL for the shop catalog.

Examples

iex> Shop.catalog_url("es-ES")
"/es/shop"

iex> Shop.catalog_url("en")
"/shop"

category_options()

Returns categories as options for select input. Returns list of {localized_name, id} tuples.

category_slug_exists?(slug, language, opts \\ [])

Checks if a category slug exists for a language.

Examples

iex> Shop.category_slug_exists?("jarrones-macetas", "es-ES")
true

category_url(category, language)

@spec category_url(PhoenixKit.Modules.Shop.Category.t(), String.t()) :: String.t()

Generates a localized URL for a category.

Returns the correct locale-prefixed URL with translated slug.

Parameters

  • category - The Category struct
  • language - Language code (e.g., "en-US", "ru", "es-ES")

Examples

iex> Shop.category_url(category, "es-ES")
"/es/shop/category/jarrones-macetas"

iex> Shop.category_url(category, "en")
"/shop/category/vases-planters"  # Default language - no prefix

change_category(category, attrs \\ %{})

Returns a changeset for category form.

change_import_config(config, attrs \\ %{})

Returns a changeset for tracking import config changes.

change_product(product, attrs \\ %{})

Returns a changeset for product form.

change_shipping_method(method, attrs \\ %{})

Returns a changeset for shipping method form.

checkout_url(language)

@spec checkout_url(String.t()) :: String.t()

Generates a localized URL for the checkout page.

Examples

iex> Shop.checkout_url("ru")
"/ru/checkout"

iex> Shop.checkout_url("en")
"/checkout"

clear_cart(cart)

Clears all items from cart.

collect_product_file_uuids(product)

Collects all storage file UUIDs associated with a single product.

collect_products_file_uuids(product_uuids)

Collects all storage file UUIDs for a list of product UUIDs.

complete_import(import_log, stats)

Marks import as completed.

convert_cart_to_order(cart, opts)

Converts a cart to a Billing.Order.

Takes an active cart with items and creates an Order with:

  • All cart items as line_items
  • Shipping as additional line item (if selected)
  • Billing profile snapshot (from profile_uuid or direct billing_data)
  • Cart marked as "converted"

For guest checkout (no user_uuid on cart):

  • Creates a guest user via Auth.create_guest_user/1
  • Guest user has confirmed_at = nil until email verification
  • Sends confirmation email automatically
  • Order remains in "pending" status

Options

  • billing_profile_uuid: uuid - Use existing billing profile (for logged-in users)
  • billing_data: map - Use direct billing data (for guest checkout)

Returns

  • {:ok, order} - Order created successfully
  • {:error, :cart_not_active} - Cart is not active
  • {:error, :cart_empty} - Cart has no items
  • {:error, :no_shipping_method} - No shipping method selected
  • {:error, :email_already_registered} - Guest email belongs to confirmed user
  • {:error, changeset} - Validation errors

count_active_carts()

Counts active carts.

create_cart(opts)

Creates a new cart.

create_category(attrs)

Creates a new category.

create_import_config(attrs \\ %{})

Creates an import config.

create_import_log(attrs)

Creates a new import log entry.

create_product(attrs)

Creates a new product.

Automatically normalizes metadata (price modifiers, option values) before saving to ensure consistent storage format.

create_shipping_method(attrs)

Creates a new shipping method.

default_storefront_filters()

Returns the default storefront filter configuration.

delete_category(category)

Deletes a category.

delete_import_config(config)

Deletes an import config.

delete_import_log(import_log)

Deletes an import log.

delete_product(product)

Deletes a product.

delete_shipping_method(method)

Deletes a shipping method.

disable_system()

Disables the shop system.

discover_filterable_options()

Discovers filterable option keys from product metadata.

Returns a list of {key, product_count} tuples sorted by count descending. Used by admin UI to auto-suggest available filters.

enable_system()

Enables the shop system.

enabled?()

Checks if the shop system is enabled.

ensure_default_import_config()

Creates the legacy default import config if no configs exist.

Returns {:created, config} if a new config was created, or :exists if configs already exist.

ensure_prom_ua_import_config()

Ensures a default Prom.ua import config exists. Creates one if no config with name "prom_ua_default" is found.

expire_old_carts()

Expires old guest carts.

fail_import(import_log, error)

Marks import as failed.

find_active_cart(opts)

Finds active cart by user_uuid or session_id.

Search priority:

  1. If user_uuid is provided, search by user_uuid first
  2. If not found and session_id is provided, search by session_id (handles guest->login transition)
  3. If only session_id is provided, search by session_id with no user_uuid

find_product_by_slug_map(slug_map)

Finds an existing product by any slug in the provided slug map.

Searches through each slug value in the map to find a matching product. Returns the first product found, or nil if no match.

Examples

iex> find_product_by_slug_map(%{"en-US" => "planter"})
%Product{} | nil

iex> find_product_by_slug_map(%{"en-US" => "planter", "es-ES" => "maceta"})
%Product{} | nil  # Finds by first matching slug

format_product_price(product, currency, style \\ :from)

Formats the product price for catalog display.

Returns:

  • "$19.99" for products without price-affecting options
  • "From $19.99" if options have different price modifiers
  • "$19.99 - $38.00" for range display

get_available_shipping_methods(cart)

Gets available shipping methods for a cart. Filters by weight, subtotal, and country.

get_cart(uuid)

Gets a cart by ID or UUID with items preloaded.

get_cart!(id)

Gets a cart by ID or UUID, raises if not found.

get_category(id, opts \\ [])

Gets a category by ID or UUID.

get_category!(id, opts \\ [])

Gets a category by ID or UUID, raises if not found.

get_category_by_any_slug(slug, opts \\ [])

Finds a category by slug in any language.

Examples

iex> Shop.get_category_by_any_slug("jarrones-macetas")
{:ok, %Category{}, "es"}

get_category_by_slug(slug, opts \\ [])

Gets a category by slug.

Supports localized slugs stored as JSONB maps.

Options

  • :language - Language code for slug lookup (default: system default)
  • :preload - Associations to preload

Examples

iex> get_category_by_slug("planters")
%Category{}

iex> get_category_by_slug("kashpo", language: "ru")
%Category{}

get_category_by_slug_localized(slug, language, opts \\ [])

Gets a category by slug with language awareness.

Searches both translated slugs and canonical slug for the specified language.

Parameters

  • slug - The URL slug to search for
  • language - Language code (e.g., "es-ES" or base code "en")
  • opts - Options: :preload, :status

Examples

iex> Shop.get_category_by_slug_localized("jarrones-macetas", "es-ES")
{:ok, %Category{}}

get_category_slug(category, language)

Gets the localized slug for a category.

Examples

iex> Shop.get_category_slug(category, "es-ES")
"jarrones-macetas"

get_config()

Returns the current shop configuration.

get_dashboard_stats()

Returns dashboard statistics for the shop.

get_default_currency()

Gets the default currency struct from Billing module.

get_default_currency_code()

Gets the default currency code from Billing module. Falls back to "USD" if Billing has no default currency configured.

get_default_import_config()

Gets the default import config, if one exists.

get_default_language()

@spec get_default_language() :: String.t()

Gets the default language code (base code, e.g., "en").

Reads from Languages module configuration or falls back to "en".

get_enabled_storefront_filters()

Returns only enabled storefront filters, sorted by position.

get_import_config(uuid)

Gets an import config by ID.

get_import_config!(id)

Gets an import config by ID, raises if not found.

get_import_config_by_name(name)

Gets an import config by name.

get_import_log(id, opts \\ [])

Gets an import log by ID.

get_import_log!(id)

Gets an import log by ID, raises if not found.

get_or_create_cart(opts)

Gets or creates a cart for the current user/session.

Options

  • :user_uuid - User UUID (for authenticated users)
  • :session_id - Session ID (for guests)

get_price_affecting_specs(product)

Gets price-affecting options for a product.

Convenience wrapper around Options.get_price_affecting_specs_for_product/1.

get_price_range(product)

Gets the price range for a product based on option modifiers.

Returns {min_price, max_price} where:

  • min_price = minimum possible price (base + min modifiers)
  • max_price = maximum possible price (base + max modifiers)

Example

# Product with base $20, material options (0, +5, +10), finish options (0%, +20%)
get_price_range(product)
# => {Decimal.new("20.00"), Decimal.new("36.00")}

get_product(id, opts \\ [])

Gets a product by ID or UUID.

get_product!(id, opts \\ [])

Gets a product by ID or UUID, raises if not found.

get_product_by_any_slug(slug, opts \\ [])

Finds a product by slug in any language.

Searches across all translated slugs to find the product. Useful for cross-language redirect when user visits with a slug from a different language.

Examples

iex> Shop.get_product_by_any_slug("maceta-geometrica")
{:ok, %Product{}, "es"}

iex> Shop.get_product_by_any_slug("nonexistent")
{:error, :not_found}

get_product_by_slug(slug, opts \\ [])

Gets a product by slug.

Supports localized slugs stored as JSONB maps.

Options

  • :language - Language code for slug lookup (default: system default)
  • :preload - Associations to preload

Examples

iex> get_product_by_slug("planter")
%Product{}

iex> get_product_by_slug("kashpo", language: "ru")
%Product{}

get_product_by_slug_localized(slug, language, opts \\ [])

Gets a product by slug with language awareness.

Searches both translated slugs and canonical slug for the specified language.

Parameters

  • slug - The URL slug to search for
  • language - Language code (e.g., "es-ES" or base code "en")
  • opts - Options: :preload, :status

Examples

iex> Shop.get_product_by_slug_localized("maceta-geometrica", "es-ES")
{:ok, %Product{}}

iex> Shop.get_product_by_slug_localized("geometric-planter", "en")
{:ok, %Product{}}

get_product_slug(product, language)

Gets the localized slug for a product.

Returns translated slug if available, otherwise canonical slug.

Examples

iex> Shop.get_product_slug(product, "es-ES")
"maceta-geometrica"

get_selectable_specs(product)

Gets all selectable options for a product (for UI display).

Returns all select/multiselect options regardless of whether they affect price. This includes options like Color that may not have price modifiers but should still be selectable in the UI.

Convenience wrapper around Options.get_selectable_specs_for_product/1.

get_shipping_method(id)

Gets a shipping method by ID or UUID.

get_shipping_method!(id)

Gets a shipping method by ID or UUID, raises if not found.

get_shipping_method_by_slug(slug)

Gets a shipping method by slug.

get_storefront_filters()

Gets storefront filter configuration from shop_config.

Returns a list of filter definition maps with keys: key, type, label, enabled, position.

Default: price filter only.

list_active_categories(opts \\ [])

Lists active categories only (for storefront display).

list_carts_with_count(opts \\ [])

Lists carts with filters for admin.

list_categories(opts \\ [])

Lists all categories.

Options

  • :parent_uuid - Filter by parent UUID (nil for root categories)
  • :status - Filter by status: "active", "hidden", "archived", or list of statuses
  • :search - Search in name
  • :preload - Associations to preload

list_categories_localized(language, opts \\ [])

Lists categories with translated fields for a specific language.

Parameters

  • language - Language code for translations
  • opts - Standard list options

Examples

iex> Shop.list_categories_localized("es-ES", status: "active")
[%Category{localized: %{name: "Jarrones...", ...}}, ...]

list_categories_with_count(opts \\ [])

Lists categories with count for pagination.

list_category_product_options(category_uuid)

Returns a list of {name, id} tuples for products in a category that have images. Used for the featured product dropdown in the admin category form.

list_empty_categories()

Lists categories that have no products assigned.

list_import_configs(opts \\ [])

Lists all active import configs.

list_import_logs(opts \\ [])

Lists recent import logs.

list_menu_categories(opts \\ [])

Lists categories visible in storefront navigation/menu. Only active categories appear in menus. Semantic alias for list_active_categories/1.

list_products(opts \\ [])

Lists all products with optional filters.

Options

  • :status - Filter by status (draft, active, archived)
  • :product_type - Filter by type (physical, digital)
  • :category_uuid - Filter by category
  • :search - Search in title and description
  • :page - Page number
  • :per_page - Items per page
  • :preload - Associations to preload

list_products_by_ids(ids)

Lists products by their IDs.

Returns products in the order of the provided IDs.

list_products_localized(language, opts \\ [])

Lists products with translated fields for a specific language.

Returns products with an additional :localized virtual map containing translated fields with fallback to defaults.

Parameters

  • language - Language code for translations
  • opts - Standard list options: :page, :per_page, :status, :category_uuid, etc.

Examples

iex> Shop.list_products_localized("es-ES", status: "active")
[%Product{localized: %{title: "Maceta...", ...}}, ...]

list_products_with_count(opts \\ [])

Lists products with count for pagination.

list_root_categories(opts \\ [])

Lists root categories (no parent).

list_shipping_methods(opts \\ [])

Lists all shipping methods.

Options

  • :active - Filter by active status
  • :country - Filter by country availability

list_visible_categories(opts \\ [])

Lists categories whose products are visible in storefront. Includes both active and unlisted categories. Use for product filtering, not for navigation menus.

mark_abandoned_carts(days \\ 7)

Marks abandoned carts (no activity for X days).

merge_guest_cart(session_id, user_uuid)

Merges guest cart into user cart after login. Accepts a user struct or user_uuid (string).

merge_localized_attrs(existing, new_attrs)

Merges localized fields from new attributes into existing product.

Preserves existing translations while adding new ones from attrs. Non-localized fields are replaced entirely.

Examples

iex> merge_localized_attrs(%Product{title: %{"en-US" => "Old"}}, %{title: %{"es-ES" => "Nuevo"}})
%{title: %{"en-US" => "Old", "es-ES" => "Nuevo"}}

product_counts_by_category()

Returns a map of category_uuid => product_count for all categories.

product_slug_exists?(slug, language, opts \\ [])

Checks if a product slug exists for a language.

Useful for validation during translation editing.

Examples

iex> Shop.product_slug_exists?("maceta-geometrica", "es-ES")
true

iex> Shop.product_slug_exists?("maceta-geometrica", "es-ES", exclude_uuid: "some-uuid")
false

product_url(product, language)

@spec product_url(PhoenixKit.Modules.Shop.Product.t(), String.t()) :: String.t()

Generates a localized URL for a product.

Returns the correct locale-prefixed URL with translated slug. The URL respects the PhoenixKit URL prefix configuration.

Parameters

  • product - The Product struct
  • language - Language code (e.g., "en-US", "ru", "es-ES")

Examples

iex> Shop.product_url(product, "es-ES")
"/es/shop/product/maceta-geometrica"

iex> Shop.product_url(product, "ru")
"/ru/shop/product/geometricheskoe-kashpo"

iex> Shop.product_url(product, "en")
"/shop/product/geometric-planter"  # Default language - no prefix

remove_from_cart(item)

Removes item from cart.

set_cart_payment_option(cart, option)

Sets payment option for cart.

set_cart_shipping(cart, method, country)

Sets shipping method for cart.

set_cart_shipping_country(cart, country)

Sets the shipping country for the cart.

start_import(import_log, total_rows)

Marks import as started.

translations()

Returns translation helpers module for direct access.

Examples

iex> Shop.translations()
PhoenixKit.Modules.Shop.Translations

update_cart_item(item, quantity)

Updates item quantity in cart.

update_category(category, attrs)

Updates a category.

update_category_translation(category, language, attrs)

Updates translation for a specific language on a category.

Parameters

  • category - The category struct
  • language - Language code (e.g., "es-ES")
  • attrs - Translation attributes: name, slug, description

Examples

iex> Shop.update_category_translation(category, "es-ES", %{
...>   "name" => "Jarrones y Macetas",
...>   "slug" => "jarrones-macetas"
...> })
{:ok, %Category{}}

update_import_config(config, attrs)

Updates an import config.

update_import_log(import_log, attrs)

Updates an import log.

update_import_progress(import_log, attrs)

Updates import progress.

update_product(product, attrs)

Updates a product.

Automatically normalizes metadata (price modifiers, option values) before saving to ensure consistent storage format.

update_product_translation(product, language, attrs)

Updates translation for a specific language on a product.

Parameters

  • product - The product struct
  • language - Language code (e.g., "es-ES")
  • attrs - Translation attributes: title, slug, description, body_html, seo_title, seo_description

Examples

iex> Shop.update_product_translation(product, "es-ES", %{
...>   "title" => "Maceta Geométrica",
...>   "slug" => "maceta-geometrica"
...> })
{:ok, %Product{}}

update_shipping_method(method, attrs)

Updates a shipping method.

update_storefront_filters(filters)

Saves storefront filter configuration.

upsert_product(attrs)

Creates or updates a product by slug.

Uses explicit find-or-create pattern with proper localized field merging. After V47 migration, slug is a JSONB map (e.g., %{"en-US" => "my-slug"}), so ON CONFLICT doesn't work correctly - this function handles the lookup manually.

Returns {:ok, product, action} where action is :inserted or :updated.

Parameters

  • attrs - Product attributes including localized fields as maps

Examples

# Create new product
iex> upsert_product(%{title: %{"en-US" => "Planter"}, slug: %{"en-US" => "planter"}, price: 10})
{:ok, %Product{}, :inserted}

# Update existing product (found by slug)
iex> upsert_product(%{title: %{"en-US" => "Planter V2"}, slug: %{"en-US" => "planter"}, price: 15})
{:ok, %Product{}, :updated}

# Add translation to existing product
iex> upsert_product(%{title: %{"es-ES" => "Maceta"}, slug: %{"es-ES" => "maceta", "en-US" => "planter"}, price: 10})
{:ok, %Product{title: %{"en-US" => "Planter", "es-ES" => "Maceta"}}, :updated}

validate_selected_specs(product, selected_specs)

Validates selected_specs against product's option schema.

Checks:

  • All spec keys exist in the option schema
  • All spec values are in allowed values list (if defined)
  • All required options have values

Returns

  • :ok - All specs are valid
  • {:error, :unknown_option_key, key} - Key not in schema
  • {:error, :invalid_option_value, %{key: key, value: value, allowed: list}} - Value not allowed
  • {:error, :missing_required_option, key} - Required option not provided

Examples

iex> validate_selected_specs(product, %{"material" => "PETG"})
:ok

iex> validate_selected_specs(product, %{"material" => "Unobtainium"})
{:error, :invalid_option_value, %{key: "material", value: "Unobtainium", allowed: ["PLA", "PETG"]}}