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