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