taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

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.