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.