taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit 4b9341f99484d28dbd8e82323d5d468a4237fb5e
parent e2436d60143ec7cb530188145916f02b018a7483
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sun,  2 Nov 2025 23:18:34 +0100

update of the dd72

Diffstat:
Mdesign-documents/072-products-units.rst | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 194 insertions(+), 35 deletions(-)

diff --git a/design-documents/072-products-units.rst b/design-documents/072-products-units.rst @@ -12,18 +12,29 @@ terminals, and merchant tooling to keep UX coherent across integrations. 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. +Field feedback highlighted several gaps in the existing product catalogue flow: + +* Conflicting requirements coexist: + + * 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 API surface. +* The merchant SPA currently requires operators to type a ``unit`` string for + every product, creating room for typos and inconsistent spellings across the + same instance. +* Product descriptions already support translations, but the ``unit`` label is + fixed, limiting the ability to localise inventory for customers. +* Some end customers, especially when travelling or having grown up with a + different measurement system than the merchant uses, might have difficulties + understanding the quantities; a predefined list of units enables conversions + that support informed buying decisions. Requirements ============ @@ -46,8 +57,148 @@ Requirements Proposed Solution ================= -1. **Extend product schemas** with optional metadata: +1. **Introduce unit catalog endpoints** + + The merchant backend exposes ``/private/units`` so operators can manage the + measurement units available to an instance. Payloads follow the + ``InternationalizedString`` pattern already used across the API (maps of + BCP 47 language tags to translated strings). + + .. http:get:: /private/units + + Return the catalogue for the current instance. + + :http:statuscode:`200 OK`: + The response body is a :ts:ref:`MerchantUnitsResponse`. + + **Details:** + + .. ts:def:: MerchantUnitsResponse + + interface MerchantUnitsResponse { + // Units available to the instance (built-in and custom). + units: MerchantUnit[]; + } + + .. ts:def:: MerchantUnit + + interface MerchantUnit { + // Backend identifier used in product payloads. + unit: string; + + // Localised long label. + unit_name_long: string; + unit_name_long_i18n: InternationalizedString | null; + + // Localised short label (preferred for UI display). + unit_name_short: string; + unit_name_short_i18n: InternationalizedString | null; + + // Whether fractional quantities are permitted by default. + unit_allow_fraction: boolean; + + // Maximum number of fractional digits to honour. + unit_precision_level: number; + + // Toggle for hiding the unit from selection lists. + unit_active: boolean; + + // True for catalogue entries shipped with the backend. + unit_builtin: boolean; + } + + ``unit_builtin`` marks records that ship with the backend and therefore + cannot be deleted. + + .. ts:def:: InternationalizedString + + type InternationalizedString = { + [lang_tag: string]: string; + }; + + .. http:get:: /private/units/$UNIT + + Return a single unit definition. + + :http:statuscode:`200 OK`: + The response body is a :ts:ref:`MerchantUnit`. + :http:statuscode:`404 Not Found`: + The identifier is unknown or belongs to a deleted record. + + .. http:post:: /private/units + Create a new custom unit. + + :http:statuscode:`204 No Content`: + The unit was created successfully. + + **Request body:** :ts:ref:`MerchantUnitCreateRequest` + + **Details:** + + .. ts:def:: MerchantUnitCreateRequest + + interface MerchantUnitCreateRequest { + unit: string; + unit_name_long: string; + //defaults to null + unit_name_long_i18n?: InternationalizedString ; + unit_name_short: string; + //defaults to null + unit_name_short_i18n?: InternationalizedString; + //defaults to false + unit_allow_fraction?: boolean; + //defaults to 0, ignored when unit_allow_fraction is false + unit_precision_level?: number; + //defaults to true + unit_active?: boolean; + } + + .. http:patch:: /private/units/$UNIT + + Update an existing unit. + + :http:statuscode:`204 No Content`: + The update was applied. + :http:statuscode:`409 Conflict`: + Attempted to modify immutable fields on a built-in unit. + + **Request body:** :ts:ref:`MerchantUnitPatchRequest` + + **Details:** + + .. ts:def:: MerchantUnitPatchRequest + + interface MerchantUnitPatchRequest { + unit_name_long?: string; + unit_name_long_i18n?: InternationalizedString | null; + unit_name_short?: string; + unit_name_short_i18n?: InternationalizedString | null; + unit_allow_fraction?: boolean; + unit_precision_level?: number; + unit_active?: boolean; + } + + Built-in units accept changes only to ``unit_allow_fraction`` and + ``unit_precision_level``. Custom units may update every attribute except + ``unit``. + + .. http:delete:: /private/units/$UNIT + + Remove a custom unit. + + :http:statuscode:`204 No Content`: + The unit was deleted. + :http:statuscode:`409 Conflict`: + Attempted to delete a built-in unit. + + Product payloads continue to accept the ``unit`` string. The backend resolves + that value against this catalogue; when no entry is found the fallback rules + from step 6 apply. + +2. **Extend product schemas** with optional metadata: + + * ``unit`` (string; existing field, now validated against the catalogue) * ``unit_allow_fraction`` (boolean) * ``unit_precision_level`` (integer 0–6) * ``unit_price`` (fixed-point decimal string) @@ -55,22 +206,23 @@ Proposed Solution “infinite” semantics) Legacy ``price`` and ``total_stock`` remain, but become compatibility shims and - must match the new values whenever present. + must match the new values whenever present. Every product record continues to + emit the legacy ``unit`` string so existing clients can operate unchanged. -2. **Accept** ``unit_quantity`` wherever clients submit quantities (inventory +3. **Accept** ``unit_quantity`` wherever clients submit quantities (inventory locks, ``inventory_products``). The backend converts the decimal string into the legacy ``quantity`` and new ``quantity_frac`` pair for storage so existing clients keep working. -3. **Return both representations** in all read APIs so integrators can migrate +4. **Return both representations** in all read APIs so integrators can migrate at their own pace. -4. **Default backend policies** +5. **Seed default units** - 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. + During instance provisioning the backend populates the units table with the + following built-in entries. Built-in entries start active with + ``unit_builtin`` = true and cannot be deleted, although their fractional policy + may be tuned as described above. .. list-table:: Default backend policies :widths: 20 10 10 30 30 @@ -261,26 +413,32 @@ Proposed Solution - 3 - metric tonne - t - * - **Custom** - - int - - 0 - - *merchant string* - - *merchant string* -5. **Quantity presentation in wallets and orders** +6. **Handle legacy and ad-hoc units gracefully** + + Older clients may still submit arbitrary ``unit`` strings in API requests. The + backend accepts those values by treating them as custom units with + ``unit_allow_fraction`` = false and ``unit_precision_level`` = 0. + The merchant SPA limits merchant to the drop-down populated via + ``GET /private/units`` so newly created products stay consistent. + +7. **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 + **MUST use the short unit label** returned by ``GET /private/units`` (or + ``GET /private/units/$UNIT``) for the referenced ``unit``. When the unit + catalogue does not contain the identifier, clients fall back to the raw + ``unit`` string. Append the selected label to the numeric value with a + non-breaking thin space (U+202F). Trailing zeros *up to* the declared ``unit_precision_level`` **MUST be trimmed**, but the displayed precision **MUST NOT** exceed the declared level. Examples:: - 1.500 kg → shown as 1.5 kg + 1.500 kg → shown as 1.500 kg 3.00 pc → shown as 3 pc For precision 0 units the fractional part is omitted entirely. -6. **Locale-aware unit translation rules for wallets** +8. **Locale-aware unit translation rules for wallets** Wallets **SHOULD** offer users the option to view quantities in familiar measurement systems. The following guidance applies: @@ -349,15 +507,16 @@ 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, updated products table to use new ``unit_total_stock`` string, - order can be created with fractional quantities. +* Merchant SPA surfaces a unit drop-down populated from ``GET /private/units``, + uses ``unit_total_stock`` in product listings, allows fractional orders where + permitted, and provides a management screen for the unit catalogue. * POS and wallet reference implementations render fractional quantities according to ``unit_allow_fraction`` / ``unit_precision_level``, allows to create orders with fractional quantities of products. * 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. +* Wallets implement the presentation and localisation guidance described in + steps 7 and 8 of this section. Alternatives ============