taler-docs

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

commit 73947d753087c015ceaaf953799617acfe730f5e
parent c83403f7040742b4e625613dc7b6670fb38563da
Author: Florian Dold <dold@taler.net>
Date:   Thu, 13 Aug 2026 18:40:06 +0200

design-documents: describe semantic token families

Diffstat:
Adesign-documents/101-semantic-token-families.rst | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdesign-documents/index.rst | 1+
2 files changed, 226 insertions(+), 0 deletions(-)

diff --git a/design-documents/101-semantic-token-families.rst b/design-documents/101-semantic-token-families.rst @@ -0,0 +1,225 @@ +DD 101: Semantic Token Families +############################### + +:Status: Experimental +:DD shepherd: Florian Dold +:First published: 2026-08-13 + +Summary +======= + +Semantic token families let merchant backend clients (initially only +``merchant-webui-ng`` and the Web PoS) describe subscription and discount +behavior in a token family's ``extra_data``. The WebUI translates the +description into v1 order choices and token outputs. The metadata is +experimental and uses the top-level keys ``experimental_subscription`` and +``experimental_discount``. + +Motivation +========== + +Merchants ned to be able to define the effect of a subscription token, +they aren't expected to manually apply the token effect every sale. + +Requirements +============ + +* Both family kinds support exact percentage and capped-flat benefits; + discounts additionally support one free item from selected categories. +* Benefits and issuance can be limited to product categories. +* A subscription consumes and replaces one token. +* A discount consumes a configured positive number of tokens and earns one + token for each qualifying paid order. +* Automatic issuance must work for full-price and alternative choices without + stacking benefits. + +Proposed Solution +================= + +Schema +------ + +The semantic object is stored under the key matching the token family's kind. +These interfaces show the semantic members of ``extra_data``; the object may +also contain unrelated top-level members. + +.. ts:def:: SubscriptionExtraData + + interface SubscriptionExtraData { + experimental_subscription: ExperimentalSubscription; + } + +.. ts:def:: DiscountExtraData + + interface DiscountExtraData { + experimental_discount: ExperimentalDiscount; + } + +A subscription supports percentage and capped-flat benefits. A discount +supports both of those and a free-item benefit. + +.. ts:def:: ExperimentalSubscription + + type ExperimentalSubscription = RedemptionCategories & + (PercentageBenefit | FlatBenefit); + +.. ts:def:: ExperimentalDiscount + + type ExperimentalDiscount = RedemptionCategories & + (PercentageBenefit | FlatBenefit | FreeItemBenefit) & { + // Positive safe integer. This many tokens are consumed on redemption. + required_tokens: Integer; + + // Rule for earning one token from a paid order. + issuance: DiscountIssuance; + }; + +.. ts:def:: RedemptionCategories + + interface RedemptionCategories { + // Non-empty array. A line item is eligible when it has any listed ID. + product_categories: CategorySnapshot[]; + } + +.. ts:def:: CategorySnapshot + + interface CategorySnapshot { + // Positive safe integer inventory category ID. + id: Integer; + + // Non-empty display-name snapshot. It is not used for matching. + name: string; + } + +.. ts:def:: PercentageBenefit + + interface PercentageBenefit { + type: "percentage"; + + // Canonical decimal string in the interval (0, 100], with no more than + // eight fractional digits. + percentage: string; + + rounding?: PercentageRounding; + } + +.. ts:def:: PercentageRounding + + interface PercentageRounding { + mode: "down" | "nearest" | "up"; + + // Positive canonical decimal increment in currency units, with no more + // than eight fractional digits. For example, "0.05" rounds to a + // five-cent increment. + precision: string; + } + +.. ts:def:: FlatBenefit + + interface FlatBenefit { + type: "flat"; + + // Positive Taler amount. The reduction is capped at the eligible + // subtotal and applies only when its currency matches the order currency. + amount: AmountString; + } + +.. ts:def:: FreeItemBenefit + + interface FreeItemBenefit { + // Valid only in ExperimentalDiscount. One unit of the lowest-priced + // eligible item is deducted. + type: "free_item"; + } + +.. ts:def:: DiscountIssuance + + interface DiscountIssuance { + // A non-empty category array restricts issuance to the matching subtotal. + // "*" uses the whole-order amount and also supports amount-only orders. + product_categories: CategorySnapshot[] | "*"; + + // Optional positive Taler amount. The threshold is inclusive and applies + // only when its currency matches the order currency. + minimum_purchase?: AmountString; + + // Whether the order may earn this family's token while redeeming the same + // family. Defaults to false when omitted. + issue_on_redemption?: boolean; + } + +The WebUI accepts only canonical percentage and precision strings: leading +zeroes, trailing fractional zeroes, signs and exponent notation are invalid. +When ``rounding`` is absent, percentage reductions round down to Taler's +smallest amount fraction. Category arrays must be non-empty; repeated IDs are +coalesced. The WebUI writes only the semantic key matching the family kind, +does not read the earlier unprefixed prototype keys, and preserves unrelated +top-level ``extra_data`` members. Additional members inside a semantic object +are not part of the schema and may be discarded when the family is edited. + +Behavior +-------- + +Categories are snapshots. Matching uses only the ID; the name is display +metadata refreshed when an existing family is saved. + +A discount additionally has ``required_tokens`` and an ``issuance`` object. +Its ``product_categories`` is either category snapshots or ``"*"`` for every +paid merchant order. ``minimum_purchase`` is optional and inclusive, and +``issue_on_redemption`` defaults to false. For example:: + + { + "experimental_discount": { + "type": "flat", + "amount": "CHF:10", + "required_tokens": 5, + "product_categories": [{"id": 1, "name": "Coffee"}], + "issuance": { + "product_categories": "*", + "minimum_purchase": "CHF:5", + "issue_on_redemption": false + } + } + } + +The WebUI calculates redemption choices separately from earned-token outputs. +It adds qualifying outputs to the full-price choice and every alternative, +except that same-family discount redemption suppresses issuance by default. +Outputs for the same family are coalesced. Eligibility and thresholds use the +exact pre-benefit subtotal; category rules require trustworthy line items, +while wildcard rules also support amount-only orders. + +Test Plan +========= + +Test metadata parsing and writing, exact benefit arithmetic, category and +wildcard eligibility, free-item selection, issuance thresholds, same-family +suppression, and output coalescing. WebUI tests cover editing, order creation, +and PoS previews. The harness earns tokens on paid orders and later redeems +the configured threshold. + +Definition of Done +================== + +The editor, order and PoS flows implement the rules above; documentation and +translations are updated; unit, UI, catalog, type, lint, and harness integration +checks pass. Until then, the metadata remains explicitly experimental. + +Alternatives +============ + +Encoding these rules in the backend would provide centralized enforcement but +requires backend and protocol changes. Explicitly configuring every order +output cannot provide automatic loyalty issuance. + +Drawbacks +========= + +The WebUI must have trustworthy line items for category rules. Category names +are snapshots and are not localized. The experimental schema may change +without migration. + +Discussion / Q&A +================ + +No unresolved questions. diff --git a/design-documents/index.rst b/design-documents/index.rst @@ -111,4 +111,5 @@ Design documents that start with "XX" are considered deprecated. 097-challenge-confirmations 098-token-fountains 099-programmable-templates + 101-semantic-token-families 999-template