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.