commit 9d62ae91b8d623e76ec847111013ac3fc08a1363
parent c0e2d941f3cd51ac72befc1d753f74f91efc6001
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Sat, 8 Nov 2025 01:02:50 +0100
some changes to 073
Diffstat:
1 file changed, 180 insertions(+), 122 deletions(-)
diff --git a/design-documents/073-extended-merchant-template.rst b/design-documents/073-extended-merchant-template.rst
@@ -8,12 +8,13 @@ Summary
template feature set with a dedicated inventory-driven template type. The new
design keeps legacy fixed-order templates intact while enabling merchants to
publish a single ``taler://pay-template`` QR code that lets the customer pick one
-or multiple inventory(product) entries directly inside the wallet.
+or multiple inventory (product) entries directly inside the wallet.
Motivation
==========
-The existing template API (see :ref:`Section 1.4.15 <merchant-template-api>` of the merchant manual and `LSD 0006 <https://lsd.gnunet.org/lsd0006/>`_)
+The existing template API (see :ref:`Section 1.4.15 <merchant-template-api>` of
+the merchant manual and `LSD 0006 <https://lsd.gnunet.org/lsd0006/>`_)
lets merchants pre-define mostly static contracts. Wallets can prompt the user
for an amount or order summary, then instantiate an order without the merchant
needing online infrastructure. This is valuable for:
@@ -31,11 +32,27 @@ operating a PoS or e-commerce website. Today they would need one QR code per pro
fall back to a free-form amount entry workflow, neither of which captures stock
keeping, category-based selections, or cart validation.
+As such we see two new scenarios:
+
+1. Tiny shops, farmers' stands, and unattended kiosks want to publish a QR code
+ next to the shelves so customers can scan once, add multiple items and get
+ order that can be paid
+2. Vending machines that usually dispense only one product per transaction
+ still benefit from exposing a catalogue in the wallet UI, but must constrain
+ the customer to one selection to lower the integration costs.
+
+Another question that arises is how the wallet retrieves products efficiently.
+Ideally the entire inventory subset arrives in one response so that up to
+roughly 300-400 items can be listed without paginations. Backends already store
+image metadata, so the wallet should also be able to fetch product pictures on
+request instead of embedding them in the initial payload.
+
Requirements
============
-* Introduce a template type system that distinguishes fixed-order templates from
- inventory-driven ones extending existing REST templates.
+* Introduce a template type system that distinguishes fixed-order templates from
+ inventory-driven ones extending existing REST templates, and creates a base for
+ new possible template types.
* Define merchant-side configuration for product selection, supporting:
* all inventory,
@@ -47,7 +64,7 @@ Requirements
* Extend the public ``GET /templates/$TEMPLATE_ID`` response to surface type,
product descriptors, selection rules, and customer-editable defaults.
* Extend the template instantiation ``POST`` to carry the selected products and
- quantities, reusing ``TemplateDetails`` object.
+ quantities, reusing the ``TemplateDetails`` object.
* Remain compatible with templates from protocol versions v13+.
Proposed Solution
@@ -56,13 +73,25 @@ Proposed Solution
Schema extensions
-----------------
+Add a derivative of the existing public ``/instances/$ID`` endpoints that lets
+wallets download product images via ``GET
+/instances/$ID/products/$IMAGE_HASH/image``.
+
+//TODO: describe in more colors
+
Introduce an explicit template type discriminator so existing contracts remain
-stable and new behaviour can be negotiated per protocol version.
+stable and new behaviour can be negotiated per template version.
.. ts:def:: TemplateType
type TemplateType = "fixed-order" | "inventory-cart";
+.. ts:def:: TemplateContractDetailsType
+
+ type TemplateContractDetailsType =
+ TemplateContractDetails | TemplateInventoryContractDetails;
+
+
Extend ``TemplateAddDetails`` and ``TemplateDetails`` to advertise the new type and, when
``template_type`` equals ``"inventory-cart"``, nest the inventory-specific
contract.
@@ -74,13 +103,6 @@ contract.
// Template ID to use.
template_id: string;
- // Template type defaults to ``fixed-order`` when missing
- // Must be either ``fixed-order`` or ``inventory-cart``
- // This prescribes, which template_contract is expected
- // TemplateContractDetails for ``fixed-order``
- // TemplateInventoryContractDetails for ``inventory-cart``
- template_type?: TemplateType;
-
// Human-readable description for the template.
template_description: string;
@@ -90,7 +112,7 @@ contract.
// Fixed contract information for orders created from
// this template.
- template_contract: TemplateContractDetails | TemplateInventoryContractDetails;
+ template_contract: TemplateContractDetailsType;
// Key-value pairs matching a subset of the
// fields from ``template_contract`` that are
@@ -103,95 +125,63 @@ contract.
interface TemplateDetails {
- // Human-readable description for the template.
- template_description: string;
-
- // Template type
- template_type: TemplateType;
-
// Fixed contract information for orders created from
// this template.
- template_contract: TemplateContractDetails | TemplateInventoryContractDetails;
+ template_contract: TemplateContractDetailsType;
- //Same as current ...
+ // Future fields remain identical to the existing structure.
}
+New contract type has next structure:
+
.. ts:def:: TemplateInventoryContractDetails
interface TemplateInventoryContractDetails {
+ // Template type defaults to ``fixed-order`` when missing.
+ // Must be either ``fixed-order`` or ``inventory-cart``.
+ // This prescribes which template_contract structure is expected.
+ // TemplateContractDetails for ``fixed-order``.
+ // TemplateInventoryContractDetails for ``inventory-cart``.
+ template_type?: TemplateType;
+
// Human-readable summary for the template.
summary?: string;
- // Required currency for payments to the template.
- // The user may specify any amount, but it must be
- // in this currency.
- currency?: string;
-
- // Minimum age buyer must have (in years). Default is 0.
- minimum_age: Integer;
+ // Requests the wallet to offer a tip entry UI. The backend
+ // verifies that amount equals selected products + tip.
+ request_tip?: boolean;
- // The time the customer need to pay before his order will be deleted.
- // It is deleted if the customer did not pay and if the duration is over.
+ // Time window to pay before the order expires unfulfilled.
pay_duration: RelativeTime;
- // Inventory details
- inventory_details: InventoryTemplateDetails;
- }
-
-The optional fields default to legacy behaviour (``template_type`` =
-``"fixed-order"``). Wallets that do not
-recognise ``"inventory-cart"`` must reject the template with a descriptive error
-message.
-
-.. ts:def:: InventoryTemplateDetails
-
- interface InventoryTemplateDetails {
-
- select_all?: boolean;
-
- // ignored if select_all is true
- select_categories?: string[];
-
- // ignored if select_all is true
- select_products?: string[];
-
- // describes whether wallet imposes selection
- // of only one object from user
- choose_one?: boolean;
+ // Selects all products from merchant inventory and overrides
+ // selected_categories and selected_products.
+ selected_all?: boolean;
- // Possibility to impose limits for each product
- item_limits?: InventoryItemLimit[];
+ // All products from selected categories are included.
+ selected_categories?: Integer[];
- // Possibility to predefine default quantity for user
- item_default?: InventoryItemDefault[];
- }
-
-.. ts:def:: InventoryItemLimit
-
- interface InventoryItemLimit {
-
- product_id: string;
-
- // format of "NUMERIC.NUMERIC" check the
- // DD72 for more info on the format
- unit_max_quantity?: string;
- unit_min_quantity?: string;
- }
+ // Explicit list of product IDs to include.
+ selected_products?: string[];
-.. ts:def:: InventoryItemDefault
-
- interface InventoryItemDefault {
+ // When true the wallet must enforce single-selection behaviour.
+ choose_one?: boolean;
+ }
- product_id: string;
- // defaults to "0"
- default_quantity_string: string;
- }
+Wallets that do not recognise ``"inventory-cart"`` continue to expect
+template-level fields such as ``minimum_age``, and when new type supplied
+it will inevitably crash, KABOOM!
-The merchant backend merges ``select_categories``, and
-``select_products`` into a concrete product list at template instantiation time.
+The merchant backend merges ``selected_categories`` and ``selected_products``
+into a concrete product list at template creation time.
+(WHICH IS VERY BAD BECAUSE IF
+MERCHANT CHANGES THE CATEGORY, KABOOM, maybe it has to stay in 2 arrays until we
+pre-calc the GET to wallet)
Duplicates are removed, and products outside the merchant's inventory produce a
-409 conflict. ``choose_one`` dictates whether the wallet must restrict the user
+404 not found.
+
+``choose_one`` dictates whether the wallet must restrict the user
to a single product/quantity combination (``true``) or allow arbitrary
combinations (``false``/absent).
@@ -219,15 +209,51 @@ Wallet discovery API
Enhance the public ``GET /instances/$INSTANCE/templates/$TEMPLATE_ID`` response
to include both the inventory configuration and the resolved product metadata.
+``template_contract`` now reuses a dedicated wallet-facing sum type so the
+payload can unambiguously switch between classic contract terms and the
+inventory capsule.
+
+.. ts:def:: TemplateWalletContractPayload
+
+ type TemplateWalletContractPayload =
+ TemplateWalletContractDetails | TemplateInventoryContractDetailsWallet;
+
+``TemplateWalletContractDetails`` is identical to the ``TemplateContractDetails``
+object defined in :ref:`merchant-template-api`. Changes relative to the current
+protocol are called out below.
+
.. ts:def:: WalletTemplateDetails
interface WalletTemplateDetails {
- template_type: TemplateType;
- template_contract: TemplateContractDetails | TemplateInventoryContractDetailsWallet;
- editable_defaults?: Object;
- required_currency?: string;
+
+ // Hard-coded information about the contract terms
+ // for this template.
+ template_contract: TemplateWalletContractPayload;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol v13.
+ editable_defaults?: Object;
+
+ // Only present when TemplateWalletContractPayload requires it.
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol v13.
+ required_currency?: string;
}
+
+``TemplateInventoryContractDetailsWallet`` intentionally omits a fixed currency
+or minimum age to allow multi-currency product listings and leave age checks to
+per-product logic when available.
+
.. ts:def:: TemplateInventoryContractDetailsWallet
interface TemplateInventoryContractDetailsWallet {
@@ -235,29 +261,31 @@ to include both the inventory configuration and the resolved product metadata.
// Human-readable summary for the template.
summary?: string;
- // Required currency for payments to the template.
- // The user may specify any amount, but it must be
- // in this currency.
- currency?: string;
-
- // Minimum age buyer must have (in years). Default is 0.
- minimum_age: Integer;
+ // Request the wallet to offer a tip entry UI.
+ request_tip?: boolean;
- // The time the customer need to pay before his order will be deleted.
- // It is deleted if the customer did not pay and if the duration is over.
+ // Time the customer has to pay before the order expires unpaid.
pay_duration: RelativeTime;
- // Information on the products received
+ // Information about the resolved products.
inventory_payload?: WalletInventoryPayload;
}
.. ts:def:: WalletInventoryPayload
interface WalletInventoryPayload {
+ // Contains all products selected by the merchant.
products: WalletInventoryProduct[];
- pagination?: InventoryPaginationCursor;
+
+ // Contains all categories referenced by the products.
+ categories: WalletInventoryCategory[];
}
+Next structure mirrors the merchant product descriptor (``ProductDetail`` in the
+merchant specification) with some extensions (unit_name_short_i18n, ) so that backend, SPA, and wallet
+share a single meaning for every field, yet we lower the number of requests between the
+wallet and backend.
+
.. ts:def:: WalletInventoryProduct
interface WalletInventoryProduct {
@@ -267,41 +295,62 @@ to include both the inventory configuration and the resolved product metadata.
description_i18n: { [lang_tag: string]: string };
taxes?: Tax[];
unit: string;
- //if non standard unit is used to which merchant defined translations
+ // Optional translations for non-standard units.
unit_name_short_i18n?: { [lang_tag: string]: string };
- unit_price: Amount;
+ unit_prices: Amount[];
unit_allow_fraction: boolean;
unit_precision_level: Integer;
- max_quantity: string;
- min_quantity: string;
- default_quantity: string;
categories?: Integer[];
- image?: string;
+ image_hash?: string;
}
-.. ts:def:: InventoryPaginationCursor
+.. ts:def:: WalletInventoryCategory
- interface InventoryPaginationCursor {
- next_offset?: string;
- total_count?: number;
+ interface WalletInventoryCategory {
+ category_id: Integer;
+ category_name: string;
+ category_name_i18n?: { [lang_tag: string]: string };
}
-Merchant may opt into pagination when the resolved inventory exceeds REST
-payload thresholds (20 products per page). Wallets that encounter ``pagination`` must fetch additional
-pages before allowing product selection, ensuring the entire subset is in sync
-with backend rules.
+This design lets wallets download hundreds of objects in a single request and
+fetch images later via the shared ``GET
+/instances/$ID/products/$IMAGE_HASH/image`` endpoint.
+
+Inventory template responses MUST include the complete product subset in a
+single payload; QR-code driven flows remain manageable only when the referenced
+catalog fragment comfortably fits into one REST response. Merchants are expected
+to keep templates constrained to a practical number of products (tens, not
+thousands). If extreme use cases ever arise, pagination can be revisited.
Template instantiation
----------------------
-Introduce a specialised request body for inventory-driven templates while
-keeping ``UsingTemplateDetails`` valid for legacy flows.
+Extend ``POST /instances/$INSTANCE/templates/$TEMPLATE_ID`` to support the
+following sum type:
+
+.. ts:def:: UsingTemplateDetailsType
+
+ type UsingTemplateDetailsType =
+ UsingTemplateDetails | InventoryTemplateUseDetails
+
.. ts:def:: InventoryTemplateUseDetails
interface InventoryTemplateUseDetails {
+
+ // Summary of the template.
summary?: string;
- amount?: Amount;
+
+ // Amount pre-calculated on the wallet side.
+ // Do we want to allow null? (e.g. wallet is lazy)
+ amount: Amount;
+
+ // Optional tip selected by the customer and mapped to
+ // an abstract line item in the resulting order.
+ tip?: Amount;
+
+ // Selected products. When choose_one = true the array must contain
+ // exactly one entry.
inventory_selection: InventoryTemplateSelection[];
}
@@ -316,8 +365,9 @@ keeping ``UsingTemplateDetails`` valid for legacy flows.
backends recompute the authoritative order amount and reject mismatches.
Wallets submit ``InventoryTemplateUseDetails`` to ``POST
/instances/$INSTANCE/templates/$TEMPLATE_ID`` when the template advertises
-``template_type`` = ``"inventory-cart"``. Classic templates continue to use
-``UsingTemplateDetails``.
+``template_type`` = ``"inventory-cart"``. ``tip`` carries the customer-selected
+gratuity whenever the template requested it; classic templates consequently
+extend ``UsingTemplateDetails`` with the same optional field.
Backend order creation logic verifies every selected product:
@@ -329,6 +379,9 @@ Backend order creation logic verifies every selected product:
path (same code as ``POST /private/orders``).
4. Record the chosen products in order metadata for fulfilment and reporting.
+When ``tip`` is present, it is simply appended as its own line item(product)
+in the order.
+
Wallet UX
---------
@@ -336,17 +389,16 @@ Wallets handle inventory templates as follows:
1. Fetch ``WalletTemplateDetails`` and cache the resolved inventory.
2. Render a cart builder respecting ``choose_one`` and field defaults.
-3. Clamp quantities to the provided limits and pre-fill from ``item_defaults``.
-4. Show a running total computed from per-product prices; totals must match the
+3. Show a running total computed from per-product prices; totals must match the
backend response before displaying the payment acceptance dialog.
-5. Gracefully handle outdated caches by retrying the ``GET`` when the ``POST``
+4. Gracefully handle outdated caches by retrying the ``GET`` when the ``POST``
returns a conflict due to inventory changes.
Compatibility rules
-------------------
* Docs mark templates containing ``template_type`` =
- ``"inventory-cart"`` as requiring protocol vSOME or later.
+ ``"inventory-cart"`` as requiring protocol vNEXT (final version TBD) or later.
* QR codes stay in the same pay-template URI parameters.
Definition of Done
@@ -359,7 +411,7 @@ Definition of Done
* Updated reference documentation (merchant manual) describes the new
template type and associated fields.
* Wallet and merchant SPA have defined workflows and designs
- for the new template
+ for the new template.
Alternatives
@@ -381,4 +433,10 @@ Drawbacks
Discussion / Q&A
================
-What to do, if customer wants to leave tip?
+What should happen when a customer wants to leave a tip?
+ In the existing template version ``tip`` is supported when the merchant
+ allows amount modifications. For the new ``inventory-cart`` type the
+ ``request_tip`` flag makes that intent explicit. The backend simply appends
+ the tip as another product that flows to the same payto target as the base
+ order. Future work can revisit tip splitting, but that extra complexity is
+ explicitly out of scope here.