096-partial-payments.rst (22831B)
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 and choices must be able to express mixed payment amounts. 36 * The existing plain :ts:type:`Amount` form must remain valid for backwards 37 compatibility. 38 * The type and meaning of existing ``amount`` fields must not change. 39 * The existing ``amount`` field remains the amount paid with Taler. 40 * The optional external payment field must not include Taler entries. 41 * The total order amount is the sum of the existing ``amount`` field and all 42 entries in ``amount_external``. 43 * The wallet must only pay the existing ``amount`` field. 44 * The POS or other accommodating application must execute all non-Taler 45 payments before the Taler payment. 46 * The Taler payment is always the last payment step. 47 * If the Taler payment fails after other payments succeeded, the POS must 48 either modify the order and retry the Taler step or refund the already 49 completed non-Taler payments. 50 * Orders with settled external payments and a failed Taler payment must remain 51 visible in a manual-resolution status. They must not be deleted by normal 52 order cleanup or by accident. 53 * The merchant backend must preserve enough information for receipts, 54 reporting and order inspection to show how the total was split. 55 * Per-method payment information must be stored in a flat structure that the 56 merchant portal can render as a generic table. 57 * The design must not require the wallet to validate that cash, card or other 58 non-Taler payments actually happened. 59 60 Proposed Solution 61 ================= 62 63 Additive Payment Field 64 ---------------------- 65 66 Keep all existing :ts:type:`Amount` fields unchanged. In particular, 67 :ts:type:`OrderV0`.``amount``, :ts:type:`OrderChoice`.``amount`, 68 :ts:type:`ContractTermsV0`.``amount`` and 69 :ts:type:`ContractChoice`.``amount`` remain plain :ts:type:`Amount` values and 70 represent the amount the wallet pays with Taler. 71 72 Add a new optional ``amount_external`` field next to these existing ``amount`` 73 fields: 74 75 .. ts:def:: ExternalPaymentInfo 76 77 interface ExternalPaymentInfo { 78 // External payment method, for example "cash" or "card". 79 // Must never be "taler". 80 method: string; 81 82 // Identifier of the payment action within the order. 83 // Examples: "cash1", "sumup1", "sumup2". 84 id: string; 85 86 // Amount covered by this payment action. 87 // Must always be present 88 amount: Amount; 89 90 // Additional method-specific fields. These fields must be 91 // stored only at this level. 92 [field: string]: string | Amount | Integer | boolean | null; 93 } 94 95 .. ts:def:: PartialPaymentFields 96 97 interface PartialPaymentFields { 98 // Payments handled outside of Taler. 99 amount_external?: ExternalPaymentInfo[]; 100 } 101 102 The proposed extension applies to the amount-bearing order and contract 103 objects: 104 105 :: 106 107 type OrderV0 = ExistingOrderV0 & PartialPaymentFields; 108 type OrderChoice = ExistingOrderChoice & PartialPaymentFields; 109 type ContractTermsV0 = ExistingContractTermsV0 & PartialPaymentFields; 110 type ContractChoice = ExistingContractChoice & PartialPaymentFields; 111 112 If ``amount_external`` is absent, the order is a regular pure Taler order and 113 the existing ``amount`` field is the total amount. If ``amount_external`` is 114 present, the existing ``amount`` field remains the Taler amount. The full 115 order or choice total is the sum of the existing ``amount`` field and all 116 entries in ``amount_external``. 117 118 For example, an order where the customer pays CHF 30 in cash and CHF 20 in 119 Taler keeps ``amount`` as ``CHF:20`` and adds ``amount_external``: 120 121 :: 122 123 { 124 "amount": "CHF:20", 125 "amount_external": [ 126 { 127 "method": "cash", 128 "id": "cash1", 129 "amount": "CHF:30", 130 "cashier_number": "7" 131 } 132 ] 133 } 134 135 This is backwards compatible for old wallets because they continue to see a 136 plain :ts:type:`Amount` in ``amount``. Such wallets may not render the full 137 mixed-payment total, but they can still pay the Taler portion. Updated wallets 138 should render both the full total and the selected Taler amount clearly. 139 140 An order may also have a Taler amount of zero. This allows a POS or merchant 141 portal to use the merchant backend for product tracking, receipts and reports 142 even when the customer paid the whole amount with cash, card or another 143 external method. Such orders are completed using the private collect operation 144 described below. 145 146 Payment Method Names 147 -------------------- 148 149 The initial reserved method name is: 150 151 * ``cash`` for cash accepted by the merchant or cashier 152 153 For now, payment method names are not centrally registered. Integrations may 154 use any stable ASCII identifier. 155 156 The name ``taler`` is reserved and must not be used in ``amount_external``. 157 Taler is represented by the existing ``amount`` field. 158 159 Payment Details 160 --------------- 161 162 For cash payments, additional fields may include the cashier name, cashier 163 number, register identifier or similar local information. For card payments, 164 additional fields may include the terminal identifier, acquirer reference, 165 transaction ID or authorization code. Other systems may add the fields they 166 need for reconciliation or audit. 167 168 The additional fields must be stored only one level below the payment entry. 169 Nested method-specific objects should not be used. This allows the merchant 170 portal to render ``amount_external`` as a simple table without knowing a custom 171 rendering format for each payment method. 172 173 The fields beyond ``method``, ``id`` and ``amount`` are deliberately generic in 174 this design and must remain flat, with no nested method-specific objects. This 175 keeps rendering simple and avoids having to standardize every card terminal, 176 cash register, external voucher or future payment integration up front. The 177 drawback is that these fields are mostly display and reconciliation metadata. 178 If GNU Taler later needs to use method-specific fields for reports, cash 179 tracking, refund UI or automated reconciliation, those fields should be 180 standardized explicitly in a future design. 181 182 Payment Flow 183 ------------ 184 185 The POS or integrating application is responsible for orchestrating mixed 186 payments: 187 188 1. Create or update the order with ``amount_external`` that reflects the 189 intended externally handled payment amount. 190 2. Run all non-Taler payment steps, such as cash handling or card terminal 191 authorization. 192 3. Start the Taler payment as the final step. 193 4. Complete the sale only after the merchant backend confirms the Taler 194 payment. If the Taler amount is zero, use the private collect operation 195 described in `Zero-Taler Order Collection`_. 196 197 The wallet receives the contract terms and computes the payable Taler amount 198 from the existing ``amount`` field. It may use ``amount_external`` to render 199 the full total so that the customer understands why the Taler amount is lower 200 than the order total. 201 202 Because the Taler payment is the last step, the wallet only sees the order and 203 contract terms after the externally handled payments have been settled. 204 205 If the payment split changes before the Taler payment starts, the expected 206 flow is to delete or abandon the old order and create a new order with updated 207 external amounts. Updating an already visible or claimed order to add or remove 208 external payments is out of scope for this design. 209 210 Zero-Taler Order Collection 211 --------------------------- 212 213 Orders whose Taler ``amount`` is zero, for example because the full total was 214 settled through ``amount_external``, are completed through a new private 215 endpoint ``POST [/instances/$INSTANCE]/private/orders/$ORDER_ID/collect``. 216 The merchant backend acts exactly like a customer wallet collecting a free 217 order, executing the same two steps a wallet would: it first claims the 218 order with a backend-generated nonce, and then marks the contract as paid. 219 Payment notifications, webhooks, triggers and inventory updates behave 220 exactly as for a wallet payment of a free order. 221 222 Collection is only possible while the order is unclaimed. If a customer wallet 223 has already claimed the order, the wallet owns the order and must execute the 224 free payment itself; the collect operation fails and reports that the order was 225 claimed by a wallet. This preserves the customer's ability to obtain a wallet 226 receipt: a POS should offer the order QR code first and only collect after the 227 customer declined or a timeout passed. 228 229 Collection is restricted to genuinely free Taler payments: the Taler 230 ``amount`` must be zero and, for v1 contracts, the selected choice must have no 231 ``inputs`` and no ``outputs``. Choices that redeem or issue tokens, such as 232 subscription tokens or donation receipts, require a customer wallet and cannot 233 be collected. 234 235 The optional ``session_id`` of the collect request is stored like the session 236 identifier of a wallet payment, so a POS device can tag and later list the 237 orders it collected. 238 239 Failure Handling 240 ---------------- 241 242 Mixed payments introduce a failure mode where a non-Taler payment has already 243 succeeded but the final Taler payment fails. The merchant backend cannot 244 automatically repair this state because it does not control the external 245 payment method. 246 247 The POS or integrating application must therefore choose one of these recovery 248 paths: 249 250 * modify the order payment split and retry the Taler payment; 251 * cancel the order and refund or void the completed non-Taler payments; 252 * proceed with different payment method, and make Taler part lower or zero. 253 254 Until one of these recovery paths is completed, the order must remain visible 255 to merchant-facing applications in a state that clearly requires manual 256 resolution. Such orders must not be deleted through normal order deletion or 257 cleanup flows. Deletion should only be possible through an explicit force 258 operation that makes it clear that externally handled payments may already have 259 settled. 260 261 Receipt Handling 262 ---------------- 263 264 For normal wallet flows, the customer can access the Taler receipt after the 265 wallet payment. In POS deployments this may not be enough. Some jurisdictions 266 require a printed or otherwise directly provided receipt, and in a mixed 267 payment flow the customer may not receive a Taler receipt if the POS 268 application performs self-pickup or the Taler amount is zero. 269 270 POS applications and other accommodating applications must therefore support a 271 mode where they retrieve the receipt themselves from the merchant backend and 272 provide it to the customer through the locally required channel, such as a 273 printer, terminal display, e-mail or another regulated receipt mechanism. 274 275 Reporting 276 --------- 277 278 The merchant backend should store ``amount_external`` as part of the contract 279 terms and expose it through order status and history APIs. Existing reporting 280 that expects a single amount should continue to show the Taler amount from the 281 existing ``amount`` field. Detailed views should show the externally handled 282 amounts and the full order total. 283 284 The merchant portal should render ``amount_external`` as a table. Common 285 columns are ``method``, ``id`` and ``amount``. Additional columns can be 286 derived from the union of the flat method-specific fields present in the 287 payment entries. The merchant portal should not need method-specific 288 rendering logic to show this information. 289 290 External Refunds 291 ---------------- 292 293 Taler refunds continue to use the existing refund mechanism and are capped at 294 the amount actually paid with Taler. Since contract terms are hashed and signed 295 at claim time, external refunds must not modify ``amount_external``. Instead, 296 the merchant backend records them separately, analogous to how Taler refunds 297 are stored outside the contract terms. 298 299 A new private operation, for example 300 ``POST /private/orders/$ORDER_ID/refund-external``, records an external refund 301 entry: 302 303 .. ts:def:: ExternalRefundInfo 304 305 interface ExternalRefundInfo { 306 // Method by which the funds were returned to the customer, 307 // for example "cash" or "card". May differ from the methods 308 // used to pay the order. Must never be "taler". 309 method: string; 310 311 // Optionally, the "id" of the "amount_external" entry this 312 // refund reverses, when the refund maps to a specific original 313 // payment, for example a card transaction reversal. 314 payment_id?: string; 315 316 // Amount returned to the customer via the external method. 317 amount: Amount; 318 319 // Human-readable refund justification, mirroring Taler refunds. 320 reason: string; 321 322 // Additional flat method-specific fields, same rules as 323 // ExternalPaymentInfo. 324 [field: string]: string | Amount | Integer | boolean | null; 325 } 326 327 The refund channel does not need to match the payment channel: a shop may 328 return the whole amount in cash even when parts of the order were paid by card 329 or with Taler. This is particularly relevant because Taler refunds are 330 constrained by the contract's refund deadline. Once it has passed, the Taler 331 portion can only be returned through an external method. 332 333 The backend therefore validates external refunds against the order total rather 334 than against individual payment entries: the cumulative externally refunded 335 amount must not exceed the full order total minus the amount already refunded 336 through Taler, and must use the same currency as the order. 337 338 Unlike Taler refunds, external refund entries are bookkeeping only. The POS or 339 external payment integration performs the actual return of funds, and no wallet 340 pickup step exists or is needed. Order status APIs expose the recorded entries, 341 for example as ``refunds_external``, and the merchant portal renders them as a 342 generic table, like ``amount_external``. 343 344 For zero-Taler orders collected without a customer wallet, no Taler refund is 345 ever possible because the Taler amount paid is zero. All refunds on such orders 346 are therefore external by construction. 347 348 Vouchers and Tokens 349 ------------------- 350 351 External vouchers can be represented as entries in ``amount_external``. Taler 352 wallet tokens, discounts, gift vouchers and P2P transfer of such vouchers are a 353 separate feature area. In particular, this design does not define how a 354 wallet-held voucher can be partially spent, whether remaining value is 355 re-issued as a new token, or how voucher tokens can be transferred between 356 wallets. 357 358 Test Plan 359 ========= 360 361 * Merchant backend tests for accepting existing plain :ts:type:`Amount` fields 362 unchanged. 363 * Merchant backend tests accepting optional ``amount_external`` in v0 orders 364 and v1 choices. 365 * Merchant backend tests rejecting ``amount_external`` with ``taler`` entries, 366 mixed currencies or invalid method names. 367 * Merchant backend tests preserving ``amount_external`` entries with flat 368 method-specific fields. 369 * Merchant backend tests keeping orders with settled external payments and a 370 failed Taler payment in a manual-resolution status. 371 * Merchant backend tests rejecting normal deletion of such orders unless an 372 explicit force operation is used. 373 * Merchant backend tests collecting an unclaimed zero-Taler v0 order and a 374 zero-amount v1 choice via the private collect endpoint. 375 * Merchant backend tests rejecting collect for orders with nonzero Taler 376 amounts, for v1 choices with token inputs or outputs, and for orders already 377 claimed by a wallet. 378 * Merchant backend tests for collect idempotency. 379 * Merchant backend tests recording external refunds, including refunds through 380 a method different from the original payment methods. 381 * Merchant backend tests rejecting external refunds that would exceed the order 382 total minus the amount refunded through Taler, or that use a different 383 currency or the method name ``taler``. 384 * Merchant backend tests exposing recorded external refunds through order 385 status APIs. 386 * Wallet core tests for paying the existing ``amount`` field and rendering the 387 full total from ``amount_external`` when present. 388 * POS integration tests for a successful cash/card-first and Taler-last flow. 389 * POS integration tests for Taler failure after a non-Taler payment succeeded. 390 391 Definition of Done 392 ================== 393 394 * Merchant backend supports the new additive ``amount_external`` field for 395 order creation, contract terms, order status and history. 396 * Merchant backend keeps all existing ``amount`` fields as plain 397 :ts:type:`Amount` values. 398 * Merchant backend validates that ``amount_external`` has no ``taler`` entries 399 and that all entries use the same currency as ``amount``. 400 * Merchant backend preserves per-method payment details in ``amount_external``. 401 * Merchant backend keeps orders with settled external payments and a failed 402 Taler payment visible for manual resolution. 403 * Merchant backend prevents normal deletion of such orders and requires an 404 explicit force operation to remove them. 405 * Merchant backend provides the private collect operation for zero-Taler 406 orders, restricted to unclaimed orders and genuinely free choices, with 407 payment notifications identical to a wallet payment. 408 * Merchant backend records external refund entries, validates them against the 409 combined refund cap, and exposes them through order status APIs. 410 * Wallet core pays the existing ``amount`` field and does not require 411 ``amount_external`` to complete the Taler payment. 412 * Wallet UIs can display the total and the selected Taler amount clearly. 413 * POS and other accommodating applications support the required orchestration: 414 non-Taler payments first, Taler payment last. 415 * Merchant portal renders ``amount_external`` as a generic table without 416 method-specific renderers. 417 * Merchant portal renders external refunds as a generic table without 418 method-specific renderers. 419 * Documentation explains that external refunds are bookkeeping entries and 420 failure recovery is owned by the integrating application. 421 422 Alternatives 423 ============ 424 425 Change the Amount Field Type 426 ---------------------------- 427 428 The initial proposal changed the existing ``amount`` fields from 429 :ts:type:`Amount` to ``Amount | AmountObject``. This was rejected because it 430 would be a destructive protocol change: every component that currently parses 431 ``amount`` as a string would have to handle a new object shape. Keeping 432 ``amount`` unchanged and adding ``amount_external`` preserves backwards 433 compatibility. 434 435 Store Payment Details in Extra 436 ------------------------------ 437 438 Another initial proposal stored the payment split under ``extra.payments``. 439 This was rejected because ``extra`` is intended for proprietary 440 merchant-specific information. Official protocol fields should be explicit 441 top-level fields, not hidden under the merchant extension area. 442 443 Create Separate Orders 444 ---------------------- 445 446 The POS could create one Taler order only for the Taler amount and track cash 447 or card payments in its own system. This avoids changing the contract amount 448 type, but it loses the single-order receipt and reporting model. It also makes 449 customer-facing order totals harder to verify. As well it looses the backup 450 and synchronisation between device possibilities. 451 452 Let Taler Run Before Other Methods 453 ---------------------------------- 454 455 Running Taler before cash or card would make the Taler part successful while 456 the external payment can still fail. That leaves the merchant with a paid 457 Taler contract for an order that may not be otherwise settled. Requiring Taler 458 to be last gives the POS a clearer recovery path because external payments can 459 still be voided, refunded or used to recompute the remaining Taler amount. As 460 well it can create problems when refund deadline for Taler option was set as 0 461 and other method of payment failed. 462 463 Use Templates or Mutable Payment Sessions 464 ----------------------------------------- 465 466 Templates or a new payment-session model could allow the customer to inspect an 467 order before choosing how to split the payment, and could support adding, 468 modifying or deleting payment parts before finalization. This would be more 469 flexible than DD96, but it would require a larger design across merchant 470 backend, POS apps and wallets. DD96 is limited to the current ``/orders`` flow 471 where external payments are settled before the Taler order is created. 472 473 Split One Order Across Multiple Taler Wallets 474 --------------------------------------------- 475 476 Multiple customers paying one order with multiple Taler wallets is out of 477 scope. The current workaround is to split the sale into multiple orders or 478 sub-orders, for example based on the products consumed by each customer. 479 480 Drawbacks 481 ========= 482 483 * POS implementations must handle partial failure and external refunds 484 carefully. 485 * Old wallets may only render the Taler amount and not the full mixed-payment 486 total until they learn the new ``amount_external`` field. 487 * The customer cannot inspect the order or contract terms in the wallet before 488 externally handled payments, such as cash or card payments, have been 489 settled. 490 * Reporting and refund UIs must distinguish total order amount from Taler-paid 491 amount. They must also distinguish Taler refunds, which require wallet 492 pickup, from external refunds, which are bookkeeping entries only. 493 494 Open Questions 495 ============== 496 497 * Should money pots store full totals, per-method totals, or both? Should 498 merchant backend auto create new pots per each new payment method found in 499 order? 500 501 Discussion / Q&A 502 ================ 503 504 * Feedback from Florian Dold: ``extra`` must remain reserved for proprietary 505 merchant fields and must not carry official protocol data. Protocol changes 506 should be additive, so the existing ``amount`` field should not change type. 507 The design was updated accordingly: the existing ``amount`` remains the 508 Taler amount, while a new additive ``amount_external`` field carries the 509 externally handled amounts and reconciliation metadata.