taler-docs

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

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


      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.
    165         pay_duration: RelativeTime;
    166 
    167         // Selects all products from merchant inventory and overrides
    168         // selected_categories and selected_products.
    169         selected_all?: boolean;
    170 
    171         // All products from selected categories are included.
    172         selected_categories?: Integer[];
    173 
    174         // Explicit list of product IDs to include.
    175         selected_products?: string[];
    176 
    177         // When true the wallet must enforce single-selection behaviour.
    178         choose_one?: boolean;
    179     }
    180 
    181 Wallets that do not recognise ``"inventory-cart"`` continue to expect
    182 template-level fields such as ``minimum_age``.  They must reject the unknown
    183 template type cleanly instead of attempting to interpret it as a legacy
    184 template.
    185 
    186 The merchant simply saves id's of ``selected_categories``
    187 and ``selected_products``.
    188 
    189 ``choose_one`` dictates whether the wallet must restrict the user
    190 to a single product/quantity combination (``true``) or allow arbitrary
    191 combinations (``false``/absent).
    192 
    193 Merchant private API updates
    194 ----------------------------
    195 
    196 ``POST`` and ``PATCH`` on ``/private/templates`` accept new ``TemplateInventoryContractDetails``.
    197 
    198 SPA
    199 ----
    200 The SPA embeds the new configuration in template
    201 creation forms:
    202 
    203 * Adding support for different ``template_type``.
    204 * Some clever ``template_type`` detection can be introduced, e.g. if the merchant selects the products
    205   automatically changed from ``fixed-order`` to ``inventory-cart``. Manage products from order page can be re-used.
    206 * Inventory selector widgets emit the union of categories and explicit product
    207   selections.
    208 * Optional quantity limits and defaults map to ``item_limits`` and ``item_default``.
    209 
    210 
    211 Wallet discovery API
    212 --------------------
    213 
    214 Enhance the public ``GET /instances/$INSTANCE/templates/$TEMPLATE_ID`` response
    215 to include both the inventory configuration and the resolved product metadata,
    216 by using ``TemplateWalletContractPayload``.
    217 
    218 .. ts:def:: TemplateWalletContractPayload
    219 
    220   type TemplateWalletContractPayload =
    221     TemplateWalletContractDetails | TemplateInventoryContractDetailsWallet;
    222 
    223 .. ts:def:: TemplateWalletContractDetails
    224 
    225   type TemplateWalletContractDetails = TemplateContractDetails;
    226 
    227 ``TemplateWalletContractDetails`` is identical to the ``TemplateContractDetails``
    228 object defined in :ref:`merchant-template-api`. Changes relative to the current
    229 protocol are called out below.
    230 
    231 .. ts:def:: WalletTemplateDetails
    232 
    233   interface WalletTemplateDetails {
    234 
    235       // Hard-coded information about the contract terms
    236       // for this template.
    237       template_contract: TemplateWalletContractPayload;
    238 
    239       // Key-value pairs matching a subset of the
    240       // fields from template_contract that are
    241       // user-editable defaults for this template.
    242       // Since protocol v13.
    243       editable_defaults?: Object;
    244 
    245       // Only present when TemplateWalletContractPayload requires it.
    246       // Required currency for payments.  Useful if no
    247       // amount is specified in the template_contract
    248       // but the user should be required to pay in a
    249       // particular currency anyway.  Merchant backends
    250       // may reject requests if the template_contract
    251       // or editable_defaults do
    252       // specify an amount in a different currency.
    253       // This parameter is optional.
    254       // Since protocol v13.
    255       required_currency?: string;
    256   }
    257 
    258 
    259 ``TemplateInventoryContractDetailsWallet`` intentionally omits a fixed currency
    260 or minimum age to allow multi-currency product listings and leave age checks to
    261 per-product logic when available.
    262 
    263 .. ts:def:: TemplateInventoryContractDetailsWallet
    264 
    265    interface TemplateInventoryContractDetailsWallet {
    266 
    267      // Human-readable summary for the template.
    268      summary?: string;
    269 
    270      // Request the wallet to offer a tip entry UI.
    271      request_tip?: boolean;
    272 
    273      // Time the customer has to pay before the order expires unpaid.
    274      pay_duration: RelativeTime;
    275 
    276      // Information about the resolved products.
    277      inventory_payload?: WalletInventoryPayload;
    278    }
    279 
    280 .. ts:def:: WalletInventoryPayload
    281 
    282   interface WalletInventoryPayload {
    283     // Contains all products selected by the merchant.
    284     products: WalletInventoryProduct[];
    285 
    286     // Contains all categories referenced by the products.
    287     categories: WalletInventoryCategory[];
    288 
    289     // Contains all custom units referenced by the products.
    290     units: WalletInventoryUnit[];
    291   }
    292 
    293 The following structures mirror the protocol-v25 inventory payload so that the
    294 backend, SPA, and wallet share a single meaning for every field while keeping
    295 the inventory available in one response.
    296 
    297 .. ts:def:: WalletInventoryProduct
    298 
    299   interface WalletInventoryProduct {
    300     product_id: Slug;
    301     product_name: string;
    302     description: string;
    303     description_i18n?: { [lang_tag: string]: string };
    304     taxes?: Tax[];
    305     unit: Slug;
    306     unit_prices: Amount[];
    307     unit_allow_fraction: boolean;
    308     unit_precision_level: Integer;
    309     remaining_stock: DecimalQuantity;
    310     categories: Integer[];
    311     image_hash?: string;
    312   }
    313 
    314 .. ts:def:: WalletInventoryCategory
    315 
    316   interface WalletInventoryCategory {
    317     category_id: Integer;
    318     category_name: string;
    319     category_name_i18n?: { [lang_tag: string]: string };
    320   }
    321 
    322 .. ts:def:: WalletInventoryUnit
    323 
    324   interface WalletInventoryUnit {
    325     unit: Slug;
    326     unit_name_long: string;
    327     unit_name_long_i18n?: { [lang_tag: string]: string };
    328     unit_name_short: string;
    329     unit_name_short_i18n?: { [lang_tag: string]: string };
    330     unit_allow_fraction: boolean;
    331     unit_precision_level: Integer;
    332   }
    333 
    334 This design lets wallets download hundreds of objects in a single request and
    335 fetch images later via the shared ``GET
    336 /instances/$ID/products/$IMAGE_HASH/image`` endpoint.
    337 
    338 Inventory template responses MUST include the complete product subset in a
    339 single payload; QR-code driven flows remain manageable only when the referenced
    340 catalog fragment comfortably fits into one REST response. Merchants are expected
    341 to keep templates constrained to a practical number of products (tens, not
    342 thousands). If extreme use cases ever arise, pagination can be revisited.
    343 
    344 Template instantiation
    345 ----------------------
    346 
    347 Extend ``POST /instances/$INSTANCE/templates/$TEMPLATE_ID`` to support the
    348 `UsingTemplateCommonRequest` type.
    349 
    350 ``amount`` lets the wallet supply a precalculated total;
    351 backends recompute the authoritative order amount and reject mismatches.
    352 Wallets submit ``InventoryTemplateUseDetails`` to ``POST
    353 /instances/$INSTANCE/templates/$TEMPLATE_ID`` when the template advertises
    354 ``template_type`` = ``"inventory-cart"``. ``tip`` carries the customer-selected
    355 gratuity whenever the template requested it; classic templates consequently
    356 extend ``UsingTemplateDetails`` with the same optional field.
    357 
    358 Backend order creation logic verifies every selected product:
    359 
    360 1. Resolve the template and compute the eligible product set.
    361 2. Ensure user selections are a subset of the resolved list and satisfy
    362    ``choose_one`` / quantity bounds.
    363 3. Construct the contract terms by embedding the selected products as line items
    364    in ``TemplateContractDetails`` before calling the internal order creation
    365    path (same code as ``POST /private/orders``).
    366 4. Record the chosen products in order metadata for fulfilment and reporting.
    367 
    368 When ``tip`` is present, it is simply appended as its own line item(product)
    369 in the order.
    370 
    371 Wallet UX
    372 ---------
    373 
    374 Wallets handle inventory templates as follows:
    375 
    376 1. Fetch ``WalletTemplateDetails`` and cache the resolved inventory.
    377 2. Render a cart builder respecting ``choose_one``.
    378 3. Show a running total computed from per-product prices; totals must match the
    379    backend response before displaying the payment acceptance dialog.
    380 4. Gracefully handle outdated caches by retrying the ``GET`` when the ``POST``
    381    returns a conflict due to inventory changes.
    382 
    383 Compatibility rules
    384 -------------------
    385 
    386 * Templates containing ``template_type`` = ``"inventory-cart"`` require
    387   merchant protocol v25 or later.
    388 * QR codes stay in the same pay-template URI parameters.
    389 
    390 Definition of Done
    391 ==================
    392 
    393 * [x] REST API changes and schema extensions are ratified by wallet and merchant.
    394 * [ ] Merchant SPA support for creating inventory-cart templates.
    395 * [ ] Integration tests cover single-product and multi-product cart creation via
    396   the new template type across merchant and wallet.
    397 * [x] Updated reference documentation describes the new template type and
    398   associated fields.
    399 * [ ] Wallet and merchant SPA have complete workflows and designs for the new
    400   template.
    401 
    402 
    403 Alternatives
    404 ============
    405 
    406 * Keep templates fixed and push cart building to merchant-hosted Web flows,
    407   trading offline capability for implementation simplicity.
    408 * Require merchants to mint one template per product, keeping the current API
    409   untouched but exacerbating QR code sprawl and inventory maintenance.
    410 
    411 Drawbacks
    412 =========
    413 
    414 * Larger template payloads may increase wallet fetch
    415   times, especially for templates with many products.
    416 * More complex validation paths in both wallet and merchant codebases.
    417 * Risk of inconsistent order totals.
    418 
    419 Discussion / Q&A
    420 ================
    421 
    422 What should happen when a customer wants to leave a tip?
    423   In the existing template version ``tip`` is supported when the merchant
    424   allows amount modifications. For the new ``inventory-cart`` type the
    425   ``request_tip`` flag makes that intent explicit. The backend simply appends
    426   the tip as another product that flows to the same payto target as the base
    427   order. Future work can revisit tip splitting, but that extra complexity is
    428   explicitly out of scope here.