feat!: define sale-basis steps for quantities - #653
Conversation
UCP's integer `quantity` had no denominator. It could represent two
bottles, but not 1.50 kg of loose goods, while preserving exact
fulfillment, adjustment, and return arithmetic.
The commerce model distinguishes three cases:
- Each or fixed package: `quantity` counts purchasable variants. A bottle
or a 50 m cable spool sold as one package uses `quantity: 1` and omits
`quantity_unit`. Its fixed contents are part of the variant identity;
`unit_price` can still display a comparison such as price per metre.
- Variable amount: the measured amount itself participates in
fulfillment and returns. `quantity_unit: { unit: "KGM", scale: 2 }`
makes each integer quantity step 0.01 kg, so `quantity: 150` means
1.50 kg.
- Units plus variable configuration: three made-to-measure curtains at
58 cm each have two independent numbers. `quantity: 3` counts the
curtains; 58 cm configures what each curtain is. Configured
measurements are intentionally deferred rather than conflated with
lifecycle quantity.
Model the first two cases with a shared unit descriptor containing a
machine `unit`, required `display_text`, and optional nonnegative
`scale`. One step is `10^-scale` of the unit, and machine identity is the
(`unit`, effective `scale`) pair. `display_text` is presentation data and
does not participate in identity matching.
Preserve `each` as the default sale basis. Omitting `quantity_unit` from
authoritative data means (`C62`, 0), and `C62` cannot use a nonzero
scale. This keeps existing countable-goods payloads unchanged.
Prefer UN/CEFACT Recommendation 20 Common Codes without embedding a UCP
unit ontology. When no code accurately identifies a unit, a Business
may use a custom identifier but must use it consistently. Platforms
treat unknown identifiers as opaque and render the supplied
`display_text`. Package codes remain outside `quantity_unit`; package
form belongs to variant identity and packages count as `each`.
Define capability behavior around the shared representation:
- Catalog advertises a variant's sale basis.
- Cart and Checkout interpret request omission as no unit assertion.
Platforms may assert a (`unit`, effective `scale`) identity, and
Businesses reject mismatches as recoverable outcomes rather than
silently converting quantities.
- Business responses echo `quantity_unit` for every non-`each` line.
- Order, fulfillment, and adjustment quantities inherit the line's
sale basis, preserving exact integer status and return arithmetic.
Quote `price` per one whole `quantity_unit.unit`, not per integer step,
because per-step prices can require fractional currency minor units.
Businesses compute `price × quantity × 10^-scale`, round once at the
line, and return authoritative totals.
For example, fasteners priced at 1299 USD per kg with `scale: 2` and
`quantity: 150` represent 1.50 kg. The line total is 1948.5 minor units,
rounded once to 1949. A fulfillment quantity of 50 represents 0.50 kg,
and an adjustment of -25 represents a 0.25 kg return.
Keep `unit_price` separate as a display comparator and harmonize its
`measure` and `reference` with the shared descriptor. Their values must
be positive integers, their units must match, and
`unit_price.currency` must equal `price.currency`; no unit or currency
conversion occurs in the comparator.
This is backward compatible for payloads that do not use `unit_price`:
`quantity` remains an integer, `quantity_unit` is optional, and omission
retains the existing `each` wire representation.
This is breaking for existing `unit_price` producers. `measure` and
`reference` must migrate from decimal/free-form measurements to integer
values with required `unit` and `display_text` descriptors. Zero and
negative comparator measures are now invalid, and same-unit and
same-currency equality are normative invariants.
The existing wording could be read as making (`unit`, effective `scale`) the complete identity of a purchasable sale unit. That conflates the denomination used for quantity arithmetic with characteristics that may configure or price the item. Define the pair as the unit descriptor's machine identity only. Keep catalog variant identity separate, clarify that other sale-unit characteristics may affect the Business-quoted price without changing its denominator, and state that Order lifecycle quantity arithmetic uses only inherited sale-basis steps. This is a description-only clarification. It adds no fields or measurement configuration behavior, preserving the current wire contract while leaving that model to a separately negotiated extension.
|
Thanks for putting this together, @igrigorik! This PR seems heavily aligned with one of the design options we discussed in #597, and it completely solves the arithmetic coupling and nesting problems of an 1. What I think is aligned with the #597 design option:
2. The One Key Difference: Approach A vs. Approach BWhere this PR diverges from that proposal is in how numeric quantities and step granularity are represented on the wire:
3. I am leaning towards Approach A (
|
Add common/types/quantity_unit.json — the shared unit descriptor composed (allOf) with an optional integer `increment`, a count of scale-steps — and remount variant/item quantity_unit on it. The bare descriptor family is unchanged: measure.json still composes unit.json, so increment cannot appear on unit_price internals or future measures. Increment is advisory merchandising policy, not a representational bound. `scale` bounds what any quantity can express; `increment` shapes what the Platform asks for: Platform-authored quantities SHOULD be increment multiples, and the Business accepts, visibly revises (never silently reinterprets), or rejects an off-increment ask through the standard recoverable-message channel. The Business MAY also revise quantities for its own reasons (e.g. limited stock), keeping revisions on-grid so stepper edits from the revised value stay valid. Business-recorded facts — fulfillment events, adjustments — are bounded only by scale, keeping catch-weight reality representable. Increment is excluded from unit-descriptor machine identity and mismatch comparison. overview.md carries the shared contract, checkout.md the normative behavior plus a snap example, cart.md delegates, order.md scopes recorded facts to scale.
Add an optional integer `increment` to the sale-basis descriptor
(common/types/quantity_unit.json, composing the shared unit descriptor
via allOf; variant/item remount onto it). Increment is a count of
scale-steps and is advisory merchandising policy: it bounds what the
Platform asks for, while scale bounds what any quantity can express.
Platform-authored quantities SHOULD be increment multiples; the
Business accepts, visibly revises, or rejects off-increment asks; and
Business-recorded facts (fulfillment events, adjustments, revisions)
are bounded only by scale. Increment is excluded from machine identity
and never appears on measure/unit_price internals.
Define the negotiation model around the descriptor:
- Discovery: the Platform SHOULD learn the sale basis (unit, scale,
increment) from the catalog; without it, omit the descriptor and
read the authoritative basis from the response echo. Assertion
verifies a previously discovered basis - omit rather than guess.
- Mismatch: silent conversion remains forbidden. The Business MAY
convert an asserted basis to its authoritative basis as a visible
line revision with a warning, else MUST reject recoverably (update:
line unchanged; create: line not created). UCP defines no
conversion factors or dimensions; whether to convert is the
Business's own determination.
- Catch-weight: picked-vs-ordered variance reconciles through
adjustments that move money together with quantity; fulfilled ==
total then holds exactly, and no rounding tolerances or epsilon
comparisons exist anywhere in the quantity lifecycle.
Docs: overview leads with the no-floating-point rationale (quantity
arithmetic feeds money) and the zero-arithmetic rendering recipe
(shift by scale, append display_text); checkout shows one mismatched
update answered two ways - conversion vs rejection - so the
machine-readable recovery path (the echoed descriptor, never message
content) is visible in the JSON; order adds a catch-weight worked
example; catalog advertises the increment as part of discovery. All
worked examples follow a single SKU (fasteners, KGM/scale 2,
increment 25) end to end; pounds appear only as the stale wrong
assertion in the mismatch exhibit.
Recast the sale-basis examples around the predominant use case: one grocery SKU — bananas at LBR/scale 2/increment 25, $0.79/lb — now runs end to end through catalog advertisement, checkout pricing, increment snap, partial fulfillment with return, and catch-weight reconciliation. The unit-conversion exhibit keeps an industrial cameo (fasteners sold by the kilogram, no increment), which both shows the model beyond grocery and keeps the mismatch pair free of increment interplay. A catalog note adds that metered offerings (MIN, HUR) ride the same contract, and overview snippets align to the lead (0.25 lb increment illustration; 1.90 lb pick against a 2.00 lb order).
|
Thanks @amithanda and @jingli, great flags and points. PTAL at the latest commits. Declarative ordering granularity: landed as Catch-weight: fully agree that fulfilled != ordered is normal, not exceptional. Order now shows the flow: the Business records the actual pick and reconciles with an adjustment that moves money together with quantity — after which First-call churn and error recovery: checkout now defines discovery explicitly. Platform learns the basis from the catalog, or omits the descriptor and reads the authoritative basis from the response echo — no error roundtrip; asserting a basis is opt-in verification of something previously discovered, not a forced guess. Mismatch handling also widened: the Business MAY convert an asserted basis to its authoritative basis as a visible line revision with a warning (one roundtrip for the cold-start case), or reject with a recoverable error. Floats vs integers is still the spicy part...There are two different problems hiding inside "quantity equality," and they need different tools:
Remedy 2 proposes one rule for each, but run the catch-weight example through them and they contradict each other. Rounding to 4 decimals only erases noise below The integer wire dissolves problem 1 outright: step counts compare exactly in every language, so the spec carries no rounding or tolerance text anywhere. That leaves problem 2 to be handled as what it is — a commercial fact, not a numeric one: the Business records the actual pick and reconciles with an adjustment that moves money together with quantity, after which Re, implementation burdenA fair concession first: unlike ISO 4217,
The examples now follow one grocery SKU end to end — catalog → pricing → increment snap → partial fulfillment with return → catch-weight reconciliation — plus a non-grocery conversion cameo. |
|
Edit (2026-08-03): Updated the proposal to communicate requirement for an arbitrary-precision decimal representation rather than an integer/scale pair. This provides direct human and AI agent readability ( Thanks @igrigorik, @jingyli, and @amithanda for the thoughtful discussion across PR #597 and PR #653. Looking across both proposals, there appears to be a path to achieve the core requirements into a single, unified
Proposed
|
|
On the "spicy part" (Floats vs. Integers) - I will propose to discuss a bit more on what to optimize for. I am still leaning more towards optimizing for simplicity and I think agent friendly WYSIWYG decimal 1. 4-Decimal Rounding (
|
|
Thanks @gsmith85! Your first point—using UN/CEFACT Rec 20 ID fragments ( Compared to opaque Rec 20 Common Codes ( Where I would caution against adopting a polymorphic Inheritance v/s Embedding Across the Order LifecycleIn PR #597, placing the unit inside
Also, see my comment (float v/s integer above) on leaning towards a more simplified representation which is easier for agents to read and understand and reduces chances of errors. |
|
@amithanda see: https://gist.github.com/igrigorik/0101b779b88635bf93cfea4eec6a9768 Let's step back, the meta conversation here is around precedence of applied principles for UCP / protocol design. I fully agree that a decimal is easier to inspect and grok visually, but to me provably-correct-by-construction property takes strict precedence in API/protocol design. To elaborate, fully specified, both proposals compute integer step counts: "round to 4 decimals" is integer steps with scale frozen at 4, carried in a noisy container. So the real choice isn't floats vs. integers. It's:
At a meta level: correctness belongs in the type system and an error class that does not exist beats one that is not supposed to happen. Quantities multiply money, and thus I'm leaning on strict type system rules. |
|
@gsmith85 @amithanda three additional threads here... Embedding vs declaring once+1 to everything @amithanda said on inheritance vs embedding — that coupling hazard is exactly why #597 was revised into the declare-once shape. One supporting point: embedding the unit in quantities can't remove the item-level declaration, because the price needs a unit before any quantity exists. A catalog variant says Unit vocabulary: codes vs readable namesOn Rec 20 ID fragments (
Variable-weight countables / @alex-jansen's caseThree Honeycrisp apples at $1.99/lb, ~0.4 lb each: this needs two numbers (a count and a weight) and that's exactly why they occupy two slots in the current design. No single-slot quantity (polymorphic or otherwise) can carry both.
Count in |
|
@igrigorik , thank you for writing out the executable test cases in 1. Integer Steps Create an Unverifiable Distributed Error Class (
|
|
@amithanda ty, we covered some of this on the TC call, but for posterity & broader audience... Order records are self-containedThe sale basis is echoed on every cart, checkout, and order line response (normative MUST) — each line carries its own Same plumbing breaks the decimal model identically, on the unit axis. A warehouse webhook emitting a bare Machine check exists, it's the assertion mechanismAny party can assert the On "pre-existing bug in main"
Now back to the fun and 🌶️🌶️🌶️ part of the conversation... Exact decimal is integer + scale, by definition"Databases use exact The root cause of our debate is: JSON has no decimal type (one number type, double semantics in practice — and JavaScript has no native decimal either (TC39's decimal proposal remains Stage 1 after 12+ years of discussion). So an exact decimal can only be encoded: as a string ( Quantities multiply money — the contracts can't be decoupledThe question is whether quantities can use a different contract than money, and they can't, because quantity is a factor in money arithmetic: We're not the first to bump heads with this problem, two examples worth studying... Dinero.js v1 shipped exactly the decoupled design: integer minor-unit money, float multipliers ( multiply(price, { amount: 32, scale: 1 }); // integer multiply, scales add, one explicit roundingAn integer quantity with a declared scale. They ran the decoupled experiment and reverted it. Separately, TC39's Stage 2 UCP's proposed descriptor is the wire serialization of where the JS platform itself is heading. Industry precedent & examplesSquare declares a per-item Google's standard money types are scaled integers. |
|
Thanks @igrigorik for sharing additional perspective on the design which provides strong justification against using decimal numer. I want to step back a bit and narrow the open design issues into two axes, because I have been mixing them and that has probably made the thread harder than it needs to be.
They are independent. Axis 2 holds whichever carrier we pick. Axis 1: carrier formatWhat I am actually arguing forA quantity should mean what it says without a reader combining it with a field declared elsewhere. Integer steps plus
You highlighted some great challenges with decimal which I acknowledge. I had not seriously considered C before this, and I think it deserves a fair discussion as well before we decide to rule it out. Why the currency analogy may not carryThe case for B rests on "quantities get money's representation, exactly as an
Discreteness. Whose property precision is. A store scale, a butcher's scale and a DC scale have different precisions, so the same SKU weighed in two places produces two precisions. That fusion has an operational edge. A pick that was exactly right leaves the line at How the ecosystem handles it
The naive reading of this table is wrong, to be fair: schema.org and Google Merchant are description and feed layers that never accumulate a quantity or derive a state; UBL is XML with an arbitrary-precision decimal type; FHIR's decimal is explicitly not an IEEE float; Shopify's Three things I think it does say:
Pressure testing option C{ "item": { "id": "var_bananas", "price": 79,
"sold_by": { "unit": "LBR", "display_text": "lb", "precision": 2 } },
"quantity": "1.50" }Where it is strong:
Where it is weak:
Axis 2: propagationQuotable is solved. Verifiable is the open question.The PR body already has the right escape hatch for count-ordered, weight-priced goods:
A whole salmon at $12.99/lb, nominally 3.2 lb: sold by The question is narrower: once the price is set by a measurement, does that measurement, and the rate applied to it, survive into the order, where a buyer, an auditor, or a payment processor would need to check it. I will call that propagation, to separate it from expressiveness. The receipt testTake your own 8/3 prescription for this exact category, since it is the clearest statement of the design and I would rather test it than a case I invented:
Run it through: 3 apples ordered, { "line_items": [ { "id": "li_apples", "item": { "id": "var_apple", "price": 80 },
"quantity": { "original": 3, "total": 3, "fulfilled": 3 },
"totals": [ { "type": "total", "amount": 240 } ], "status": "fulfilled" } ],
"adjustments": [ { "id": "adj_1", "type": "price_adjustment", "status": "completed",
"line_items": [ { "id": "li_apples", "quantity": 0 } ],
"totals": [ { "type": "total", "amount": -12 } ] } ] }The test: hand this to someone checking their receipt and ask them to verify the $12 credit. They cannot. Why this is structural, not a gap in one example
Not a grocery-only shapeThe PR's own catalog prose contains a second instance of the same fork:
Schema-identical to the apples case. For a spool the per-metre figure is a shelf label and This also matches how the rest of the ecosystem treats it: schema.org gives the pricing basis its own field with its own unit ( I am not proposing where the fix goes. The question I would like on the table is narrower: when a Business-quoted price is set by a measurement, should that measurement travel into the order the same way the sale basis already does? |
|
I primarily focused on the 🌶️🌶️🌶️ part, but here are some observations and conclusions heading into our discussion coming out of my deep dive prep: Observations1. ISO 4217 & Fractional Minor UnitsRegarding the argument about losing exactness at the point of multiplication: rounding is mathematically inevitable regardless of integer purity if we're ISO 4217. ISO 4217 fixes the precision of the currency side of the operation, so you can end up with accumulated loss from minor unit truncation. For a continuously weighted good sold at $1.50 / kg, a purchase of 1.33 kg yields $1.995, which must ultimately be rounded. If the implementor isn't careful, these errors can grow in aggregate. Relatedly, relying universally on ISO 4217 integer amount fields presents a limitation: it cannot model sub-cent / fractional minor units. It struggles to cleanly accommodate several commerce categories, including:
Stripe, who uses ISO 4217 predominantly, does use decimal representations for currency in their APIs as well to accommodate these scenarios. 3. Industry Precedents around Precision: Square & StripeSquare: Uses an explicit precision parameter (which parallels scale in intent), but pairs it with a string quantity: Square’s approach maintains direct API readability while declaring validation bounds. Stripe: Uses ISO 4217 integer cents primarily for settlement and transfers across banking systems (merchants deal in fractional amounts; banks settle in whole minor units). For rates and quantities, Stripe uses decimal strings bound to 12 decimal places across their specification (e.g., unit_amount_decimal, quantity_decimal). 4. On numeric typesStripe in particular, but others as well, use a combination of integer and string types to model integers and decimals respectively. To the extent floats are used they are in contexts where the math is less sensitive (e.g. discovery and display contexts) or the risk of rounding error is bounded. High Level Conclusions
Proposed UCP Numeric Selection PhilosophyWe should codify our numeric formatting preferences in the core UCP documentation for posterity, a draft take would be something like: • Integers (64-bit): Used for financial settlement (amount in ISO 4217 minor units) and discrete, indivisible whole counts. Closing ThoughtsA one-size-fits-all approach (i.e., using only integers) will struggle to endure the full diversity of commerce scenarios UCP will encounter. Codifying a clear, multi-tier numeric philosophy gives us an opportunity to resolve this debate broadly and permanently for the project. |
When an item is priced by a measurement different from its sale basis (apples at $2.00/lb, sold per each), the rate that determines the charge previously stopped at the catalog, leaving settled orders unverifiable from their own records. The fix is the sale-basis symmetry rule, applied a second time: quantity_unit MUST travel on line responses when the sale basis is not `each`; unit_price MUST travel on every cart, checkout, and order line whose pricing basis differs from its sale basis. Presence on the line is the marker: a line-level unit_price carries the transactional rate, a catalog-only one is a display comparator. unit_price is extracted to a shared type (shopping/types/unit_price.json) mounted on variant and item (response-side). Adjustments gain an optional settled measure (the shared measure type): the Business-recorded measurement a price settlement reconciles. Its unit identity MUST match the line's pricing basis, and quantity: 0 is the pure-price-settlement form, leaving the count lifecycle untouched. A settled measure is Business-recorded fact and lives in core; Buyer-configured measurements that define item identity remain a separate negotiated extension on the same primitive. order.md adds the count-sold, measure-priced example: 3 apples at $0.80 each (nominal 0.40 lb x $2.00/lb), picked weight 1.14 lb. Nominal 3 x 40 = 120 steps, settled 114, delta 6 steps x 200 x 10^-2 = 12 - the -12 adjustment verifies from the order alone, in integer arithmetic.
|
@amithanda good catch on the receipt test -- agreed, and addressed via 449c189. The satisfying part is that no new machinery was needed, the fix is the existing symmetry rule applied a second time:
Please double-check the logic. |
|
For the representation, I'll attempt to recap where we're at... I believe we converged and agree that bare JSON numbers are out. Both remaining options encode the same canonical pair (integer coefficient + declared scale + unit) and that pair is the well-trodden design everywhere exactness exists: The debate is how it's applied to UCP:
My strict priority stack for evaluating the options is: correctness > cost of implementation > presentation. The protocol is judged by its laziest consumer, not by its most diligent. The job of protocol designer is to eliminate and prevent footguns where possible; to define a protocol that yields right outcomes by construction where possible, not by normative prose and appeal to diligence. Conversely, this means preferring strict and early validation and loud and obvious errors. For a commerce protocol, money and inputs that interact with it are load-bearing, and thus my strong belief in above priority stack for this discussion. For me the int path wins because... A) Exactness is unconditional vs opt-in. An integer is exact in every JSON parser ever shipped — there is no lazy path to corruption. A string is exact until the first B) Validation can be enforced at the boundary, consumer failure is loud. C) It's self-consistent and coherent contract across the protocol. We already mandated minor units for prices, enforcing same contract for quantity does not introduce net new cognitive load. Further you're operating in same regime and have guaranteed consistency; if we mix strings and minor units then we're asking you to double the work (do minor units here, but make sure to do proper decimal parsing over there), and forfeit properties outlined above. To be clear, yes the explict tradeoff is that presentation on the wire comes last in this priority stack, but that -- to me -- is a worthwhile and obvious trade, because correctness and self-consistency are more important, and our tooling and AI agent friends can easily be taught to do trivial decimal-point shift operations when operating on these primitives -- and one consistent contract is strictly better than mixed regime. |
Close the two numeric follow-ups from TC discussion of the integer wire: prescribe integer range (Greg's 64-bit flag) and add ingestion guidance for data leaving the protocol boundary (Amit's request). Every integer-valued field is now capped at +/-(2^53 - 1), the range within which every JSON implementation agrees exactly on integer values (RFC 8259, Section 6) and within which JCS canonicalization - required for AP2 mandate signing - is defined. The bound is enforced by schema, not prose: minimum/maximum added to amount, signed_amount, measure.value, and every quantity field, so an out-of-range value is schema-invalid and rejected by existing validation. Verified empirically: parsers agree at 2^53 - 1 and diverge one integer past it (a big-int parser yields ...993 where a double-based parser yields ...992 and re-serializes the corruption). The same cap derives a maximum for scale: at scale 16, one whole unit (10^16 steps) exceeds the emittable range, so scale gains maximum: 15 - a bound derived from the range rule rather than chosen, superseding the earlier no-arbitrary-cap stance and dominating shipped precedent (Square 5, micros 6, nanos 9, Stripe 12). Behavioral contract kept to the two things schemas cannot check: arithmetic MUST be exact (in-range products such as amount x quantity can exceed 64 bits, so use wider integers or overflow checks), and an implementation that cannot produce an exact, in-range result MUST surface an error rather than emit, display, or act on an approximate or wrapped value. Within the wire range, IEEE 754 binary64 - a JavaScript Number from JSON.parse - holds every integer exactly; the hazard is arithmetic, not representation. Ingestion guidance: convert once at the boundary into an exact decimal type (NUMERIC, BigDecimal, Decimal) or carry the (value, scale) pair; no scale application or value-bearing arithmetic in binary floating point.
|
@amithanda @gsmith85 updated to capture outstanding feedback from our review. PTAL, hopefully last and final pass! |
Context: #597. This PR drafts alternate solution shape...
UCP's integer
quantityhas no denominator. It can represent two bottles, but not 1.50 kg of loose goods while preserving exact fulfillment, adjustment, and return arithmetic. This PR keepsquantityas an integer count of steps and addsquantity_unitto define their denomination and granularity. One step is10^-scaleofunit. The (unit, effectivescale) pair is the unit descriptor's machine identity; it neither identifies the purchasable item nor exhaustively describes one sale unit. Omittingquantity_unitfrom authoritative data preserves the existingeachbasis.This paves the way for...
1. Each or fixed package
quantitycounts purchasable variants. Existing countable-goods payloads are unchanged:{ "item": { "id": "var_bottle" }, "quantity": 2 }2. Variable amount
When the measured amount itself participates in fulfillment and returns,
quantity_unitdenominatesquantity. An authoritative USD transaction line for loose fasteners sold in hundredth-of-a-kilogram steps carries the per-kilogram price, sale basis, and quantity together:{ "id": "li_fasteners", "item": { "id": "var_fasteners", "title": "Stainless Steel Fasteners", "price": 1299, "quantity_unit": { "unit": "KGM", "scale": 2, "display_text": "kg" } }, "quantity": 150, "totals": [ { "type": "subtotal", "amount": 1949 }, { "type": "total", "amount": 1949 } ] }Here
quantity: 150means 1.50 kg anditem.price: 1299is 1299 minor units per whole kilogram. The Business computes1299 × 150 × 10^-2 = 1948.5and rounds once to the authoritative line total of1949($19.49).The same steps flow through the order lifecycle:
{ "quantity": { "original": 150, "total": 150, "fulfilled": 50 }, "status": "partial" }Here
fulfilled: 50means 0.50 kg and an adjustment of-25means a 0.25 kg return—all in one inherited unit.Key design decisions
unit, effectivescale).scaledefaults to0; requireddisplay_textis presentation-only. This identity defines the denomination, not the complete purchasable item.C62representseachand cannot use a nonzero scale.display_text. Package codes are excluded because packages count aseach.unit, effectivescale). Businesses MUST echo non-eachdescriptors and surface mismatches; neither side silently converts quantities.price × quantity × 10^-scale, round once at the line, and return authoritative totals.Compatibility and migration
Backward compatible:
quantityremains an integer everywhere.quantity_unitis optional; omission retains the existingeachwire representation.Breaking:
This is a Core Protocol
feat!change because existingunit_priceproducers must migratemeasureandreferencefrom decimal/free-form measurements to positive integer values plus the shared descriptor (unit,display_text, optionalscale). Their units and currencies must match, and zero or negative comparator measures are invalid. This removes a second unit vocabulary and numeric representation rather than preserving parallel paths.Checklist