commit 8d375620a6001120ac4f0e9dd924967900314619
parent d711e37bab9aba079931780c975f252cea71fcbc
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Tue, 4 Nov 2025 14:56:51 +0100
adding 073
Diffstat:
4 files changed, 388 insertions(+), 0 deletions(-)
diff --git a/conf.py b/conf.py
@@ -108,6 +108,7 @@ release = "1.0.0"
exclude_patterns = [
"_build",
"_exts",
+ ".*",
"cf",
"prebuilt",
"**/README.md",
diff --git a/core/api-merchant.rst b/core/api-merchant.rst
@@ -4623,6 +4623,8 @@ to validate that a customer made a payment.
The backend does not know the instance or the OTP device.
+.. _merchant-template-api:
+
---------
Templates
---------
diff --git a/design-documents/073-extended-merchant-template.rst b/design-documents/073-extended-merchant-template.rst
@@ -0,0 +1,384 @@
+DD 73: Extended Merchant Template
+#################################
+
+Summary
+=======
+
+`#0010234 <https://bugs.gnunet.org/view.php?id=10234>`_ targets a wallet-first shopping cart experience by extending the merchant
+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.
+
+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/>`_)
+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:
+
+* Offline and low-connectivity points-of-sale where only the customer's device
+ has network access.
+* Static Web sites that want to embed a payment link or QR code without running
+ dynamic backend logic.
+* Donation flows where the payer sets the amount but contract metadata stays
+ stable.
+
+However, the current model fails to cover micro-merchants who want to publish a
+small inventory, have the wallet enforce contract terms, and still avoid
+operating a PoS or e-commerce website. Today they would need one QR code per product or
+fall back to a free-form amount entry workflow, neither of which captures stock
+keeping, category-based selections, or cart validation.
+
+Requirements
+============
+
+* Introduce a template type system that distinguishes fixed-order templates from
+ inventory-driven ones extending existing REST templates.
+* Define merchant-side configuration for product selection, supporting:
+
+ * all inventory,
+ * category-filtered subsets, and
+ * explicitly enumerated product IDs; combinations must be merged without
+ duplicates.
+* Describe wallet-side UX affordances for choosing exactly one product or
+ multiple products, driven by a ``choose_one`` style flag.
+* 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.
+* Remain compatible with templates from protocol versions v13+.
+
+Proposed Solution
+=================
+
+Schema extensions
+-----------------
+
+Introduce an explicit template type discriminator so existing contracts remain
+stable and new behaviour can be negotiated per protocol version.
+
+.. ts:def:: TemplateType
+
+ type TemplateType = "fixed-order" | "inventory-cart";
+
+Extend ``TemplateAddDetails`` and ``TemplateDetails`` to advertise the new type and, when
+``template_type`` equals ``"inventory-cart"``, nest the inventory-specific
+contract.
+
+.. ts:def:: TemplateAddDetails
+
+ interface TemplateAddDetails {
+
+ // 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;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Fixed contract information for orders created from
+ // this template.
+ template_contract: TemplateContractDetails | TemplateInventoryContractDetails;
+
+ // 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;
+ }
+
+.. ts:def:: TemplateDetails
+
+ 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;
+
+ //Same as current ...
+ }
+
+.. ts:def:: TemplateInventoryContractDetails
+
+ interface TemplateInventoryContractDetails {
+
+ // 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;
+
+ // 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.
+ 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;
+
+ // Possibility to impose limits for each product
+ item_limits?: InventoryItemLimit[];
+
+ // 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;
+ }
+
+.. ts:def:: InventoryItemDefault
+
+ interface InventoryItemDefault {
+
+ product_id: string;
+ // defaults to "0"
+ default_quantity_string: string;
+ }
+
+The merchant backend merges ``select_categories``, and
+``select_products`` into a concrete product list at template instantiation time.
+Duplicates are removed, and products outside the merchant's inventory produce a
+409 conflict. ``choose_one`` dictates whether the wallet must restrict the user
+to a single product/quantity combination (``true``) or allow arbitrary
+combinations (``false``/absent).
+
+Merchant private API updates
+----------------------------
+
+``POST`` and ``PATCH`` on ``/private/templates`` accept new ``TemplateInventoryContractDetails``.
+
+SPA
+----
+The SPA embeds the new configuration in template
+creation forms:
+
+* Adding support for different ``template_type``.
+* Some clever ``template_type`` detection can be introduced, e.g. if the merchant selects the products
+ automatically changed from ``fixed-order`` to ``inventory-cart``. Manage products from order page can be re-used.
+* Inventory selector widgets emit the union of categories and explicit product
+ selections.
+* Optional quantity limits and defaults map to ``item_limits`` and ``item_default``.
+
+
+Wallet discovery API
+--------------------
+
+Enhance the public ``GET /instances/$INSTANCE/templates/$TEMPLATE_ID`` response
+to include both the inventory configuration and the resolved product metadata.
+
+.. ts:def:: WalletTemplateDetails
+
+ interface WalletTemplateDetails {
+ template_type: TemplateType;
+ template_contract: TemplateContractDetails | TemplateInventoryContractDetailsWallet;
+ editable_defaults?: Object;
+ required_currency?: string;
+ }
+
+.. ts:def:: TemplateInventoryContractDetailsWallet
+
+ interface TemplateInventoryContractDetailsWallet {
+
+ // 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;
+
+ // 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.
+ pay_duration: RelativeTime;
+
+ // Information on the products received
+ inventory_payload?: WalletInventoryPayload;
+ }
+
+.. ts:def:: WalletInventoryPayload
+
+ interface WalletInventoryPayload {
+ products: WalletInventoryProduct[];
+ pagination?: InventoryPaginationCursor;
+ }
+
+.. ts:def:: WalletInventoryProduct
+
+ interface WalletInventoryProduct {
+ product_id: string;
+ product_name: string;
+ description: string;
+ description_i18n: { [lang_tag: string]: string };
+ taxes?: Tax[];
+ unit: string;
+ //if non standard unit is used to which merchant defined translations
+ unit_name_short_i18n?: { [lang_tag: string]: string };
+ unit_price: Amount;
+ unit_allow_fraction: boolean;
+ unit_precision_level: Integer;
+ max_quantity: string;
+ min_quantity: string;
+ default_quantity: string;
+ categories?: Integer[];
+ image?: string;
+ }
+
+.. ts:def:: InventoryPaginationCursor
+
+ interface InventoryPaginationCursor {
+ next_offset?: string;
+ total_count?: number;
+ }
+
+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.
+
+Template instantiation
+----------------------
+
+Introduce a specialised request body for inventory-driven templates while
+keeping ``UsingTemplateDetails`` valid for legacy flows.
+
+.. ts:def:: InventoryTemplateUseDetails
+
+ interface InventoryTemplateUseDetails {
+ summary?: string;
+ amount?: Amount;
+ inventory_selection: InventoryTemplateSelection[];
+ }
+
+.. ts:def:: InventoryTemplateSelection
+
+ interface InventoryTemplateSelection {
+ product_id: string;
+ quantity: string;
+ }
+
+``amount`` lets the wallet supply a precalculated total;
+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``.
+
+Backend order creation logic verifies every selected product:
+
+1. Resolve the template and compute the eligible product set.
+2. Ensure user selections are a subset of the resolved list and satisfy
+ ``choose_one`` / quantity bounds.
+3. Construct the contract terms by embedding the selected products as line items
+ in ``TemplateContractDetails`` before calling the internal order creation
+ path (same code as ``POST /private/orders``).
+4. Record the chosen products in order metadata for fulfilment and reporting.
+
+Wallet UX
+---------
+
+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
+ backend response before displaying the payment acceptance dialog.
+5. 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.
+* QR codes stay in the same pay-template URI parameters.
+
+Definition of Done
+==================
+
+* REST API changes and schema extensions are ratified by wallet, merchant, and
+ SPA.
+* Integration tests cover single-product and multi-product cart creation via
+ the new template type.
+* 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
+
+
+Alternatives
+============
+
+* Keep templates fixed and push cart building to merchant-hosted Web flows,
+ trading offline capability for implementation simplicity.
+* Require merchants to mint one template per product, keeping the current API
+ untouched but exacerbating QR code sprawl and inventory maintenance.
+
+Drawbacks
+=========
+
+* Larger template payloads may increase wallet fetch
+ times, especially for templates with many products.
+* More complex validation paths in both wallet and merchant codebases.
+* Risk of inconsistent order totals.
+
+Discussion / Q&A
+================
+
+What to do, if customer wants to leave tip?
diff --git a/design-documents/index.rst b/design-documents/index.rst
@@ -84,4 +84,5 @@ Design documents that start with "XX" are considered deprecated.
070-alias-directory-mailbox
071-auto-refresh
072-products-units
+ 073-extended-merchant-template
999-template