taler-docs

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

096-partial-payments.rst (32347B)


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