taler-docs

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

073-extended-merchant-template.rst (16044B)


      1 DD 73: Extended Merchant Template
      2 #################################
      3 
      4 :Design status: Accepted
      5 :Implementation status: Partial
      6 :DD shepherd: TBD
      7 :Historical contributors: Bohdan Potuzhnyi
      8 :First published: 2025-11-04
      9 :Last substantive change: 2025-11-11
     10 :Implementation evidence: ``merchant`` (2025-11-15); backend and wallet protocol support landed, while merchant WebUI creation remains incomplete
     11 :Normative references: ``core/api-merchant.rst`` and ``core/merchant/get-templates-TEMPLATE_ID.rst``
     12 
     13 Summary
     14 =======
     15 
     16 `#0010234 <https://bugs.gnunet.org/view.php?id=10234>`__ targets a wallet-first shopping cart experience by extending the merchant
     17 template feature set with a dedicated inventory-driven template type. The new
     18 design keeps legacy fixed-order templates intact while enabling merchants to
     19 publish a single ``taler://pay-template`` QR code that lets the customer pick one
     20 or multiple inventory (product) entries directly inside the wallet.
     21 
     22 Motivation
     23 ==========
     24 
     25 The existing template API (see :ref:`Section 1.4.15 <merchant-template-api>` of
     26 the merchant manual and `LSD 0006 <https://lsd.gnunet.org/lsd0006/>`__)
     27 lets merchants pre-define mostly static contracts. Wallets can prompt the user
     28 for an amount or order summary, then instantiate an order without the merchant
     29 needing online infrastructure. This is valuable for:
     30 
     31 * Offline and low-connectivity points-of-sale where only the customer's device
     32   has network access.
     33 * Static Web sites that want to embed a payment link or QR code without running
     34   dynamic backend logic.
     35 * Donation flows where the payer sets the amount but contract metadata stays
     36   stable.
     37 
     38 However, the current model fails to cover micro-merchants who want to publish a
     39 small inventory, have the wallet enforce contract terms, and still avoid
     40 operating a PoS or e-commerce website. Today they would need one QR code per product or
     41 fall back to a free-form amount entry workflow, neither of which captures stock
     42 keeping, category-based selections, or cart validation.
     43 
     44 As such we see two new scenarios:
     45 
     46 1. Tiny shops, farmers' stands, and unattended kiosks want to publish a QR code
     47    next to the shelves so customers can scan once, add multiple items and get
     48    order that can be paid
     49 2. Vending machines that usually dispense only one product per transaction
     50    still benefit from exposing a catalogue in the wallet UI, but must constrain
     51    the customer to one selection to lower the integration costs.
     52 
     53 Another question that arises is how the wallet retrieves products efficiently.
     54 Ideally the entire inventory subset arrives in one response so that up to
     55 roughly 300-400 items can be listed without paginations. Backends already store
     56 image metadata, so the wallet should also be able to fetch product pictures on
     57 request instead of embedding them in the initial payload.
     58 
     59 Requirements
     60 ============
     61 
     62 * Introduce a template type system that distinguishes fixed-order templates from
     63   inventory-driven ones extending existing REST templates, and creates a base for
     64   new possible template types.
     65 * Define merchant-side configuration for product selection, supporting:
     66 
     67   * all inventory,
     68   * category-filtered subsets, and
     69   * explicitly enumerated product IDs; combinations must be merged without
     70     duplicates.
     71 * Describe wallet-side UX affordances for choosing exactly one product or
     72   multiple products, driven by a ``choose_one`` style flag.
     73 * Extend the public ``GET /templates/$TEMPLATE_ID`` response to surface type,
     74   product descriptors, selection rules, and customer-editable defaults.
     75 * Extend the template instantiation ``POST`` to carry the selected products and
     76   quantities, reusing the ``TemplateDetails`` object.
     77 * Preserve handling of legacy template types from protocol versions v13+;
     78   clients that do not support the new inventory-cart type must reject that
     79   type cleanly.
     80 
     81 Proposed Solution
     82 =================
     83 
     84 Schema extensions
     85 -----------------
     86 
     87 Add an endpoint that lets
     88 wallets download product images via ``GET
     89 /instances/$ID/products/$IMAGE_HASH/image``.
     90 
     91 Introduce template type discriminator so processing
     92 of the template can be done per template version.
     93 
     94 .. ts:def:: TemplateType
     95 
     96   type TemplateType = "fixed-order" | "inventory-cart";
     97 
     98 .. ts:def:: TemplateContractDetailsType
     99 
    100   type TemplateContractDetailsType =
    101     TemplateContractDetails | TemplateInventoryContractDetails;
    102 
    103 
    104 Extend ``TemplateAddDetails`` and ``TemplateDetails`` to advertise the new type and, when
    105 ``template_type`` equals ``"inventory-cart"``, nest the inventory-specific
    106 contract.
    107 
    108 .. ts:def:: TemplateAddDetails
    109 
    110     interface TemplateAddDetails {
    111 
    112       // Template ID to use.
    113       template_id: Slug;
    114 
    115       // Human-readable description for the template.
    116       template_description: string;
    117 
    118       // OTP device ID.
    119       // This parameter is optional.
    120       otp_id?: Slug;
    121 
    122       // Fixed contract information for orders created from
    123       // this template.
    124       template_contract: TemplateContractDetailsType;
    125 
    126       // Key-value pairs matching a subset of the
    127       // fields from ``template_contract`` that are
    128       // user-editable defaults for this template.
    129       // Since protocol **v13**.
    130       editable_defaults?: Object;
    131     }
    132 
    133 .. ts:def:: TemplateDetails
    134 
    135     interface TemplateDetails {
    136 
    137       // Fixed contract information for orders created from
    138       // this template.
    139       template_contract: TemplateContractDetailsType;
    140 
    141       // Future fields remain identical to the existing structure.
    142     }
    143 
    144 New contract type has next structure:
    145 
    146 .. ts:def:: TemplateInventoryContractDetails
    147 
    148     interface TemplateInventoryContractDetails {
    149 
    150         // Template type defaults to ``fixed-order`` when missing.
    151         // Must be either ``fixed-order`` or ``inventory-cart``.
    152         // This prescribes which template_contract structure is expected.
    153         // TemplateContractDetails for ``fixed-order``.
    154         // TemplateInventoryContractDetails for ``inventory-cart``.
    155         template_type?: TemplateType;
    156 
    157         // Human-readable summary for the template.
    158         summary?: string;
    159 
    160         // Requests the wallet to offer a tip entry UI. The backend
    161         // verifies that amount equals selected products + tip.
    162         request_tip?: boolean;
    163 
    164         // Time window to pay before the order expires unfulfilled. If omitted
    165         // or zero, the instance's default pay delay is used. "forever" is invalid.
    166         pay_duration?: RelativeTime;
    167 
    168         // Selects all products from merchant inventory and overrides
    169         // selected_categories and selected_products.
    170         selected_all?: boolean;
    171 
    172         // All products from selected categories are included.
    173         selected_categories?: Integer[];
    174 
    175         // Explicit list of product IDs to include.
    176         selected_products?: string[];
    177 
    178         // When true the wallet must enforce single-selection behaviour.
    179         choose_one?: boolean;
    180     }
    181 
    182 Wallets that do not recognise ``"inventory-cart"`` continue to expect
    183 template-level fields such as ``minimum_age``.  They must reject the unknown
    184 template type cleanly instead of attempting to interpret it as a legacy
    185 template.
    186 
    187 The merchant simply saves id's of ``selected_categories``
    188 and ``selected_products``.
    189 
    190 ``choose_one`` dictates whether the wallet must restrict the user
    191 to a single product/quantity combination (``true``) or allow arbitrary
    192 combinations (``false``/absent).
    193 
    194 Merchant private API updates
    195 ----------------------------
    196 
    197 ``POST`` and ``PATCH`` on ``/private/templates`` accept new ``TemplateInventoryContractDetails``.
    198 
    199 SPA
    200 ----
    201 The SPA embeds the new configuration in template
    202 creation forms:
    203 
    204 * Adding support for different ``template_type``.
    205 * Some clever ``template_type`` detection can be introduced, e.g. if the merchant selects the products
    206   automatically changed from ``fixed-order`` to ``inventory-cart``. Manage products from order page can be re-used.
    207 * Inventory selector widgets emit the union of categories and explicit product
    208   selections.
    209 * Optional quantity limits and defaults map to ``item_limits`` and ``item_default``.
    210 
    211 
    212 Wallet discovery API
    213 --------------------
    214 
    215 Enhance the public ``GET /instances/$INSTANCE/templates/$TEMPLATE_ID`` response
    216 to include both the inventory configuration and the resolved product metadata,
    217 by using ``TemplateWalletContractPayload``.
    218 
    219 .. ts:def:: TemplateWalletContractPayload
    220 
    221   type TemplateWalletContractPayload =
    222     TemplateWalletContractDetails | TemplateInventoryContractDetailsWallet;
    223 
    224 .. ts:def:: TemplateWalletContractDetails
    225 
    226   type TemplateWalletContractDetails = TemplateContractDetails;
    227 
    228 ``TemplateWalletContractDetails`` is identical to the ``TemplateContractDetails``
    229 object defined in :ref:`merchant-template-api`. Changes relative to the current
    230 protocol are called out below.
    231 
    232 .. ts:def:: WalletTemplateDetails
    233 
    234   interface WalletTemplateDetails {
    235 
    236       // Hard-coded information about the contract terms
    237       // for this template.
    238       template_contract: TemplateWalletContractPayload;
    239 
    240       // Key-value pairs matching a subset of the
    241       // fields from template_contract that are
    242       // user-editable defaults for this template.
    243       // Since protocol v13.
    244       editable_defaults?: Object;
    245 
    246       // Only present when TemplateWalletContractPayload requires it.
    247       // Required currency for payments.  Useful if no
    248       // amount is specified in the template_contract
    249       // but the user should be required to pay in a
    250       // particular currency anyway.  Merchant backends
    251       // may reject requests if the template_contract
    252       // or editable_defaults do
    253       // specify an amount in a different currency.
    254       // This parameter is optional.
    255       // Since protocol v13.
    256       required_currency?: string;
    257   }
    258 
    259 
    260 ``TemplateInventoryContractDetailsWallet`` intentionally omits a fixed currency
    261 or minimum age to allow multi-currency product listings and leave age checks to
    262 per-product logic when available.
    263 
    264 .. ts:def:: TemplateInventoryContractDetailsWallet
    265 
    266    interface TemplateInventoryContractDetailsWallet {
    267 
    268      // Human-readable summary for the template.
    269      summary?: string;
    270 
    271      // Request the wallet to offer a tip entry UI.
    272      request_tip?: boolean;
    273 
    274      // Time the customer has to pay before the order expires unpaid. If omitted
    275      // or zero, the instance's default pay delay is used. "forever" is invalid.
    276      pay_duration?: RelativeTime;
    277 
    278      // Information about the resolved products.
    279      inventory_payload?: WalletInventoryPayload;
    280    }
    281 
    282 .. ts:def:: WalletInventoryPayload
    283 
    284   interface WalletInventoryPayload {
    285     // Contains all products selected by the merchant.
    286     products: WalletInventoryProduct[];
    287 
    288     // Contains all categories referenced by the products.
    289     categories: WalletInventoryCategory[];
    290 
    291     // Contains all custom units referenced by the products.
    292     units: WalletInventoryUnit[];
    293   }
    294 
    295 The following structures mirror the protocol-v25 inventory payload so that the
    296 backend, SPA, and wallet share a single meaning for every field while keeping
    297 the inventory available in one response.
    298 
    299 .. ts:def:: WalletInventoryProduct
    300 
    301   interface WalletInventoryProduct {
    302     product_id: Slug;
    303     product_name: string;
    304     description: string;
    305     description_i18n?: { [lang_tag: string]: string };
    306     taxes?: Tax[];
    307     unit: Slug;
    308     unit_prices: Amount[];
    309     unit_allow_fraction: boolean;
    310     unit_precision_level: Integer;
    311     remaining_stock: DecimalQuantity;
    312     categories: Integer[];
    313     image_hash?: string;
    314   }
    315 
    316 .. ts:def:: WalletInventoryCategory
    317 
    318   interface WalletInventoryCategory {
    319     category_id: Integer;
    320     category_name: string;
    321     category_name_i18n?: { [lang_tag: string]: string };
    322   }
    323 
    324 .. ts:def:: WalletInventoryUnit
    325 
    326   interface WalletInventoryUnit {
    327     unit: Slug;
    328     unit_name_long: string;
    329     unit_name_long_i18n?: { [lang_tag: string]: string };
    330     unit_name_short: string;
    331     unit_name_short_i18n?: { [lang_tag: string]: string };
    332     unit_allow_fraction: boolean;
    333     unit_precision_level: Integer;
    334   }
    335 
    336 This design lets wallets download hundreds of objects in a single request and
    337 fetch images later via the shared ``GET
    338 /instances/$ID/products/$IMAGE_HASH/image`` endpoint.
    339 
    340 Inventory template responses MUST include the complete product subset in a
    341 single payload; QR-code driven flows remain manageable only when the referenced
    342 catalog fragment comfortably fits into one REST response. Merchants are expected
    343 to keep templates constrained to a practical number of products (tens, not
    344 thousands). If extreme use cases ever arise, pagination can be revisited.
    345 
    346 Template instantiation
    347 ----------------------
    348 
    349 Extend ``POST /instances/$INSTANCE/templates/$TEMPLATE_ID`` to support the
    350 `UsingTemplateCommonRequest` type.
    351 
    352 ``amount`` lets the wallet supply a precalculated total;
    353 backends recompute the authoritative order amount and reject mismatches.
    354 Wallets submit ``InventoryTemplateUseDetails`` to ``POST
    355 /instances/$INSTANCE/templates/$TEMPLATE_ID`` when the template advertises
    356 ``template_type`` = ``"inventory-cart"``. ``tip`` carries the customer-selected
    357 gratuity whenever the template requested it; classic templates consequently
    358 extend ``UsingTemplateDetails`` with the same optional field.
    359 
    360 Backend order creation logic verifies every selected product:
    361 
    362 1. Resolve the template and compute the eligible product set.
    363 2. Ensure user selections are a subset of the resolved list and satisfy
    364    ``choose_one`` / quantity bounds.
    365 3. Construct the contract terms by embedding the selected products as line items
    366    in ``TemplateContractDetails`` before calling the internal order creation
    367    path (same code as ``POST /private/orders``).
    368 4. Record the chosen products in order metadata for fulfilment and reporting.
    369 
    370 When ``tip`` is present, it is simply appended as its own line item(product)
    371 in the order.
    372 
    373 Wallet UX
    374 ---------
    375 
    376 Wallets handle inventory templates as follows:
    377 
    378 1. Fetch ``WalletTemplateDetails`` and cache the resolved inventory.
    379 2. Render a cart builder respecting ``choose_one``.
    380 3. Show a running total computed from per-product prices; totals must match the
    381    backend response before displaying the payment acceptance dialog.
    382 4. Gracefully handle outdated caches by retrying the ``GET`` when the ``POST``
    383    returns a conflict due to inventory changes.
    384 
    385 Compatibility rules
    386 -------------------
    387 
    388 * Templates containing ``template_type`` = ``"inventory-cart"`` require
    389   merchant protocol v25 or later.
    390 * QR codes stay in the same pay-template URI parameters.
    391 
    392 Definition of Done
    393 ==================
    394 
    395 * [x] REST API changes and schema extensions are ratified by wallet and merchant.
    396 * [ ] Merchant SPA support for creating inventory-cart templates.
    397 * [ ] Integration tests cover single-product and multi-product cart creation via
    398   the new template type across merchant and wallet.
    399 * [x] Updated reference documentation describes the new template type and
    400   associated fields.
    401 * [ ] Wallet and merchant SPA have complete workflows and designs for the new
    402   template.
    403 
    404 
    405 Alternatives
    406 ============
    407 
    408 * Keep templates fixed and push cart building to merchant-hosted Web flows,
    409   trading offline capability for implementation simplicity.
    410 * Require merchants to mint one template per product, keeping the current API
    411   untouched but exacerbating QR code sprawl and inventory maintenance.
    412 
    413 Drawbacks
    414 =========
    415 
    416 * Larger template payloads may increase wallet fetch
    417   times, especially for templates with many products.
    418 * More complex validation paths in both wallet and merchant codebases.
    419 * Risk of inconsistent order totals.
    420 
    421 Discussion / Q&A
    422 ================
    423 
    424 What should happen when a customer wants to leave a tip?
    425   In the existing template version ``tip`` is supported when the merchant
    426   allows amount modifications. For the new ``inventory-cart`` type the
    427   ``request_tip`` flag makes that intent explicit. The backend simply appends
    428   the tip as another product that flows to the same payto target as the base
    429   order. Future work can revisit tip splitting, but that extra complexity is
    430   explicitly out of scope here.