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.