21.100. DD 101: Semantic Token Families#
- Status:
Experimental
- DD shepherd:
Florian Dold
- First published:
2026-08-13
21.100.1. 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.
21.100.2. 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.
21.100.3. 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.
21.100.4. Proposed Solution#
21.100.4.1. 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.
interface SubscriptionExtraData {
experimental_subscription: ExperimentalSubscription;
}
interface DiscountExtraData {
experimental_discount: ExperimentalDiscount;
}
A subscription supports percentage and capped-flat benefits. A discount supports both of those and a free-item benefit.
type ExperimentalSubscription = RedemptionCategories &
(PercentageBenefit | FlatBenefit);
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;
};
interface RedemptionCategories {
// Non-empty array. A line item is eligible when it has any listed ID.
product_categories: CategorySnapshot[];
}
interface CategorySnapshot {
// Positive safe integer inventory category ID.
id: Integer;
// Non-empty display-name snapshot. It is not used for matching.
name: string;
}
interface PercentageBenefit {
type: "percentage";
// Canonical decimal string in the interval (0, 100], with no more than
// eight fractional digits.
percentage: string;
rounding?: 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;
}
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;
}
interface FreeItemBenefit {
// Valid only in ExperimentalDiscount. One unit of the lowest-priced
// eligible item is deducted.
type: "free_item";
}
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.
21.100.4.2. 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.
21.100.5. 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.
21.100.6. 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.
21.100.7. 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.
21.100.8. 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.
21.100.9. Discussion / Q&A#
No unresolved questions.