taler-docs

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

096-partial-payments.rst (14164B)


      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 * The merchant backend must preserve enough information for receipts,
     51   reporting and order inspection to show how the total was split.
     52 * Per-method payment information must be stored in a flat structure that the
     53   merchant portal can render as a generic table.
     54 * The design must not require the wallet to validate that cash, card or other
     55   non-Taler payments actually happened.
     56 
     57 Proposed Solution
     58 =================
     59 
     60 Additive Payment Field
     61 ----------------------
     62 
     63 Keep all existing :ts:type:`Amount` fields unchanged. In particular,
     64 :ts:type:`OrderV0`.``amount``, :ts:type:`OrderChoice`.``amount`,
     65 :ts:type:`ContractTermsV0`.``amount`` and
     66 :ts:type:`ContractChoice`.``amount`` remain plain :ts:type:`Amount` values and
     67 represent the amount the wallet pays with Taler.
     68 
     69 Add a new optional ``amount_external`` field next to these existing ``amount``
     70 fields:
     71 
     72 .. ts:def:: ExternalPaymentInfo
     73 
     74   interface ExternalPaymentInfo {
     75     // External payment method, for example "cash" or "card".
     76     // Must never be "taler".
     77     method: string;
     78 
     79     // Identifier of the payment action within the order.
     80     // Examples: "cash1", "sumup1", "sumup2".
     81     id: string;
     82 
     83     // Amount covered by this payment action.
     84     // Must always be present
     85     amount: Amount;
     86 
     87     // Additional method-specific fields. These fields must be
     88     // stored only at this level.
     89     [field: string]: string | Amount | Integer | boolean | null;
     90   }
     91 
     92 .. ts:def:: PartialPaymentFields
     93 
     94   interface PartialPaymentFields {
     95     // Payments handled outside of Taler.
     96     amount_external?: ExternalPaymentInfo[];
     97   }
     98 
     99 The proposed extension applies to the amount-bearing order and contract
    100 objects:
    101 
    102 ::
    103 
    104   type OrderV0 = ExistingOrderV0 & PartialPaymentFields;
    105   type OrderChoice = ExistingOrderChoice & PartialPaymentFields;
    106   type ContractTermsV0 = ExistingContractTermsV0 & PartialPaymentFields;
    107   type ContractChoice = ExistingContractChoice & PartialPaymentFields;
    108 
    109 If ``amount_external`` is absent, the order is a regular pure Taler order and
    110 the existing ``amount`` field is the total amount. If ``amount_external`` is
    111 present, the existing ``amount`` field remains the Taler amount. The full
    112 order or choice total is the sum of the existing ``amount`` field and all
    113 entries in ``amount_external``.
    114 
    115 For example, an order where the customer pays CHF 30 in cash and CHF 20 in
    116 Taler keeps ``amount`` as ``CHF:20`` and adds ``amount_external``:
    117 
    118 ::
    119 
    120   {
    121     "amount": "CHF:20",
    122     "amount_external": [
    123       {
    124         "method": "cash",
    125         "id": "cash1",
    126         "amount": "CHF:30",
    127         "cashier_number": "7"
    128       }
    129     ]
    130   }
    131 
    132 This is backwards compatible for old wallets because they continue to see a
    133 plain :ts:type:`Amount` in ``amount``. Such wallets may not render the full
    134 mixed-payment total, but they can still pay the Taler portion. Updated wallets
    135 should render both the full total and the selected Taler amount clearly.
    136 
    137 Payment Method Names
    138 --------------------
    139 
    140 The initial reserved method name is:
    141 
    142 * ``cash`` for cash accepted by the merchant or cashier
    143 
    144 Other names are allowed for integrations, but they should be stable ASCII
    145 identifiers.
    146 
    147 The name ``taler`` is reserved and must not be used in ``amount_external``.
    148 Taler is represented by the existing ``amount`` field.
    149 
    150 Payment Details
    151 ---------------
    152 
    153 For cash payments, additional fields may include the cashier name, cashier
    154 number, register identifier or similar local information. For card payments,
    155 additional fields may include the terminal identifier, acquirer reference,
    156 transaction ID or authorization code. Other systems may add the fields they
    157 need for reconciliation or audit.
    158 
    159 The additional fields must be stored only one level below the payment entry.
    160 Nested method-specific objects should not be used. This allows the merchant
    161 portal to render ``amount_external`` as a simple table without knowing a custom
    162 rendering format for each payment method.
    163 
    164 Payment Flow
    165 ------------
    166 
    167 The POS or integrating application is responsible for orchestrating mixed
    168 payments:
    169 
    170 1. Create or update the order with ``amount_external`` that reflects the
    171    intended externally handled payment amount.
    172 2. Run all non-Taler payment steps, such as cash handling or card terminal
    173    authorization.
    174 3. Start the Taler payment as the final step.
    175 4. Complete the sale only after the merchant backend confirms the Taler
    176    payment, unless the ``taler`` amount is zero.
    177 
    178 The wallet receives the contract terms and computes the payable Taler amount
    179 from the existing ``amount`` field. It may use ``amount_external`` to render
    180 the full total so that the customer understands why the Taler amount is lower
    181 than the order total.
    182 
    183 Failure Handling
    184 ----------------
    185 
    186 Mixed payments introduce a failure mode where a non-Taler payment has already
    187 succeeded but the final Taler payment fails. The merchant backend cannot
    188 automatically repair this state because it does not control the external
    189 payment method.
    190 
    191 The POS or integrating application must therefore choose one of these recovery
    192 paths:
    193 
    194 * modify the order payment split and retry the Taler payment;
    195 * cancel the order and refund or void the completed non-Taler payments;
    196 * proceed with different payment method, and make Taler part lower or zero.
    197 
    198 Receipt Handling
    199 ----------------
    200 
    201 For normal wallet flows, the customer can access the Taler receipt after the
    202 wallet payment. In POS deployments this may not be enough. Some jurisdictions
    203 require a printed or otherwise directly provided receipt, and in a mixed
    204 payment flow the customer may not receive a Taler receipt if the POS
    205 application performs self-pickup or the Taler amount is zero.
    206 
    207 POS applications and other accommodating applications must therefore support a
    208 mode where they retrieve the receipt themselves from the merchant backend and
    209 provide it to the customer through the locally required channel, such as a
    210 printer, terminal display, e-mail or another regulated receipt mechanism.
    211 
    212 Reporting
    213 ---------
    214 
    215 The merchant backend should store ``amount_external`` as part of the contract
    216 terms and expose it through order status and history APIs. Existing reporting
    217 that expects a single amount should continue to show the Taler amount from the
    218 existing ``amount`` field. Detailed views should show the externally handled
    219 amounts and the full order total.
    220 
    221 The merchant portal should render ``amount_external`` as a table. Common
    222 columns are ``method``, ``id`` and ``amount``. Additional columns can be
    223 derived from the union of the flat method-specific fields present in the
    224 payment entries. The merchant portal should not need method-specific
    225 rendering logic to show this information.
    226 
    227 Refunds
    228 -------
    229 
    230 Taler refunds can only refund the amount actually paid with Taler. Refunds for
    231 cash, card or other methods remain the responsibility of the POS or external
    232 payment integration. When an order has a split amount, APIs and UIs should
    233 avoid wording that implies the merchant backend can refund the whole order
    234 through Taler.
    235 
    236 Test Plan
    237 =========
    238 
    239 * Merchant backend tests for accepting existing plain :ts:type:`Amount` fields
    240   unchanged.
    241 * Merchant backend tests accepting optional ``amount_external`` in v0 orders
    242   and v1 choices.
    243 * Merchant backend tests rejecting ``amount_external`` with ``taler`` entries,
    244   mixed currencies or invalid method names.
    245 * Merchant backend tests preserving ``amount_external`` entries with flat
    246   method-specific fields.
    247 * Wallet core tests for paying the existing ``amount`` field and rendering the
    248   full total from ``amount_external`` when present.
    249 * POS integration tests for a successful cash/card-first and Taler-last flow.
    250 * POS integration tests for Taler failure after a non-Taler payment succeeded.
    251 
    252 Definition of Done
    253 ==================
    254 
    255 * Merchant backend supports the new additive ``amount_external`` field for
    256   order creation, contract terms, order status and history.
    257 * Merchant backend keeps all existing ``amount`` fields as plain
    258   :ts:type:`Amount` values.
    259 * Merchant backend validates that ``amount_external`` has no ``taler`` entries
    260   and that all entries use the same currency as ``amount``.
    261 * Merchant backend preserves per-method payment details in ``amount_external``.
    262 * Wallet core pays the existing ``amount`` field and does not require
    263   ``amount_external`` to complete the Taler payment.
    264 * Wallet UIs can display the total and the selected Taler amount clearly.
    265 * POS and other accommodating applications support the required orchestration:
    266   non-Taler payments first, Taler payment last.
    267 * Merchant portal renders ``amount_external`` as a generic table without
    268   method-specific renderers.
    269 * Documentation explains that non-Taler refunds and failure recovery are owned
    270   by the integrating application.
    271 
    272 Alternatives
    273 ============
    274 
    275 Change the Amount Field Type
    276 ----------------------------
    277 
    278 The initial proposal changed the existing ``amount`` fields from
    279 :ts:type:`Amount` to ``Amount | AmountObject``. This was rejected because it
    280 would be a destructive protocol change: every component that currently parses
    281 ``amount`` as a string would have to handle a new object shape. Keeping
    282 ``amount`` unchanged and adding ``amount_external`` preserves backwards
    283 compatibility.
    284 
    285 Store Payment Details in Extra
    286 ------------------------------
    287 
    288 Another initial proposal stored the payment split under ``extra.payments``.
    289 This was rejected because ``extra`` is intended for proprietary
    290 merchant-specific information. Official protocol fields should be explicit
    291 top-level fields, not hidden under the merchant extension area.
    292 
    293 Create Separate Orders
    294 ----------------------
    295 
    296 The POS could create one Taler order only for the Taler amount and track cash
    297 or card payments in its own system. This avoids changing the contract amount
    298 type, but it loses the single-order receipt and reporting model. It also makes
    299 customer-facing order totals harder to verify. As well it looses the backup
    300 and synchronisation between device possibilities.
    301 
    302 Let Taler Run Before Other Methods
    303 ----------------------------------
    304 
    305 Running Taler before cash or card would make the Taler part successful while
    306 the external payment can still fail. That leaves the merchant with a paid
    307 Taler contract for an order that may not be otherwise settled. Requiring Taler
    308 to be last gives the POS a clearer recovery path because external payments can
    309 still be voided, refunded or used to recompute the remaining Taler amount. As
    310 well it can create problems when refund deadline for Taler option was set as 0
    311 and other method of payment failed.
    312 
    313 Drawbacks
    314 =========
    315 
    316 * POS implementations must handle partial failure and external refunds
    317   carefully.
    318 * Old wallets may only render the Taler amount and not the full mixed-payment
    319   total until they learn the new ``amount_external`` field.
    320 * Reporting and refund UIs must distinguish total order amount from Taler-paid
    321   amount.
    322 
    323 Open Questions
    324 ==============
    325 
    326 * Should money pots store full totals, per-method totals, or both? Should
    327   merchant backend auto create new pots per each new payment method found in
    328   order?
    329 * Should payment method names be centrally registered, or is validation of
    330   stable ASCII identifiers sufficient?
    331 * Should there be a dedicated status for orders where non-Taler payments
    332   succeeded but the final Taler payment failed? or we can just delete them?
    333 * Should the merchant backend expose explicit receipt self-pickup endpoints for
    334   POS devices, or are existing private order status APIs sufficient?
    335 
    336 Discussion / Q&A
    337 ================
    338 
    339 * Feedback from Florian Dold: ``extra`` must remain reserved for proprietary
    340   merchant fields and must not carry official protocol data. Protocol changes
    341   should be additive, so the existing ``amount`` field should not change type.
    342   The design was updated accordingly: the existing ``amount`` remains the
    343   Taler amount, while a new additive ``amount_external`` field carries the
    344   externally handled amounts and reconciliation metadata.