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
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.
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.
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.
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" / "цена договорная").
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".
Renders a price for display.
Options
:amount— the exact amount to render (required for:selected,:cartand: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.
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.
The unit text for a language, with default-language fallback.
Returns nil when the product has no unit — callers render the plain price.