taler-docs

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

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.