Current section

Files

Jump to

mix.exs

defmodule PhoenixKitEcommerce.MixProject do
  use Mix.Project

  @version "0.5.21"
  @source_url "https://github.com/BeamLabEU/phoenix_kit_ecommerce"

  def project do
    [
      app: :phoenix_kit_ecommerce,
      version: @version,
      elixir: "~> 1.18",
      elixirc_paths: elixirc_paths(Mix.env()),
      # `compat/shop.ex` intentionally redefines the core
      # `PhoenixKit.Modules.Shop` namespace during the transition to the
      # `PhoenixKitEcommerce` namespace, which triggers a module-redefinition
      # warning that `--warnings-as-errors` would otherwise fail on.
      # Removal condition: drop this once core no longer ships the old
      # `PhoenixKit.Modules.Shop` namespace (then delete compat/shop.ex too).
      elixirc_options: [ignore_module_conflict: true],
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases(),
      test_ignore_filters: [~r"/support/"],
      test_coverage: [
        ignore_modules: [
          ~r/^PhoenixKitEcommerce\.Test\./,
          PhoenixKitEcommerce.DataCase,
          PhoenixKitEcommerce.LiveCase,
          PhoenixKitEcommerce.ActivityLogAssertions
        ]
      ],

      # Hex
      description: "E-commerce module for PhoenixKit — products, categories, cart, checkout",
      package: package(),

      # Dialyzer
      dialyzer: [plt_add_apps: [:phoenix_kit, :phoenix_kit_billing, :mix]],

      # Docs
      name: "PhoenixKitEcommerce",
      source_url: @source_url,
      docs: docs()
    ]
  end

  def application do
    [extra_applications: [:logger, :gettext]]
  end

  def cli do
    [preferred_envs: ["test.setup": :test, "test.reset": :test]]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp aliases do
    [
      quality: ["format", "credo --strict", "dialyzer"],
      "quality.ci": ["format --check-formatted", "credo --strict", "dialyzer"],
      precommit: [
        "compile --force --warnings-as-errors",
        "deps.unlock --check-unused",
        # Scan for retired Hex deps. Run via `cmd` so Hex bootstraps in a fresh
        # process — the hex.* archive tasks aren't resolvable via Mix.Task.run
        # inside an alias.
        "cmd mix hex.audit",
        "quality.ci"
      ],
      "test.setup": [
        "ecto.create --quiet -r PhoenixKitEcommerce.Test.Repo"
      ],
      "test.reset": [
        "ecto.drop --quiet -r PhoenixKitEcommerce.Test.Repo",
        "test.setup"
      ]
    ]
  end

  # phoenix_kit deps resolve from Hex by default. For cross-repo work against a
  # local checkout, export <APP>_PATH — e.g. PHOENIX_KIT_PATH=../phoenix_kit or
  # PHOENIX_KIT_AI_PATH=../phoenix_kit_ai. Unset => the published pin, so
  # mix hex.publish is unaffected.
  defp pk_dep(app, requirement, opts \\ []) do
    env_var = String.upcase(Atom.to_string(app)) <> "_PATH"

    # A set-but-blank value (`PHOENIX_KIT_PATH=`) reads as "" rather than nil;
    # treat it as unset so it falls back to the Hex pin instead of building a
    # broken `path: ""` dep.
    case System.get_env(env_var) do
      path when is_binary(path) and path != "" ->
        {app, [path: path, override: true] ++ opts}

      _unset when opts == [] ->
        {app, requirement}

      _unset ->
        {app, requirement, opts}
    end
  end

  defp deps do
    [
      # The floor is 2.43.0: `ImageFingerprint` runs ImageMagick through
      # core's `ImageProcessor.limit_args/0` and `pinned_input/2` (resource
      # limits, decoder pinned to the sniffed format), first shipped there.
      # That subsumes 2.38.0 — the actor and the activity log
      # (`PhoenixKitWeb.Actor`, `PhoenixKit.Activity.log/3`), not
      # feature-detected, so a lower core fails to compile.
      # Everything the earlier floors guaranteed is subsumed: V186's
      # `base_currency`/`exchange_rate`/`base_unit_price` cart columns (2.16.0
      # — below it every cart write raised `undefined_column`),
      # `Slug.put_slug/3` (2.4.0) and V171's projection pkeys (2.6.0).
      # Patch-precise floor in the compound form, so the ceiling stays open
      # through every later 2.x minor (a three-segment `~> 2.43.0` would pin
      # one minor; see test/core_pin_conformance_test.exs).
      pk_dep(:phoenix_kit, ">= 2.43.0 and < 3.0.0"),

      # Gettext for per-module i18n of sidebar tab labels.
      {:gettext, "~> 1.0"},

      # Billing integration for checkout and order conversion.
      # 0.13 is the floor (raised from 0.11 for per-domain-currency Э2,
      # plan §0.3/§8.5): `PhoenixKitEcommerce.Events.subscribe_currencies/0`
      # calls `PhoenixKitBilling.Events.subscribe_currencies/0` (subscribed
      # from every storefront LiveView that shows a converted price —
      # `catalog_category.ex`, `shop_catalog.ex`, `catalog_product.ex`,
      # `checkout_page.ex`), which does not exist below the release
      # carrying Э2's `{:currencies_changed, code}` PubSub event (CHANGELOG
      # 0.13.0, merged upstream 2026-09-07 as PR #33). Below the floor this
      # is `UndefinedFunctionError` on the storefront the moment a shopper
      # opens the catalog, not at compile time — exactly what §8.5's hard
      # floor exists to make impossible to install. 0.11's own reasons
      # remain true too: `create_cart/1` and both add-to-cart paths call
      # `PhoenixKitBilling.get_base_currency/0`, `get_display_currency/0`,
      # `resolve_display_currency/1`, and `Currency.present/3`/
      # `effective_rate/2`; 0.5.2's `payment_option_uuid` on
      # `PhoenixKitBilling.Order` also still required — both now subsumed
      # by the higher floor.
      #
      # 0.20 raised it again: checkout renders `BillingProfileFields` and
      # builds its form over `BillingProfile.fields_changeset/3` /
      # `form_fields/0`, none of which exist in 0.19.x - there the checkout
      # page is an `UndefinedFunctionError` the moment a shopper opens it,
      # not a compile error.
      pk_dep(:phoenix_kit_billing, "~> 0.20"),
      # Optional: only the AI-translate UI/adapter use it, and both compile out
      # when it's absent (see ProductForm's @ai_translate? flag). Version tracks
      # the actual API used (Translatable behaviour, AITranslate components).
      #
      # FLOOR RAISED for the translation-control design: the translation
      # prompts this package rolls out (`AITranslatable`,
      # `CategoryAITranslatable`) carry their entire source section as
      # `{{SourceFields}}` — a variable bound only by phoenix_kit_ai
      # 0.20.0, the release carrying the design's §9.1 change. An older
      # engine renders `{{SourceFields}}` literally, so every translation
      # call would go out with an empty SOURCE section: it costs money,
      # fails to parse, and nothing but a log warning says why.
      #
      # 0.20.0 also carries §9.3 (`put_translation/4` receiving the
      # source fields the worker actually read via `opts[:source_fields]`).
      # Without it the staleness model of design §4.1 is inert rather than
      # merely degraded: every write arrives with no source to hash, so
      # nothing is ever fingerprinted, write-narrowing never engages, and
      # each write erases the field's reference instead (see
      # `TranslationFingerprint.apply_writes/3` — that erase is what keeps
      # the sweep convergent against an engine lacking §9.3, at the cost
      # of every touched pair falling back to `:unknown` for the operator
      # to resolve by hand).
      #
      # RAISED AGAIN to 0.24: the sweep worker runs on the engine's shared
      # `PhoenixKitAI.TranslationSweep` (its tick, gates, candidate
      # selection and ceiling), first shipped in 0.24.0. The worker
      # feature-detects the engine, so an older one degrades to "sweep
      # unavailable" rather than crashing — but `~> 0.24` is what makes the
      # sweep exist at all.
      pk_dep(:phoenix_kit_ai, "~> 0.24", optional: true),
      # Optional: only the "Stock tracking" switch uses it. Without it the
      # switch is shown disabled with a pointer to Modules -> Not installed
      # (see `PhoenixKitEcommerce.StockTracking`).
      pk_dep(:phoenix_kit_warehouse, "~> 0.5", optional: true),
      # No declared dependency on `phoenix_kit_catalogue`, though this app
      # DOES call into `PhoenixKitCatalogue` directly (the ProductSource
      # catalogue adapter, `Catalogue.Writer`, and the Shopify-sync 6a
      # path call `Catalogue.{get_item_by_slug/3, get_category_by_slug/3,
      # get_item!/1, create_item/2, update_item/2, list_catalogues/0,
      # translated_*}`, `Catalogue.AttributeSets.{resolve_for_item/2,
      # resolve_for_items/2, list_sets/1}`, `Catalogue.Slugs.from_title/2`
      # and several `Schemas.*` structs — none of that is duck-typed).
      # It IS true for the older extension-slot integration
      # (`PhoenixKitEcommerce.Catalogue.Extension`), which is fully
      # duck-typed and calls nothing in `PhoenixKitCatalogue` directly.
      # No released version yet ships the API the ProductSource adapter
      # targets, several of which exist only on the fork's own
      # `feature/shop-extensions` branch — so a `~>` floor here could
      # only be inaccurate, and every one of those call sites is
      # unreachable unless a host resolves that exact branch. Add
      # `pk_dep(:phoenix_kit_catalogue, "~> <floor>", optional: true)`
      # once a release carries the API.
      #
      # `catalogue_test_deps/0` below is the narrow exception: a
      # TEST-ONLY, path-only, opt-in bridge so the `:catalogue`-tagged
      # tests in this repo (`writer_images_test.exs`,
      # `writer_variants_test.exs`, `collection_sync_test.exs`,
      # `shopify_sync_media_panel_test.exs`, `product_source/catalogue/*`)
      # can actually execute against a real local checkout instead of
      # staying excluded forever — set `PHOENIX_KIT_CATALOGUE_PATH` and
      # `PHOENIX_KIT_ENTITIES_PATH` (matching branches the host app
      # itself pins) and run `mix test`; leave them unset and no dependency
      # is added by the bridge. The bridge itself never resolves via Hex,
      # so the "no released version" rationale above is untouched. Note
      # that the optional `phoenix_kit_warehouse` dependency above pulls
      # `phoenix_kit_catalogue` (and `phoenix_kit_entities`) in from Hex
      # transitively; with it resolved `test_helper.exs` no longer excludes
      # `:catalogue` and those tests run by default.

      # LiveView is needed for the admin and storefront pages.
      {:phoenix_live_view, "~> 1.1"},

      # Phoenix web framework (controllers, routing).
      {:phoenix, "~> 1.7"},

      # Ecto for database queries and schemas.
      {:ecto_sql, "~> 3.12"},

      # Background job processing (CSV imports, image migration).
      {:oban, "~> 2.20"},

      # UUIDv7 primary key generation.
      {:uuidv7, "~> 1.0"},

      # CSV parsing for product imports.
      {:nimble_csv, "~> 1.2"},

      # HTTP client for downloading product images.
      {:req, "~> 0.5"},

      # JSON encoding/decoding.
      {:jason, "~> 1.4"},

      # Documentation generation.
      {:ex_doc, "~> 0.39", only: :dev, runtime: false},

      # Code quality.
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},

      # `Phoenix.LiveViewTest` parses HTML via `lazy_html` for `element/2`,
      # `render(view) =~ "..."`, etc. Test-only.
      {:lazy_html, ">= 0.1.0", only: :test},

      # `html_to_markdown_test.exs` renders the converter's Markdown output
      # back through `MDEx.to_html!/1` to assert on CommonMark nesting
      # semantics. Declared explicitly (rather than relying on the
      # transitive resolution through `phoenix_kit`) so the test suite
      # doesn't depend on another dependency's dependency tree. No
      # `only: :test` here: `leaf` (a `phoenix_kit` dep) already
      # requires `mdex` unrestricted to every environment, and Mix
      # rejects a narrower `:only` than what's already resolved for the
      # same package. Version matches the constraint `phoenix_kit`/`leaf`
      # already lock to.
      {:mdex, "~> 0.13"}
    ] ++ catalogue_test_deps()
  end

  # See the comment above `pk_dep(:phoenix_kit_catalogue, ...)`. Returns
  # `[]` (no dependency declared at all) unless BOTH path env vars are
  # set to a non-blank value; a path dep here can never fall back to
  # resolving from Hex the way `pk_dep/3` does for the other optional
  # deps, so an unset var can only ever mean "excluded", never "resolve
  # some floor version that may not have the API".
  defp catalogue_test_deps do
    catalogue_path = System.get_env("PHOENIX_KIT_CATALOGUE_PATH")
    entities_path = System.get_env("PHOENIX_KIT_ENTITIES_PATH")

    if is_binary(catalogue_path) and catalogue_path != "" and is_binary(entities_path) and
         entities_path != "" do
      [
        {:phoenix_kit_catalogue, path: catalogue_path, only: :test},
        {:phoenix_kit_entities, path: entities_path, only: :test}
      ]
    else
      []
    end
  end

  defp package do
    [
      licenses: ["MIT"],
      links: %{"GitHub" => @source_url},
      # `priv/gettext`, not `priv`: Hex ignores .gitignore, and the test
      # Storage writes real files into `priv/media`, which pushed a release
      # tarball past Hex's 16 MB limit.
      files: ~w(lib priv/gettext .formatter.exs mix.exs README.md CHANGELOG.md LICENSE)
    ]
  end

  defp docs do
    [
      main: "PhoenixKitEcommerce",
      # Tags in this repo are bare version numbers, not v-prefixed — a "v" ref
      # points at a tag that does not exist and 404s every HexDocs source link.
      source_ref: @version,
      source_url: @source_url
    ]
  end
end