Current section
Files
Jump to
Current section
Files
phoenix_kit_ecommerce
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## 0.1.13 - 2026-08-04
Checkout security and wrong-money fixes (PR #12), plus the post-merge review
in `dev_docs/pull_requests/2026/12-checkout-security-policy-settings/`.
0.1.12 was tagged but never published, so its changes ship here too.
### Added
- **`PhoenixKitEcommerce.Policy`** — seven admin-controllable policy keys behind
one module, so the settings UI and the enforcement points cannot disagree
about a default. Every key ships in its safe position and every reader fails
**closed** on a settings-layer error: `shop_order_lookup_policy`,
`shop_allow_raw_html_descriptions`, `shop_allow_svg_uploads`,
`shop_image_import_allow_private_networks`, `shop_default_tax_country`,
`shop_import_cleanup_scope`, `shop_legacy_cookie_until`. Genuine invariants
deliberately get no setting. All policy changes are activity-logged.
- **`css_sources/0`** — documented in the README since forever, never
implemented, so Tailwind purged every class used only by this module's
templates from the host build. The absence was masked whenever any other
PhoenixKit module was installed.
- **`PhoenixKitEcommerce.Import.Money`** — one supplier-money parser shared by
every import format.
- First test suites for `Options` pricing, the import money parser, the
security regressions, guest order access, and the admin-permission contract.
### Fixed
- **Billing-profile IDOR.** `select_profile` accepted any client-supplied uuid
and order creation copied that profile's name, address, phone and email onto
the attacker's order. Now checked at selection, at `confirm_order`, and again
in the context — `convert_cart_to_order/2` is public and re-exported, so it
must not depend on one caller remembering.
- **Order confirmation pages were world-readable.** Access was granted for any
order with a nil `user_uuid` and for any order whose owner was unconfirmed —
and guest checkout creates exactly such users. Orders now record the placing
shop session, with a `cart_uuid → cart.session_id` fallback so pre-existing
guest orders keep working.
- **Cart takeover on a shared browser.** The logged-in fallback matched carts by
`session_id` without the `is_nil(user_uuid)` guard, and the cookie outlives
logout by 30 days. The cookie is now signed, `SameSite=Lax`, Secure-on-HTTPS,
with a narrow one-time migration for pre-signing cookies.
- **Stored XSS on the storefront** via `sanitize={false}` and a bare `raw/1`.
- **SSRF in the CSV image importer**, three ways: redirects were never
re-validated, IPv4-mapped IPv6 bypassed the private-range check entirely
(`::ffff:127.0.0.1` read as public), and no guard existed before that.
Post-merge: both address families are now resolved, so an IPv6-only image
host is no longer blocked outright and a public-A/private-AAAA host no longer
slips through.
- **Draft, inactive and archived products were public and purchasable**, via two
separate paths.
- **Tax was zero on every order** — the cart page leaves `shipping_country` nil
and checkout never set it. Fixing that surfaced two more: tax was charged on
the whole subtotal while `CartItem` carries a `taxable` flag, and the review
screen showed a pre-tax total while conversion charged tax.
- **Percentage discounts did nothing** — the percent branch was gated on
`compare(sum, 0) == :gt`, so a negative percent was silently discarded.
**Price ranges came out inverted** (`Enum.min/max` compare `%Decimal{}` by
Erlang term order, not value). **Fixed-only prices were never rounded.**
Post-merge: all three also affected `get_price_range/3` — the storefront's
"From $X" — which kept its own copy of the arithmetic. Both functions now
share one helper.
- **CSV prices were silently wrong** — `"12,50"` imported as 12, and the first
hardening multiplied `"1.2345"` by 10,000. Post-merge: the Shopify
`"Variant Price"` path still truncated the same way, and since `base_price` is
the minimum variant price, one mangled row priced every variant.
- **An unreadable price became free stock** — the parser returned nil and the
call site turned it into `Decimal.new(0)`. The row now fails validation.
- **Free shipping** via a method the cart had outgrown; **lost cart totals**
under concurrency; **cart edits could commit after the order was created**.
- Confirmation email moved outside the conversion transaction, so an SMTP
failure can no longer roll back a paid order.
- Import cleanup no longer deletes every empty category in the catalog.
- Five LiveViews had no `handle_info` catch-all; malformed `phx-value-*`
payloads crashed sockets through `String.to_integer/1`.
- Guest carts orphaned by a fabricated session id on the cross-language
product path.
### Changed
- Category admin actions re-check `Scope.has_module_access?(scope, "shop")`
rather than `can_access_admin_area?/1`, which is true for any permission
holder.
- 25 new strings extracted and translated into ru and et.
- Docs: the Settings Keys sections in `README.md` and `AGENTS.md` listed four
unprefixed keys the code never read.
## 0.1.12 - 2026-07-27
### Changed
- **Stop calling the deprecated `PhoenixKit.Users.Auth.Scope.admin?/1`.** All 4
call sites (`Web.Categories`) now call `Scope.can_access_admin_area?/1`, the
name core renamed it to in phoenix_kit 1.7.214. The old name is a pure
`@deprecated` delegate, so **no behavior change** — this only silences the
deprecation warning host apps were eating on every compile of this library,
with no way to fix it themselves.
- **Dependency floor raised to `phoenix_kit ~> 1.7.214`** (from `~> 1.7.189`) —
`can_access_admin_area?/1` does not exist below it, so an older core would be
an `UndefinedFunctionError` at call time rather than a warning. This was not
hypothetical: the lockfile was resolving 1.7.194.
- Dependency lockfile bumps: `phoenix_kit` 1.7.194 → 1.7.216, `phoenix_live_view`
1.2.7 → 1.2.8, `beamlab_ex_aws_sqs` 4.0.0 → 5.0.0, `beamlab_countries` 1.0.8 →
1.1.0, `fresco` 0.8.0 → 0.10.0, `etcher` 0.7.2 → 0.9.0, `ex_ast` 0.12.10 →
0.13.1, `elixir_make` 0.9.0 → 0.10.0, `mdex` 0.13.3 → 0.13.4, `mdex_native`
0.2.5 → 0.2.6, `hackney` 4.5.2 → 4.6.0, `req` 0.6.2 → 0.6.3, `tessera` 0.3.2 →
0.3.4, `bandit` 1.12.0 → 1.12.4, `igniter` 0.8.2 → 0.8.3, `leaf` 0.3.0 → 0.3.2,
plus `plug_crypto`, `mint`, `quic`, `lazy_html`, `glob_ex`, `earmark_parser`.
## 0.1.11 - 2026-07-16
### Added
- **Admin i18n wave.** All 12 admin LiveViews (carts, categories, category
form, dashboard, import show, imports, product detail, product form,
products, settings, shipping method form, shipping methods) are now
fully gettext-wrapped, using the module-owned `PhoenixKitEcommerce.Gettext`
backend consistently (replacing leftover `PhoenixKitWeb.Gettext` calls).
The `ru`/`et` catalogs were extended from 483 to 2,500+ msgids.
### Fixed
- **Gaps in the i18n wave above, found in post-merge review.** The
product-form Translations tab (title/slug/description/SEO field labels
and placeholders), several product-form help strings, the products-list
empty state, the CSV-import confirm-step summary, an import-details
notice, and a category-option delete confirmation were left unwrapped
and rendered in English regardless of locale — not caught by
`mix gettext.extract --check-up-to-date`, since that check only verifies
already-wrapped calls, not coverage. All wrapped and translated
(en/ru/et).
- **Cart and shipping-method page-header counts pluralize correctly.**
`carts.ex` and `shipping_methods.ex` used plain `gettext` for their
"%{count} ... total/configured" headers instead of `ngettext`, so the
Russian translation hardcoded a genitive-plural noun regardless of
count — rendering "1 корзин всего" / "1 методов настроено" at
`count = 1`. Converted to `ngettext`, matching the categories/products
pattern, with correct 3-form ru and 2-form et translations.
- Corrected 12 catalog entries gettext's fuzzy-matcher mis-filled from an
unrelated adjacent label while merging the strings above (e.g. "No
products found" pre-filled with the Russian/Estonian translation of "No
categories found").
See `dev_docs/pull_requests/2026/11-admin-i18n-wave/CLAUDE_REVIEW.md` for
the full review.
## 0.1.10 - 2026-07-12
### Fixed
- **Search terms are escaped before hitting ILIKE.** User input flowed
into the `%term%` pattern raw, so `%`/`_`/`\` acted as wildcards on a
route open to unauthenticated visitors: searching `100%` matched every
product containing "100", an SKU search for `AB_100` also matched
`ABX100`, and a term ending in `\` silently corrupted the pattern into
requiring a literal `%`. Terms are now length-capped (100 chars),
NUL-stripped (a crafted `%00` raised `Postgrex.Error`), and
LIKE-escaped in one choke point (`search_like_pattern/1`) shared by the
product, category, and admin-cart search filters. The cap also closes a
cheap unauthenticated seq-scan amplifier (unbounded multi-KB patterns
against six ILIKE evaluations per row, two of them `jsonb_each_text`
expansions).
- **Category pages now show a filters-aware zero-result state.** With an
active search (or price/vendor filter) matching nothing, the category
page claimed "No products in this category — check back soon" even
though the category has products; it now says the filters matched
nothing and offers a clear-filters affordance, matching the main
catalog page. Both the guest and authenticated layouts share one
`category_empty_state` component now.
- Documented (code comment) the cross-release ordering limitation of
`merge_missing_builtin_filters/1`'s below-minimum positioning scheme:
it holds within one merge pass; a hypothetical second sidebar-leading
built-in added in a future release should renormalize positions on
save instead of extending the scheme.
## 0.1.9 - 2026-07-11
### Added
- **Storefront product search.** A new built-in `search` filter type lets
customers search the public catalog (main page and category pages) by
product name, description, SKU (`metadata->>'sku'`), and tags — matching
in any configured language. Renders as a search box at the top of the
storefront filter sidebar, driven by a `?search=` URL param so results
are shareable and survive navigation. Enabled by default for new
installs; existing installs discover it on the Settings page (merged in
disabled via `merge_missing_builtin_filters/1`) and enable it with one
toggle.
- Context: `list_products/1` `:search` now also matches SKU and tags
(previously title/description only), so admin product search finds
products by article number too.
### Fixed
- **Storefront search no longer raises `ambiguous_column`.** The `:search`
SQL fragment referenced unqualified `title`/`description` columns, which
turn ambiguous the moment `:exclude_hidden_categories` joins the
categories table — the exact combination every public catalog query
uses. Columns are now bound through the product binding. Pinned by a
regression test.
- **`merge_missing_builtin_filters/1` no longer collides positions.** A
saved config from before `search` existed can already hold another
filter at the same default position (e.g. `price` at `0`); the merged-in
filter now sorts strictly below every saved filter's position instead of
reusing its default position outright, so it renders first once enabled
as intended, even on upgraded installs.
## 0.1.8 - 2026-06-08
### Added
- Developer tooling: `pk_dep/3` in `mix.exs` resolves any `phoenix_kit*` dependency from a local checkout when `<APP>_PATH` is exported (e.g. `PHOENIX_KIT_PATH=../phoenix_kit mix test`), for cross-repo development. Unset — the default — keeps the published Hex pin, so `mix hex.publish` and CI resolve exactly as before. A set-but-blank value (`PHOENIX_KIT_PATH=`) is also treated as unset rather than producing a broken `path: ""` dep. Documented in `AGENTS.md`.
### Changed
- Refresh dependency lock: `phoenix_kit` 1.7.131 → 1.7.133 and `phoenix_kit_billing` 0.4.0 → 0.5.0, plus transitive bumps (`bandit` 1.11.1 → 1.12.0, `etcher` 0.6.5 → 0.6.6, `fresco` 0.6.3 → 0.7.1, `req` 0.5.18 → 0.6.1, `spitfire` 0.3.12 → 0.3.13, `tesla` 1.18.3 → 1.20.0). Declared dependency requirements are unchanged; no library or migration code changed.
## 0.1.7 - 2026-06-05
### Changed
- Migrate the category and product admin forms to core form components. Non-translatable scalar fields (status, product_type, vendor, price/compare_at/cost, position) and the changeset-backed selects (`parent_uuid`, `featured_product_uuid`, `category_uuid`) now render through core `<.input>`/`<.select>`. An `assign_form/2` helper keeps `:changeset` (consumed by the multilang `TranslationTabs` fields) and `:form = to_form(changeset)` in sync, so inline validation errors render on `validate`. Translatable fields, dynamic-option selects, and the option/media machinery are intentionally left as-is.
- Route web body strings through the module's own `PhoenixKitEcommerce.Gettext` backend (was the parent app's `PhoenixKitWeb.Gettext`). Generic UI-chrome strings are still resolved against core to keep its vetted translations; shop-domain strings resolve in-module and ship `et`/`ru` catalogues (correct 2-form/3-form plurals).
### Fixed
- Inline validation errors now render on the migrated `<.select>` fields, via the `phoenix_kit` 1.7.131 core `Select` component (it now maps `field.errors` like `<.input>` does).
### Dependencies
- Bump `phoenix_kit` 1.7.130 → 1.7.131 (core `<.select>` surfaces `field.errors`) and `etcher` 0.5.5 → 0.6.5.
## 0.1.6 - 2026-06-04
### Fixed
- Order `tax_rate` persisted as `0` on cart→order conversion. `build_order_attrs/3` hardcoded a `0` rate while `tax_amount` was computed from the live rate, so every order recorded a 0% rate — breaking receipts, tax reports, and refund math that derive the percentage from the order. Now uses `get_tax_rate(cart)` (single source of truth). Pinned by an end-to-end regression test.
- `unique_constraint` names for `ShippingMethod` (`:slug`) and `ImportConfig` (`:name`, `:uuid`) did not match the actual DB indexes, so duplicate inserts raised `Ecto.ConstraintError` instead of returning `{:error, changeset}`. Pinned to the real index names.
- No-billing tax fallback applied the configured `billing_default_tax_rate` even when `billing_tax_enabled` was `false`; now gated on the enabled flag, matching the `PhoenixKitBilling`-loaded path.
- `DialectMapper.resolve_dialect/2` → `/1` after the `phoenix_kit` upgrade dropped the 2-arity form, restoring a clean `--warnings-as-errors` build.
### Added
- `PhoenixKitEcommerce.Activity` — PII-safe LiveView-layer activity-logging wrapper around `PhoenixKit.Activity`. Logs admin mutations (products incl. bulk, categories, shipping methods, import configs/runs) and never crashes the caller (no-ops when core's `PhoenixKit.Activity` is absent; rescues DB errors).
- `PhoenixKitEcommerce.Errors` — central gettext-backed mapping from error atoms to user-facing strings (`PhoenixKitEcommerce.Gettext` backend). Wired into the `:cart_not_active` and `:unknown_format` paths.
- Test harness: `DataCase`/`LiveCase`, test endpoint/router/layouts, schema via `PhoenixKit.Migration.ensure_current/2`, and full schema/context/LiveView coverage (6 → 183 tests).
### Changed
- Cap `include_keywords`/`exclude_keywords`/`exclude_phrases` import filters at 100 entries each with a friendly changeset error (was unbounded).
- Centralize the nil-currency `$` fallback as a single documented module attribute.
- Migrate the shipping-method form to core components (`<.input>`/`<.select>`/`<.textarea>` with `assign_form/2`) for inline validation errors.
- Log a warning on `billing_default_tax_rate` parse failure instead of silently falling back to `0`.
- Refresh `mix.lock` for the upgraded `phoenix_kit` and related dependencies.
## 0.1.5 - 2026-05-09
### Fixed
- Mark `PhoenixKit.Modules.Shop` compat module as `@moduledoc false` to silence ex_doc warnings about `defdelegate` pointing at hidden `@impl PhoenixKit.Module` callbacks. Docs-only change; no behavioural difference.
### Added
- Broaden i18n test coverage to iterate `admin_tabs/0 ++ settings_tabs/0 ++ user_dashboard_tabs/0` (was admin-only). Add direct `Gettext.gettext(EcommerceGettext, ...)` assertions independent of `Tab.localized_label/1`.
## 0.1.4 - 2026-05-08
### Added
- Per-module Gettext backend (`PhoenixKitEcommerce.Gettext`) with `en`/`ru`/`et` catalogues for all admin sidebar tab labels. Requires `phoenix_kit` release that ships the `gettext_backend` Tab API ([BeamLabEU/phoenix_kit#522](https://github.com/BeamLabEU/phoenix_kit/pull/522)); on older releases tabs render raw English (graceful degradation).
## 0.1.3 - 2026-04-06
### Added
- Add `version/0` callback to display package version on modules page
### Changed
- Remove deprecated `select-bordered` class for daisyUI 5 compatibility
- Expand compat module with full delegation list
- Add `elixirc_options: [ignore_module_conflict: true]` for umbrella compatibility
### Fixed
- Fix compilation errors after core changes
## 0.1.2 - 2026-03-30
### Added
- Compat alias modules (`lib/phoenix_kit_ecommerce/compat/`) bridging old `PhoenixKit.Modules.Shop.*` namespace to `PhoenixKitEcommerce.*` for backward compatibility
- Billing module as single source of truth for tax rates with Settings fallback
### Changed
- Remove explicit `LayoutWrapper.app_layout` from 14 admin LiveViews (core now auto-applies admin layout; kept in `shop_layouts.ex` for public storefront)
- Convert 5 admin list pages (carts, categories, products, shipping methods, import configs) to `table_default` + `table_row_menu` components
- Remove 65 duplicate files from old `lib/phoenix_kit/modules/shop/` namespace and 2 duplicate mix tasks
### Fixed
- Add admin authorization check (`Scope.admin?/1`) to individual category delete, matching bulk operations
- Replace raw `<tr><td>` empty-state rows with proper `table_default` components in shipping methods and import configs
- Fix weight formatting in shipping methods to use `Float.round/2` for precision consistency with carts
## 0.1.1 - 2026-03-29
### Changed
- Restructure from `lib/phoenix_kit/modules/shop/` to `lib/phoenix_kit_ecommerce/` matching standard extracted-repo convention
- Rename all modules from `PhoenixKit.Modules.Shop.*` to `PhoenixKitEcommerce.*`
- Update billing references from `PhoenixKit.Modules.Billing.*` to `PhoenixKitBilling.*`
- Move mix tasks to `lib/phoenix_kit_ecommerce/mix_tasks/`
- Add `:mix` to dialyzer PLT apps for clean dialyzer runs
### Fixed
- Fix 33 nesting depth violations (max depth 2) by extracting helper functions and using `with` chains
- Fix 11 cyclomatic complexity violations (max 9) by splitting large functions into multi-clause helpers
- Fix 25 alias ordering issues across all modules
- Fix dead code in `get_mapped_image/4` where `|| current_image` could never trigger
- Add `elixirc_paths/1`, `aliases/0` (quality/precommit) to mix.exs matching sibling conventions
### Added
- Comprehensive README with features, installation, usage examples, architecture, and troubleshooting
- AGENTS.md for AI agent guidance
- CHANGELOG.md for release tracking
- MIT LICENSE file
## 0.1.0 - 2026-03-29
### Added
- Initial e-commerce module with PhoenixKit.Module behaviour
- Product catalog with physical and digital product support
- Multi-language content (titles, slugs, descriptions, SEO fields)
- Hierarchical categories with nesting and per-category option schemas
- Dynamic product options with fixed and percentage price modifiers
- Shopping cart with guest (session-based) and user (persistent) modes
- Cart real-time sync across tabs via PubSub
- Checkout flow with PhoenixKitBilling integration for order conversion
- Shipping methods with weight/price constraints and geographic restrictions
- CSV import system with automatic format detection (Shopify, Prom.ua, generic)
- Oban workers for async CSV imports and image migration
- Admin dashboard with product, category, shipping, cart, and import management
- Public storefront pages (catalog, category, product detail, cart, checkout)
- User order history and order detail pages
- Import configuration profiles with keyword filtering and category rules
- Product deduplication mix task