taler-docs

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

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:
Mdesign-documents/073-extended-merchant-template.rst | 302+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
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.