taler-docs

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

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


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