13.72. DD 72: Products Units#
13.72.1. Summary#
Introduce canonical unit_* metadata for merchant inventory so prices and
stock levels can be expressed with fractional precision while retaining legacy
integer fields for backwards compatibility. Provide guidance to wallets, PoS
terminals, and merchant tooling to keep UX coherent across integrations.
13.72.2. Motivation#
Bug reports revealed two conflicting requirements:
Products sold by measurable attributes (for example potatoes by kilogram) need fractional support so customers can order 1.5 kg without hacks.
Discrete products (for example “pieces” of cheese) must remain integer-only; allowing 1.2 pc would break inventory management.
The existing API exposes only integer fields (quantity, total_stock,
price). Simply switching to floating-point values would enable nonsensical
orders and introduce rounding issues. After team discussion it was decided
that explicit unit_* metadata can be introduced for overall cleanliness of the
code base.
13.72.3. Requirements#
Preserve compatibility: accept and emit the legacy integer fields while marking them deprecated once
unit_*alternatives exist. When both are supplied the backend must check that values match.Use a predictable format: fixed-point decimal strings
INTEGER[.FRACTION]with up to eight fractional digits; reject scientific notation and special floating-point tokens.Provide backend-chosen defaults per unit identifier so new front-ends can present appropriate UI without manual configuration.
Allow merchants to override the default policy through explicit fields.
Update every affected endpoint (GET/POST/PATCH products, PoS inventory, lock, order creation, contract products) to expose and accept the new metadata.
Document expectations for merchant back-ends, PoS clients, and wallets to ensure consistent behaviour across the ecosystem.
13.72.4. Proposed Solution#
Extend product schemas with optional metadata:
unit_allow_fraction(boolean)unit_precision_level(integer 0–6)unit_price(fixed-point decimal string)unit_total_stock(fixed-point decimal string,-1keeps the “infinite” semantics)
Legacy
priceandtotal_stockremain, but become compatibility shims and must match the new values whenever present.Accept
unit_quantitywherever clients submit quantities (inventory locks,inventory_products). The backend converts the decimal string into the legacyquantityand newquantity_fracpair for storage so existing clients keep working.Return both representations in all read APIs so integrators can migrate at their own pace.
Default backend policies
Any backend unit identifier that is not listed in the table below SHALL be treated as Custom:
unit_allow_fraction= false,unit_precision_level= 0, and both default labels (long/short) echo the merchant-supplied string.
BackendStr |
Type |
Precision |
Default label (long) |
Default label (short) |
|---|---|---|---|---|
Piece |
int |
0 |
piece |
pc |
Set |
int |
0 |
set |
set |
SizeUnitCm |
float |
1 |
centimetre |
cm |
SizeUnitDm |
float |
3 |
decimetre |
dm |
SizeUnitFoot |
float |
3 |
foot |
ft |
SizeUnitInch |
float |
2 |
inch |
in |
SizeUnitM |
float |
3 |
metre |
m |
SizeUnitMm |
int |
0 |
millimetre |
mm |
SurfaceUnitCm2 |
float |
2 |
square centimetre |
cm² |
SurfaceUnitDm2 |
float |
3 |
square decimetre |
dm² |
SurfaceUnitFoot2 |
float |
3 |
square foot |
ft² |
SurfaceUnitInch2 |
float |
4 |
square inch |
in² |
SurfaceUnitM2 |
float |
4 |
square metre |
m² |
SurfaceUnitMm2 |
float |
1 |
square millimetre |
mm² |
TimeUnitDay |
float |
3 |
day |
d |
TimeUnitHour |
float |
2 |
hour |
h |
TimeUnitMinute |
float |
3 |
minute |
min |
TimeUnitMonth |
float |
2 |
month |
mo |
TimeUnitSecond |
float |
3 |
second |
s |
TimeUnitWeek |
float |
3 |
week |
wk |
TimeUnitYear |
float |
4 |
year |
yr |
VolumeUnitCm3 |
float |
3 |
cubic centimetre |
cm³ |
VolumeUnitDm3 |
float |
5 |
cubic decimetre |
dm³ |
VolumeUnitFoot3 |
float |
5 |
cubic foot |
ft³ |
VolumeUnitGallon |
float |
3 |
gallon |
gal |
VolumeUnitInch3 |
float |
2 |
cubic inch |
in³ |
VolumeUnitLitre |
float |
3 |
litre |
L |
VolumeUnitM3 |
float |
6 |
cubic metre |
m³ |
VolumeUnitMm3 |
float |
1 |
cubic millimetre |
mm³ |
VolumeUnitOunce |
float |
2 |
fluid ounce |
fl oz |
WeightUnitG |
float |
1 |
gram |
g |
WeightUnitKg |
float |
3 |
kilogram |
kg |
WeightUnitMg |
int |
0 |
milligram |
mg |
WeightUnitOunce |
float |
2 |
ounce |
oz |
WeightUnitPound |
float |
3 |
pound |
lb |
WeightUnitTon |
float |
3 |
metric tonne |
t |
Custom |
int |
0 |
merchant string |
merchant string |
Quantity presentation in wallets and orders
When displaying order details or cart lines, wallet and POS front-ends MUST use the short unit label from the table above, appended to the numeric value with a non-breaking thin space (U+202F). Trailing zeros up to the declared
unit_precision_levelMUST be trimmed, but the displayed precision MUST NOT exceed the declared level. Examples:1.500 kg → shown as 1.5 kg 3.00 pc → shown as 3 pc
For precision 0 units the fractional part is omitted entirely.
Locale-aware unit translation rules for wallets
Wallets SHOULD offer users the option to view quantities in familiar measurement systems. The following guidance applies:
Detect the buyer locale using the platform-standard mechanism (e.g.
navigator.languagein browsers or OS locale on mobile). Only when the locale primary region is in the CLDR “IU-customary group” (US,LR,MM,GB) SHALL conversions default to imperial/US-customary, and vice-versa when the merchant lists imperial units but the buyer locale is SI-centred.Supported automatic conversions and factors (SI -> US and US -> SI):
Supported automatic conversions and factors# SI unit
US/imperial unit
factor
kilogram (
kg)pound (
lb)2.20462
gram (
g)ounce (
oz)0.035274
litre (
L)fluid ounce (
fl oz)33.814
metre (
m)foot (
ft)3.28084
square metre (
m²)square foot (
ft²)10.7639
cubic metre (
m³)cubic foot (
ft³)35.3147
Conversions MUST round to the wallet’s target
unit_precision_levelusing bankers-rounding to minimise cumulative error.When a converted value is displayed it SHOULD be prefixed with “ca.” (or
≈symbol) and rendered in a visually subdued style (e.g. 60% opacity) to signal approximation; the merchant-provided unit remains the authoritative primary value.The original backend value MUST be preserved in the contract terms; conversions are presentation-only.
Wallets SHOULD expose a global numeric-system setting in their preferences with the values
off,automatic,SI, andimperial.off – never perform unit conversions; display exactly the merchant-supplied units.
automatic – apply the locale heuristic described above (imperial for
US,GB,LR,MM; SI otherwise).SI – always display quantities in SI units (no conversion if the merchant already uses SI).
imperial – always display quantities converted to imperial/US-customary units (no conversion if the merchant already uses imperial).
13.72.5. Definition of Done#
(Only applicable to design documents that describe a new feature. While the DoD is not satisfied yet, a user-facing feature must be behind a feature flag or dev-mode flag.)
Merchant backend accepts and emits the new metadata for product CRUD, inventory locks, and order creation.
Merchant SPAA has updated screen for product creation, which facilitates the default units.
POS and wallet reference implementations render fractional quantities according to
unit_allow_fraction/unit_precision_level.Legacy clients continue to function using the integer fields, with automated tests ensuring that canonical and legacy values stay in sync.
Wallets follows points 5 and 6 of Proposed Solution.
13.72.6. Alternatives#
Replace integers with floating-point numbers. This was ruled out because it cannot prevent semantically invalid requests (for example 1.2 pieces) and reintroduces floating-point rounding issues into price calculations.
13.72.7. Drawbacks#
Payloads grow slightly because responses include both canonical decimal strings and legacy integers.
Integrations must update their tooling to emit and validate decimal strings, which adds complexity compared to sending plain integers.