taler-docs

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

096-partial-payments.rst (22831B)


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