PhoenixKitEcommerce.PriceDisplay (PhoenixKitEcommerce v0.2.0)

Copy Markdown View Source

How a price is written on the storefront: From €40.00 /hour.

Two independent decorations, both optional and both off by default:

  • a unit — free text per language ("per hour", "/m²", "в час"), so a service shop can state its pricing model. Free text rather than a vocabulary: nothing here needs to know what an hour is, and every shop invents units the next one has never heard of.

  • a "From" prefix — set explicitly by the admin, or implied when the product's options genuinely produce a price range.

Storage

Both live under one reserved key in Product.metadata:

%{"_price_display" => %{
    "unit" => %{"en" => "per hour", "ru" => "в час"},
    "from" => true
  }}

A single versioned namespace rather than two loose top-level keys: the metadata map is an advertised extension point, so this module's data stays in one place that can grow (and be recognised) without colliding with whatever a host already stores there. _-prefixed matches the existing _option_values / _price_modifiers convention, and option keys may not begin with _, so no user-defined option can shadow it.

Context is load-bearing

render/4 takes a context because the same product means different things on different pages:

  • :catalog — the product's asking price. May show "From", and the amount comes from the product's option-aware range.
  • :selected — the price for the options the shopper picked. Exact, so never "From"; keeps the unit.
  • :cart / :order — a SNAPSHOT of what was (or will be) charged. Exact, never "From", and the unit comes from the snapshot rather than the live product, so an edit or deletion cannot rewrite a line the customer already agreed to.

Absent data renders exactly what the module rendered before this existed.

Summary

Functions

Whether any line in a cart or order was agreed as "price on request".

Builds the storable namespace map from admin form input.

Whether a stored LINE was agreed as "price on request".

The reserved metadata key. Consumers that copy metadata around (the CSV upsert, the product form) use this to preserve the namespace.

Whether this product's price is negotiated rather than listed ("price on request" / "цена договорная").

Renders a price for display.

Reads the display settings out of a product (or a raw metadata map).

The unit text for a language, with default-language fallback.

Functions

any_line_on_request?(lines)

Whether any line in a cart or order was agreed as "price on request".

Suppressing the amount per line leaves the TOTAL saying something the shop does not mean: an on-request line snapshots 0, so a cart holding one reads "Total 0.00" beside a "Proceed to Checkout" button, and a mixed cart quietly omits the on-request item from a figure the shopper reads as the whole bill. The totals themselves stay as they are — they are the amounts billing will charge — so the pages that show one disclose what it leaves out.

build(unit_map, from?)

Builds the storable namespace map from admin form input.

Blank units are dropped so an untouched form does not persist empty strings, and values are length-bounded — this text renders next to a price on a public page, it is not a description field.

build(unit_map, from?, on_request?)

line_on_request?(line)

Whether a stored LINE was agreed as "price on request".

This is the snapshot side of on_request?/1, and every page that renders a line must ask it rather than reading the live product. It accepts both shapes the storefront holds a line in: a CartItem (the flag lives under metadata) and an order line item (a plain map, flag at the top level).

A helper rather than the inline (item.metadata || %{})["price_on_request"] == true this replaces: five templates need the answer — cart, checkout review, the product page's "already in cart" notice, the confirmation and order details — and the two that expressed it inline were the two that got it, while the other three rendered 0.00 for a line with no price.

metadata_key()

The reserved metadata key. Consumers that copy metadata around (the CSV upsert, the product form) use this to preserve the namespace.

on_request?(product_or_metadata)

Whether this product's price is negotiated rather than listed ("price on request" / "цена договорная").

Read the LINE's own snapshot for :cart and :order (metadata["price_on_request"]), never the live product. A product can be edited or deleted after a line is created (product_uuid is ON DELETE SET NULL), and a line that was agreed as "on request" must not later render as a number, nor a priced line as "on request".

render(product, currency, ctx, opts \\ [])

Renders a price for display.

Options

  • :amount — the exact amount to render (required for :selected, :cart and :order; ignored for :catalog, which derives the product's range).
  • :unit — an explicit unit string, used by snapshot contexts so a cart line can render the unit it stored rather than the live one.
  • :language — the viewer's language, for unit resolution.
  • :range_style:from (default) or :range, catalog only.

settings(metadata)

Reads the display settings out of a product (or a raw metadata map).

Returns %{unit: %{lang => text}, from: boolean, on_request: boolean} with safe defaults.

unit_for(product_or_metadata, language)

The unit text for a language, with default-language fallback.

Returns nil when the product has no unit — callers render the plain price.