096-partial-payments.rst (32347B)
1 DD 96: Partial Payments 2 ####################### 3 4 Summary 5 ======= 6 7 This document proposes support for orders where only part of the total amount 8 is paid with Taler and the remaining amount is paid with other payment 9 methods, such as cash, card, vouchers or others. 10 11 The protocol change must be additive. The existing :ts:type:`Amount` field of 12 an order or choice continues to represent the amount paid with Taler. A new 13 optional ``amount_external`` field carries externally handled payment amounts 14 and the reconciliation metadata needed by POS applications and merchant 15 back-office users. 16 17 Motivation 18 ========== 19 20 In person purchases might involve mixed payments. A customer may pay part of 21 an order in cash and the rest with Taler, or a cashier may need to combine 22 Taler with a card terminal, voucher system or other local payment method. 23 Today, the merchant backend and wallet assume that the amount in the contract 24 is the amount the wallet pays with Taler. This model cannot represent a 25 single receipt and order that is settled by multiple methods. 26 27 The goal is not to make the merchant backend process card or cash payments. 28 The goal is to let the merchant backend, wallet core and POS applications agree 29 on the order total, the Taler portion and the non-Taler portions that must have 30 already been completed outside of Taler. 31 32 Requirements 33 ============ 34 35 * Orders must be able to express mixed payment amounts, for both v0 36 orders and v1 orders with choices. 37 * The existing plain :ts:type:`Amount` form must remain valid for backwards 38 compatibility. 39 * The type and meaning of existing ``amount`` fields must not change. 40 * The existing ``amount`` field remains the amount paid with Taler. 41 * The optional external payment field must not include Taler entries. 42 * The total order amount is the sum of the existing ``amount`` field and all 43 entries in ``amount_external``. This total must be well-defined, so all 44 amounts involved must use a single currency. 45 * The wallet must only pay the existing ``amount`` field. 46 * The POS or other accommodating application must execute all non-Taler 47 payments before the Taler payment. 48 * The Taler payment is always the last payment step. 49 * If the Taler payment fails after other payments succeeded, the POS must 50 either modify the order and retry the Taler step or refund the already 51 completed non-Taler payments. 52 * Orders with settled external payments and a failed Taler payment must 53 remain visible to merchant-facing applications. They must not be deleted 54 by normal order deletion or by accident. 55 * The merchant backend must preserve enough information for receipts, 56 reporting and order inspection to show how the total was split. 57 * Per-method payment information must be stored in a flat structure that the 58 merchant portal can render as a generic table. 59 * The design must not require the wallet to validate that cash, card or other 60 non-Taler payments actually happened. 61 62 Proposed Solution 63 ================= 64 65 Additive Payment Field 66 ---------------------- 67 68 Keep all existing :ts:type:`Amount` fields unchanged. In particular, 69 :ts:type:`OrderV0`.``amount``, :ts:type:`OrderChoice`.``amount`, 70 :ts:type:`ContractTermsV0`.``amount`` and 71 :ts:type:`ContractChoice`.``amount`` remain plain :ts:type:`Amount` values and 72 represent the amount the wallet pays with Taler. 73 74 Add a new optional ``amount_external`` field to the *common* part of orders 75 and contract terms (like ``products`` or the deadlines in 76 :ts:type:`OrderCommon`): 77 78 .. ts:def:: ExternalPaymentInfo 79 80 interface ExternalPaymentInfo { 81 // External payment method, for example "cash" or "card". 82 // Must never be "taler". 83 method: string; 84 85 // Identifier of the payment action within the order. 86 // Examples: "cash1", "sumup1", "sumup2". 87 id: string; 88 89 // Amount covered by this payment action. 90 // Must always be present 91 amount: Amount; 92 93 // Additional method-specific fields. These fields must be 94 // stored only at this level. 95 [field: string]: string | Amount | Integer | boolean | null; 96 } 97 98 .. ts:def:: PartialPaymentFields 99 100 interface PartialPaymentFields { 101 // Payments handled outside of Taler. 102 amount_external?: ExternalPaymentInfo[]; 103 } 104 105 The proposed extension applies to the common part shared by all order and 106 contract terms versions: 107 108 :: 109 110 type OrderCommon = ExistingOrderCommon & PartialPaymentFields; 111 type ContractTermsCommon = ExistingContractTermsCommon & PartialPaymentFields; 112 113 The field is deliberately *not* attached to the individual v1 choices: 114 external payments are settled *before* the wallet ever sees the contract, 115 so they are facts about the order that cannot vary with the choice the 116 wallet later selects. A per-choice field would force the POS to duplicate 117 identical entries into every choice and would even allow choices to state 118 contradictory external payments, which the backend could not police. 119 120 If ``amount_external`` is absent, the order is a regular pure Taler order and 121 the existing ``amount`` field is the total amount. If ``amount_external`` is 122 present, the existing ``amount`` field remains the Taler amount. The full 123 order total is the sum of the selected ``amount`` field (the top-level 124 ``amount`` for v0, the selected choice's ``amount`` for v1) and all 125 entries in ``amount_external``. 126 127 For this total to be well-defined, the backend rejects orders where 128 ``amount_external`` is present and any amount uses a different currency: all 129 entries in ``amount_external`` must use the currency of the Taler ``amount`` 130 and, for v1 orders, *every* choice must be denominated in that same currency. 131 Multi-currency choices remain possible for pure Taler orders without 132 ``amount_external``. 133 134 For example, an order where the customer pays CHF 30 in cash and CHF 20 in 135 Taler keeps ``amount`` as ``CHF:20`` and adds ``amount_external``: 136 137 :: 138 139 { 140 "amount": "CHF:20", 141 "amount_external": [ 142 { 143 "method": "cash", 144 "id": "cash1", 145 "amount": "CHF:30", 146 "cashier_number": "7" 147 } 148 ] 149 } 150 151 For a v1 order the field stays at the top level next to ``choices``; the 152 CHF 30 cash payment applies regardless of which choice the wallet selects: 153 154 :: 155 156 { 157 "version": 1, 158 "amount_external": [ 159 { "method": "cash", "id": "cash1", "amount": "CHF:30" } 160 ], 161 "choices": [ 162 { "amount": "CHF:20", ... }, 163 { "amount": "CHF:15", "inputs": [ ... ], ... } 164 ] 165 } 166 167 This is backwards compatible for old wallets because they continue to see a 168 plain :ts:type:`Amount` in ``amount``. Such wallets may not render the full 169 mixed-payment total, but they can still pay the Taler portion. Updated wallets 170 should render both the full total and the selected Taler amount clearly. 171 172 An order may also have a Taler amount of zero. This allows a POS or merchant 173 portal to use the merchant backend for product tracking, receipts and reports 174 even when the customer paid the whole amount with cash, card or another 175 external method. Such orders are completed using the private collect operation 176 described below. 177 178 Payment Method Names 179 -------------------- 180 181 The initial reserved method name is: 182 183 * ``cash`` for cash accepted by the merchant or cashier 184 185 For now, payment method names are not centrally registered. Integrations may 186 use any stable ASCII identifier. 187 188 The name ``taler`` is reserved and must not be used in ``amount_external``. 189 Taler is represented by the existing ``amount`` field. 190 191 Payment Details 192 --------------- 193 194 For cash payments, additional fields may include the cashier name, cashier 195 number, register identifier or similar local information. For card payments, 196 additional fields may include the terminal identifier, acquirer reference, 197 transaction ID or authorization code. Other systems may add the fields they 198 need for reconciliation or audit. 199 200 The additional fields must be stored only one level below the payment entry. 201 Nested method-specific objects should not be used. This allows the merchant 202 portal to render ``amount_external`` as a simple table without knowing a custom 203 rendering format for each payment method. 204 205 The fields beyond ``method``, ``id`` and ``amount`` are deliberately generic in 206 this design and must remain flat, with no nested method-specific objects. This 207 keeps rendering simple and avoids having to standardize every card terminal, 208 cash register, external voucher or future payment integration up front. The 209 drawback is that these fields are mostly display and reconciliation metadata. 210 If GNU Taler later needs to use method-specific fields for reports, cash 211 tracking, refund UI or automated reconciliation, those fields should be 212 standardized explicitly in a future design. 213 214 Payment Flow 215 ------------ 216 217 The POS or integrating application is responsible for orchestrating mixed 218 payments: 219 220 1. Create or update the order with ``amount_external`` that reflects the 221 intended externally handled payment amount. 222 2. Run all non-Taler payment steps, such as cash handling or card terminal 223 authorization. 224 3. Start the Taler payment as the final step. 225 4. Complete the sale only after the merchant backend confirms the Taler 226 payment. If the Taler amount is zero, use the private collect operation 227 described in `Zero-Taler Order Collection`_. 228 229 The wallet receives the contract terms and computes the payable Taler amount 230 from the existing ``amount`` field. It may use ``amount_external`` to render 231 the full total so that the customer understands why the Taler amount is lower 232 than the order total. 233 234 Because the Taler payment is the last step, the wallet only sees the order and 235 contract terms after the externally handled payments have been settled. 236 237 If the payment split changes before the Taler payment starts, the expected 238 flow is to delete or abandon the old order and create a new order with updated 239 external amounts. Updating an already visible or claimed order to add or remove 240 external payments is out of scope for this design. 241 242 Zero-Taler Order Collection 243 --------------------------- 244 245 Orders whose Taler ``amount`` is zero, for example because the full total was 246 settled through ``amount_external``, are completed through a new private 247 endpoint ``POST [/instances/$INSTANCE]/private/orders/$ORDER_ID/collect``. 248 The merchant backend acts exactly like a customer wallet collecting a free 249 order, executing the same two steps a wallet would: it first claims the 250 order with a backend-generated nonce, and then has the order paid. 251 252 Neither step is reimplemented. Claiming calls the same routine the wallet 253 facing claim endpoint calls, and for the payment the request is turned into 254 the payment request a wallet would send for a free order -- no coins, and the 255 selected ``choice_index`` -- and handed to the payment handler itself. Payment 256 notifications, webhooks, triggers and inventory updates therefore do not merely 257 resemble those of a wallet payment, they are the same code, and the response of 258 the collect endpoint is the response of that payment. Adding a step to the 259 payment flow later does not require touching this endpoint. 260 261 Only genuinely free orders are handed over, as the payment logic expects to be 262 paid with coins and reports nothing useful when it is not. 263 264 The backend derives the claim nonce deterministically from the instance 265 public key and the order identifier. This makes the collect operation idempotent 266 (repeating it after a success or a crash between the claim and the payment 267 step completes or replays harmlessly) and lets the backend distinguish 268 orders it collected itself from orders claimed by a customer wallet. 269 270 Collection is only possible while the order is unclaimed. If a customer wallet 271 has already claimed the order, the wallet owns the order and must execute the 272 free payment itself; the collect operation fails and reports that the order 273 was claimed by a wallet 274 (``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_ALREADY_CLAIMED``). 275 This preserves the customer's ability to obtain a wallet 276 receipt: a POS should offer the order QR code first and only collect after the 277 customer declined collecting it with own wallet. 278 279 Collection is restricted to genuinely free Taler payments: the Taler 280 ``amount`` must be zero and, for v1 contracts, the selected choice must have no 281 ``inputs`` and no ``outputs``. Choices that redeem or issue tokens, such as 282 subscription tokens or donation receipts, require a customer wallet and cannot 283 be collected 284 (``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_NOT_FREE``). 285 286 The optional ``session_id`` of the collect request is stored like the session 287 identifier of a wallet payment, so a POS device can tag and later list the 288 orders it collected. For v1 orders, the ``choice_index`` of the collect 289 request selects the choice to complete and is mandatory: the backend never 290 picks a choice on its own, exactly as a wallet has to select one when paying. 291 Collecting a v1 order without a ``choice_index`` fails with 292 ``TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING``, and naming a 293 choice that does not exist -- including naming any choice for a v0 order, 294 which has none -- fails with 295 ``TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS``. 296 Defaulting to the first choice was rejected: which choice was completed 297 determines the amount, the tokens consumed and issued, and the fulfillment, 298 so it is a decision the client must make explicitly rather than one the 299 backend guesses. 300 301 Failure Handling 302 ---------------- 303 304 Mixed payments introduce a failure mode where a non-Taler payment has already 305 succeeded but the final Taler payment fails. The merchant backend cannot 306 automatically repair this state because it does not control the external 307 payment method. 308 309 The POS or integrating application must therefore choose one of these recovery 310 paths: 311 312 * modify the order payment split and retry the Taler payment; 313 * cancel the order and refund or void the completed non-Taler payments; 314 * proceed with different payment method, and make Taler part lower or zero. 315 316 Until one of these recovery paths is completed, the order must remain visible 317 to merchant-facing applications. No dedicated order status value is 318 introduced for this: the order simply remains in its regular claimed or 319 unpaid state, and merchant-facing applications can recognize the situation 320 from the presence of ``amount_external`` in an unpaid order. Such orders 321 must not be deleted through normal order deletion. Any order carrying 322 ``amount_external`` can only be deleted by explicitly passing ``force=yes`` 323 to ``DELETE /private/orders/$ORDER_ID``; a normal deletion attempt fails 324 with ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_EXTERNALLY_PAID``. The force 325 requirement makes it clear that externally handled payments may already 326 have settled. 327 328 Receipt Handling 329 ---------------- 330 331 For normal wallet flows, the customer can access the Taler receipt after the 332 wallet payment. In POS deployments this may not be enough. Some jurisdictions 333 require a printed or otherwise directly provided receipt, and in a mixed 334 payment flow the customer may not receive a Taler receipt if the POS 335 application performs self-pickup or the Taler amount is zero. 336 337 POS applications and other accommodating applications must therefore support a 338 mode where they retrieve the receipt themselves from the merchant backend and 339 provide it to the customer through the locally required channel, such as a 340 printer, terminal display, e-mail or another regulated receipt mechanism. 341 342 Reporting 343 --------- 344 345 The merchant backend should store ``amount_external`` as part of the contract 346 terms and expose it through order status and history APIs. Existing reporting 347 that expects a single amount should continue to show the Taler amount from the 348 existing ``amount`` field. Detailed views should show the externally handled 349 amounts and the full order total. 350 351 The merchant portal should render ``amount_external`` as a table. Common 352 columns are ``method``, ``id`` and ``amount``. Additional columns can be 353 derived from the union of the flat method-specific fields present in the 354 payment entries. The merchant portal should not need method-specific 355 rendering logic to show this information. 356 357 External Refunds 358 ---------------- 359 360 Taler refunds continue to use the existing refund mechanism and are capped at 361 the amount actually paid with Taler. Since contract terms are hashed and signed 362 at claim time, external refunds must not modify ``amount_external``. Instead, 363 the merchant backend records them separately, analogous to how Taler refunds 364 are stored outside the contract terms. 365 366 A new private operation, for example 367 ``POST /private/orders/$ORDER_ID/refund-external``, records an external refund 368 entry: 369 370 .. ts:def:: ExternalRefundInfo 371 372 interface ExternalRefundInfo { 373 // Method by which the funds were returned to the customer, 374 // for example "cash" or "card". May differ from the methods 375 // used to pay the order. Must never be "taler". 376 method: string; 377 378 // Identifier of this refund within the order, chosen by the 379 // merchant. Mandatory, see below. 380 id: string; 381 382 // Optionally, the "id" of the "amount_external" entry this 383 // refund reverses, when the refund maps to a specific original 384 // payment, for example a card transaction reversal. 385 payment_id?: string; 386 387 // Amount returned to the customer via the external method. 388 amount: Amount; 389 390 // Human-readable refund justification, mirroring Taler refunds. 391 reason: string; 392 } 393 394 Unlike ``amount_external`` entries, external refunds are stored in dedicated 395 typed database columns rather than as a JSON blob, so every field the backend 396 reports is authoritative. Consequently an external refund carries no 397 method-specific extra fields for now; if a concrete integration needs them, 398 they can be added later without changing the fields above. 399 400 The ``id`` makes recording an external refund idempotent. The backend stores a 401 hash of the request body alongside the entry, mirroring how ``h_post_data`` 402 makes order creation idempotent: repeating a request with the same ``id`` and 403 an identical body succeeds without recording a second refund, whereas reusing 404 an ``id`` with different details fails with 405 ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_EXTERNAL_ALREADY_EXISTS``. 406 This matters because a point-of-sale application that retries after a network 407 failure must not consume the refundable amount twice. 408 409 The ``id`` is mandatory rather than generated by the backend when absent, even 410 though order identifiers work the other way around. The difference is what a 411 duplicate costs: a duplicated order is inert and eventually cleaned up, whereas 412 a duplicated external refund permanently consumes part of the amount that may 413 still be refunded for a real order. Since a recorded external refund cannot be 414 removed, the identifier has to come from whoever knows whether two requests 415 describe the same real-world refund, which is the client. 416 417 Being unable to remove a recorded external refund is a deliberate limitation 418 of this design, matching Taler refunds, which can only ever be increased. It 419 does mean a refund recorded in error -- a wrong amount, or cash that never 420 actually left the till -- cannot be corrected, and deleting the order is not 421 an option because external refunds only exist on paid orders. Should this turn 422 out to be needed in practice, the natural extension is to mark such entries as 423 void rather than to delete them, so that the mistake stays visible for 424 bookkeeping, and to exclude voided entries when computing the refunded total. 425 That is left to a future design document. 426 427 The refund channel does not need to match the payment channel: a shop may 428 return the whole amount in cash even when parts of the order were paid by card 429 or with Taler. This is particularly relevant because Taler refunds are 430 constrained by the contract's refund deadline. Once it has passed, the Taler 431 portion can only be returned through an external method. 432 433 For this reason external refunds are available for *all paid orders*, not 434 only for orders that carry ``amount_external``: a pure Taler order whose 435 refund deadline has passed can still be settled with the customer in cash, 436 and the merchant backend must be able to record that for proper bookkeeping. 437 438 External refunds require the order to be paid; recording a refund for an 439 unpaid order fails with 440 ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID``. An unpaid 441 order has no settled Taler payment to reverse; if its external payments need 442 to be undone, the POS deletes the order (with force) and, if needed, creates 443 a new one with an updated payment split. 444 445 The backend validates external refunds against the order total rather 446 than against individual payment entries: the cumulative externally refunded 447 amount must not exceed the full order total minus the amount already refunded 448 through Taler 449 (``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_EXTERNAL_INCONSISTENT_AMOUNT``), 450 and must use the same currency as the order. There is no override for these 451 checks; they protect the refund ledger against recording errors. 452 453 Unlike Taler refunds, external refund entries are bookkeeping only. The POS or 454 external payment integration performs the actual return of funds, and no wallet 455 pickup step exists or is needed. Order status APIs expose the recorded entries, 456 for example as ``refunds_external``, and the merchant portal renders them as a 457 generic table, like ``amount_external``. 458 459 For zero-Taler orders collected without a customer wallet, no Taler refund is 460 ever possible because the Taler amount paid is zero. All refunds on such orders 461 are therefore external by construction. 462 463 Vouchers and Tokens 464 ------------------- 465 466 External vouchers can be represented as entries in ``amount_external``. Taler 467 wallet tokens, discounts, gift vouchers and P2P transfer of such vouchers are a 468 separate feature area. In particular, this design does not define how a 469 wallet-held voucher can be partially spent, whether remaining value is 470 re-issued as a new token, or how voucher tokens can be transferred between 471 wallets. 472 473 Test Plan 474 ========= 475 476 * Merchant backend tests for accepting existing plain :ts:type:`Amount` fields 477 unchanged. 478 * Merchant backend tests accepting optional ``amount_external`` in the common 479 part of v0 and v1 orders. 480 * Merchant backend tests rejecting ``amount_external`` with ``taler`` entries, 481 mixed currencies, invalid method names, duplicate entry ids or nested 482 method-specific fields. 483 * Merchant backend tests rejecting v1 orders with ``amount_external`` where 484 any choice uses a different currency than the external entries. 485 * Merchant backend tests preserving ``amount_external`` entries with flat 486 method-specific fields. 487 * Merchant backend tests rejecting normal deletion of orders carrying 488 ``amount_external`` unless an explicit force operation is used. 489 * Merchant backend tests collecting an unclaimed zero-Taler v0 order and a 490 zero-amount v1 choice via the private collect endpoint. 491 * Merchant backend tests rejecting collect for orders with nonzero Taler 492 amounts, for v1 choices with token inputs or outputs, and for orders already 493 claimed by a wallet. 494 * Merchant backend tests rejecting collect of a v1 order that does not select 495 a choice or selects one that does not exist, and of a v0 order that selects 496 a choice although it has none. 497 * Merchant backend tests for collect idempotency. 498 * Merchant backend tests rejecting normal deletion of an order claimed by a 499 wallet unless an explicit force operation is used. 500 * Merchant backend tests recording external refunds, including refunds through 501 a method different from the original payment methods and refunds on pure 502 Taler orders without ``amount_external``. 503 * Merchant backend tests rejecting external refunds that would exceed the order 504 total minus the amount refunded through Taler, that use a different 505 currency or the method name ``taler``, or that target an unpaid order. 506 * Merchant backend tests recording an external refund twice under the same 507 ``id``, once with an identical request body, which has to be idempotent, and 508 once with different details, which has to be refused as a conflict. 509 * Wallet core tests for paying the existing ``amount`` field and rendering the 510 full total from ``amount_external`` when present. 511 * POS integration tests for a successful cash/card-first and Taler-last flow. 512 * POS integration tests for Taler failure after a non-Taler payment succeeded. 513 514 Definition of Done 515 ================== 516 517 * Merchant backend supports the new additive ``amount_external`` field for 518 order creation, contract terms, order status and history. 519 * Merchant backend keeps all existing ``amount`` fields as plain 520 :ts:type:`Amount` values. 521 * Merchant backend validates that ``amount_external`` has no ``taler`` entries, 522 that all entries use the same currency as ``amount`` and that, for v1 523 orders, every choice uses that currency as well. 524 * Merchant backend preserves per-method payment details in ``amount_external``. 525 * Merchant backend keeps orders with settled external payments and a failed 526 Taler payment visible for manual resolution. 527 * Merchant backend prevents normal deletion of such orders and requires an 528 explicit force operation to remove them. 529 * Merchant backend provides the private collect operation for zero-Taler 530 orders, restricted to unclaimed orders and genuinely free choices, with 531 payment notifications identical to a wallet payment, implemented 532 idempotently. 533 * Merchant backend records external refund entries for any paid order, 534 validates them against the combined refund cap, and exposes them through 535 order status APIs. 536 * Wallet core pays the existing ``amount`` field and does not require 537 ``amount_external`` to complete the Taler payment. 538 * Wallet UIs can display the total and the selected Taler amount clearly. 539 * POS and other accommodating applications support the required orchestration: 540 non-Taler payments first, Taler payment last. 541 * Merchant portal renders ``amount_external`` as a generic table without 542 method-specific renderers. 543 * Merchant portal renders external refunds as a generic table without 544 method-specific renderers. 545 * Documentation explains that external refunds are bookkeeping entries and 546 failure recovery is owned by the integrating application. 547 548 Alternatives 549 ============ 550 551 Change the Amount Field Type 552 ---------------------------- 553 554 The initial proposal changed the existing ``amount`` fields from 555 :ts:type:`Amount` to ``Amount | AmountObject``. This was rejected because it 556 would be a destructive protocol change: every component that currently parses 557 ``amount`` as a string would have to handle a new object shape. Keeping 558 ``amount`` unchanged and adding ``amount_external`` preserves backwards 559 compatibility. 560 561 Attach amount_external to Individual Choices 562 -------------------------------------------- 563 564 An earlier revision of this design attached ``amount_external`` to each 565 amount-bearing object, i.e. also to the individual v1 ``choices`` 566 (``type OrderChoice = ExistingOrderChoice & PartialPaymentFields``). This 567 was rejected: external payments are settled before the contract is shown to 568 the wallet, so they cannot differ between choices. Per-choice fields would 569 have forced the POS to duplicate identical entries into every choice and 570 would have allowed choices to state contradictory external payments without 571 the backend being able to reject them. It also left the external refund cap 572 ill-defined for unpaid multi-choice orders. Placing the field in the common 573 part avoids all of this at the cost of requiring a single currency across 574 all choices whenever ``amount_external`` is present. 575 576 Store Payment Details in Extra 577 ------------------------------ 578 579 Another initial proposal stored the payment split under ``extra.payments``. 580 This was rejected because ``extra`` is intended for proprietary 581 merchant-specific information. Official protocol fields should be explicit 582 top-level fields, not hidden under the merchant extension area. 583 584 Create Separate Orders 585 ---------------------- 586 587 The POS could create one Taler order only for the Taler amount and track cash 588 or card payments in its own system. This avoids changing the contract amount 589 type, but it loses the single-order receipt and reporting model. It also makes 590 customer-facing order totals harder to verify. As well it looses the backup 591 and synchronisation between device possibilities. 592 593 Let Taler Run Before Other Methods 594 ---------------------------------- 595 596 Running Taler before cash or card would make the Taler part successful while 597 the external payment can still fail. That leaves the merchant with a paid 598 Taler contract for an order that may not be otherwise settled. Requiring Taler 599 to be last gives the POS a clearer recovery path because external payments can 600 still be voided, refunded or used to recompute the remaining Taler amount. As 601 well it can create problems when refund deadline for Taler option was set as 0 602 and other method of payment failed. 603 604 Use Templates or Mutable Payment Sessions 605 ----------------------------------------- 606 607 Templates or a new payment-session model could allow the customer to inspect an 608 order before choosing how to split the payment, and could support adding, 609 modifying or deleting payment parts before finalization. This would be more 610 flexible than DD96, but it would require a larger design across merchant 611 backend, POS apps and wallets. DD96 is limited to the current ``/orders`` flow 612 where external payments are settled before the Taler order is created. 613 614 Split One Order Across Multiple Taler Wallets 615 --------------------------------------------- 616 617 Multiple customers paying one order with multiple Taler wallets is out of 618 scope. The current workaround is to split the sale into multiple orders or 619 sub-orders, for example based on the products consumed by each customer. 620 621 Drawbacks 622 ========= 623 624 * POS implementations must handle partial failure and external refunds 625 carefully. 626 * Old wallets may only render the Taler amount and not the full mixed-payment 627 total until they learn the new ``amount_external`` field. 628 * The customer cannot inspect the order or contract terms in the wallet before 629 externally handled payments, such as cash or card payments, have been 630 settled. 631 * Reporting and refund UIs must distinguish total order amount from Taler-paid 632 amount. They must also distinguish Taler refunds, which require wallet 633 pickup, from external refunds, which are bookkeeping entries only. 634 635 Open Questions 636 ============== 637 638 * Should money pots store full totals, per-method totals, or both? Should 639 merchant backend auto create new pots per each new payment method found in 640 order? 641 642 Discussion / Q&A 643 ================ 644 645 * Feedback from Florian Dold: ``extra`` must remain reserved for proprietary 646 merchant fields and must not carry official protocol data. Protocol changes 647 should be additive, so the existing ``amount`` field should not change type. 648 The design was updated accordingly: the existing ``amount`` remains the 649 Taler amount, while a new additive ``amount_external`` field carries the 650 externally handled amounts and reconciliation metadata. 651 652 * Revisions from the implementation review of the merchant backend 653 (2026-07): ``amount_external`` was moved from the per-choice objects to 654 the common order/contract terms part, since settled external payments are 655 facts that cannot vary with the wallet's selection (see `Attach 656 amount_external to Individual Choices`_). As a consequence, orders with 657 ``amount_external`` are restricted to a single currency across all 658 choices, so that the order total and the external refund cap are always 659 well-defined. External refunds were clarified to require a *paid* order 660 and to be available for all paid orders, including pure Taler orders, 661 so that returns after the Taler refund deadline can be recorded for 662 proper bookkeeping; a force override for the refund cap was considered 663 and rejected, since the strict check protects the refund ledger against 664 recording errors. Collect was specified to be idempotent via a 665 deterministic backend claim nonce, with a mandatory ``choice_index`` for 666 v1 orders. No dedicated manual-resolution status value was 667 introduced; blocking non-forced deletion was deemed sufficient. Error 668 codes 2535-2538 (``..._COLLECT_NOT_FREE``, ``..._COLLECT_ALREADY_CLAIMED``, 669 ``..._DELETE_ORDERS_EXTERNALLY_PAID``, 670 ``..._REFUND_EXTERNAL_INCONSISTENT_AMOUNT``) were registered in GANA for 671 the new failure modes.