20.96. DD 96: Partial Payments#
20.96.1. Summary#
This document proposes support for orders where only part of the total amount is paid with Taler and the remaining amount is paid with other payment methods, such as cash, card, vouchers or others.
The protocol change must be additive. The existing Amount field of
an order or choice continues to represent the amount paid with Taler. A new
optional amount_external field carries externally handled payment amounts
and the reconciliation metadata needed by POS applications and merchant
back-office users.
20.96.2. Motivation#
In person purchases might involve mixed payments. A customer may pay part of an order in cash and the rest with Taler, or a cashier may need to combine Taler with a card terminal, voucher system or other local payment method. Today, the merchant backend and wallet assume that the amount in the contract is the amount the wallet pays with Taler. This model cannot represent a single receipt and order that is settled by multiple methods.
The goal is not to make the merchant backend process card or cash payments. The goal is to let the merchant backend, wallet core and POS applications agree on the order total, the Taler portion and the non-Taler portions that must have already been completed outside of Taler.
20.96.3. Requirements#
Orders and choices must be able to express mixed payment amounts.
The existing plain Amount form must remain valid for backwards compatibility.
The type and meaning of existing
amountfields must not change.The existing
amountfield remains the amount paid with Taler.The optional external payment field must not include Taler entries.
The total order amount is the sum of the existing
amountfield and all entries inamount_external.The wallet must only pay the existing
amountfield.The POS or other accommodating application must execute all non-Taler payments before the Taler payment.
The Taler payment is always the last payment step.
If the Taler payment fails after other payments succeeded, the POS must either modify the order and retry the Taler step or refund the already completed non-Taler payments.
Orders with settled external payments and a failed Taler payment must remain visible in a manual-resolution status. They must not be deleted by normal order cleanup or by accident.
The merchant backend must preserve enough information for receipts, reporting and order inspection to show how the total was split.
Per-method payment information must be stored in a flat structure that the merchant portal can render as a generic table.
The design must not require the wallet to validate that cash, card or other non-Taler payments actually happened.
20.96.4. Proposed Solution#
20.96.4.1. Additive Payment Field#
Keep all existing Amount fields unchanged. In particular, OrderV0.``amount``, OrderChoice.``amount`, ContractTermsV0.``amount`` and ContractChoice.``amount`` remain plain Amount values and represent the amount the wallet pays with Taler.
Add a new optional amount_external field next to these existing amount
fields:
interface ExternalPaymentInfo {
// External payment method, for example "cash" or "card".
// Must never be "taler".
method: string;
// Identifier of the payment action within the order.
// Examples: "cash1", "sumup1", "sumup2".
id: string;
// Amount covered by this payment action.
// Must always be present
amount: Amount;
// Additional method-specific fields. These fields must be
// stored only at this level.
[field: string]: string | Amount | Integer | boolean | null;
}
interface PartialPaymentFields {
// Payments handled outside of Taler.
amount_external?: ExternalPaymentInfo[];
}
The proposed extension applies to the amount-bearing order and contract objects:
type OrderV0 = ExistingOrderV0 & PartialPaymentFields;
type OrderChoice = ExistingOrderChoice & PartialPaymentFields;
type ContractTermsV0 = ExistingContractTermsV0 & PartialPaymentFields;
type ContractChoice = ExistingContractChoice & PartialPaymentFields;
If amount_external is absent, the order is a regular pure Taler order and
the existing amount field is the total amount. If amount_external is
present, the existing amount field remains the Taler amount. The full
order or choice total is the sum of the existing amount field and all
entries in amount_external.
For example, an order where the customer pays CHF 30 in cash and CHF 20 in
Taler keeps amount as CHF:20 and adds amount_external:
{
"amount": "CHF:20",
"amount_external": [
{
"method": "cash",
"id": "cash1",
"amount": "CHF:30",
"cashier_number": "7"
}
]
}
This is backwards compatible for old wallets because they continue to see a
plain Amount in amount. Such wallets may not render the full
mixed-payment total, but they can still pay the Taler portion. Updated wallets
should render both the full total and the selected Taler amount clearly.
An order may also have a Taler amount of zero. This allows a POS or merchant portal to use the merchant backend for product tracking, receipts and reports even when the customer paid the whole amount with cash, card or another external method. Such orders are completed using the private collect operation described below.
20.96.4.2. Payment Method Names#
The initial reserved method name is:
cashfor cash accepted by the merchant or cashier
For now, payment method names are not centrally registered. Integrations may use any stable ASCII identifier.
The name taler is reserved and must not be used in amount_external.
Taler is represented by the existing amount field.
20.96.4.3. Payment Details#
For cash payments, additional fields may include the cashier name, cashier number, register identifier or similar local information. For card payments, additional fields may include the terminal identifier, acquirer reference, transaction ID or authorization code. Other systems may add the fields they need for reconciliation or audit.
The additional fields must be stored only one level below the payment entry.
Nested method-specific objects should not be used. This allows the merchant
portal to render amount_external as a simple table without knowing a custom
rendering format for each payment method.
The fields beyond method, id and amount are deliberately generic in
this design and must remain flat, with no nested method-specific objects. This
keeps rendering simple and avoids having to standardize every card terminal,
cash register, external voucher or future payment integration up front. The
drawback is that these fields are mostly display and reconciliation metadata.
If GNU Taler later needs to use method-specific fields for reports, cash
tracking, refund UI or automated reconciliation, those fields should be
standardized explicitly in a future design.
20.96.4.4. Payment Flow#
The POS or integrating application is responsible for orchestrating mixed payments:
Create or update the order with
amount_externalthat reflects the intended externally handled payment amount.Run all non-Taler payment steps, such as cash handling or card terminal authorization.
Start the Taler payment as the final step.
Complete the sale only after the merchant backend confirms the Taler payment. If the Taler amount is zero, use the private collect operation described in Zero-Taler Order Collection.
The wallet receives the contract terms and computes the payable Taler amount
from the existing amount field. It may use amount_external to render
the full total so that the customer understands why the Taler amount is lower
than the order total.
Because the Taler payment is the last step, the wallet only sees the order and contract terms after the externally handled payments have been settled.
If the payment split changes before the Taler payment starts, the expected flow is to delete or abandon the old order and create a new order with updated external amounts. Updating an already visible or claimed order to add or remove external payments is out of scope for this design.
20.96.4.5. Zero-Taler Order Collection#
Orders whose Taler amount is zero, for example because the full total was
settled through amount_external, are completed through a new private
endpoint POST [/instances/$INSTANCE]/private/orders/$ORDER_ID/collect.
The merchant backend acts exactly like a customer wallet collecting a free
order, executing the same two steps a wallet would: it first claims the
order with a backend-generated nonce, and then marks the contract as paid.
Payment notifications, webhooks, triggers and inventory updates behave
exactly as for a wallet payment of a free order.
Collection is only possible while the order is unclaimed. If a customer wallet has already claimed the order, the wallet owns the order and must execute the free payment itself; the collect operation fails and reports that the order was claimed by a wallet. This preserves the customer’s ability to obtain a wallet receipt: a POS should offer the order QR code first and only collect after the customer declined or a timeout passed.
Collection is restricted to genuinely free Taler payments: the Taler
amount must be zero and, for v1 contracts, the selected choice must have no
inputs and no outputs. Choices that redeem or issue tokens, such as
subscription tokens or donation receipts, require a customer wallet and cannot
be collected.
The optional session_id of the collect request is stored like the session
identifier of a wallet payment, so a POS device can tag and later list the
orders it collected.
20.96.4.6. Failure Handling#
Mixed payments introduce a failure mode where a non-Taler payment has already succeeded but the final Taler payment fails. The merchant backend cannot automatically repair this state because it does not control the external payment method.
The POS or integrating application must therefore choose one of these recovery paths:
modify the order payment split and retry the Taler payment;
cancel the order and refund or void the completed non-Taler payments;
proceed with different payment method, and make Taler part lower or zero.
Until one of these recovery paths is completed, the order must remain visible to merchant-facing applications in a state that clearly requires manual resolution. Such orders must not be deleted through normal order deletion or cleanup flows. Deletion should only be possible through an explicit force operation that makes it clear that externally handled payments may already have settled.
20.96.4.7. Receipt Handling#
For normal wallet flows, the customer can access the Taler receipt after the wallet payment. In POS deployments this may not be enough. Some jurisdictions require a printed or otherwise directly provided receipt, and in a mixed payment flow the customer may not receive a Taler receipt if the POS application performs self-pickup or the Taler amount is zero.
POS applications and other accommodating applications must therefore support a mode where they retrieve the receipt themselves from the merchant backend and provide it to the customer through the locally required channel, such as a printer, terminal display, e-mail or another regulated receipt mechanism.
20.96.4.8. Reporting#
The merchant backend should store amount_external as part of the contract
terms and expose it through order status and history APIs. Existing reporting
that expects a single amount should continue to show the Taler amount from the
existing amount field. Detailed views should show the externally handled
amounts and the full order total.
The merchant portal should render amount_external as a table. Common
columns are method, id and amount. Additional columns can be
derived from the union of the flat method-specific fields present in the
payment entries. The merchant portal should not need method-specific
rendering logic to show this information.
20.96.4.9. External Refunds#
Taler refunds continue to use the existing refund mechanism and are capped at
the amount actually paid with Taler. Since contract terms are hashed and signed
at claim time, external refunds must not modify amount_external. Instead,
the merchant backend records them separately, analogous to how Taler refunds
are stored outside the contract terms.
A new private operation, for example
POST /private/orders/$ORDER_ID/refund-external, records an external refund
entry:
interface ExternalRefundInfo {
// Method by which the funds were returned to the customer,
// for example "cash" or "card". May differ from the methods
// used to pay the order. Must never be "taler".
method: string;
// Optionally, the "id" of the "amount_external" entry this
// refund reverses, when the refund maps to a specific original
// payment, for example a card transaction reversal.
payment_id?: string;
// Amount returned to the customer via the external method.
amount: Amount;
// Human-readable refund justification, mirroring Taler refunds.
reason: string;
// Additional flat method-specific fields, same rules as
// ExternalPaymentInfo.
[field: string]: string | Amount | Integer | boolean | null;
}
The refund channel does not need to match the payment channel: a shop may return the whole amount in cash even when parts of the order were paid by card or with Taler. This is particularly relevant because Taler refunds are constrained by the contract’s refund deadline. Once it has passed, the Taler portion can only be returned through an external method.
The backend therefore validates external refunds against the order total rather than against individual payment entries: the cumulative externally refunded amount must not exceed the full order total minus the amount already refunded through Taler, and must use the same currency as the order.
Unlike Taler refunds, external refund entries are bookkeeping only. The POS or
external payment integration performs the actual return of funds, and no wallet
pickup step exists or is needed. Order status APIs expose the recorded entries,
for example as refunds_external, and the merchant portal renders them as a
generic table, like amount_external.
For zero-Taler orders collected without a customer wallet, no Taler refund is ever possible because the Taler amount paid is zero. All refunds on such orders are therefore external by construction.
20.96.4.10. Vouchers and Tokens#
External vouchers can be represented as entries in amount_external. Taler
wallet tokens, discounts, gift vouchers and P2P transfer of such vouchers are a
separate feature area. In particular, this design does not define how a
wallet-held voucher can be partially spent, whether remaining value is
re-issued as a new token, or how voucher tokens can be transferred between
wallets.
20.96.5. Test Plan#
Merchant backend tests for accepting existing plain Amount fields unchanged.
Merchant backend tests accepting optional
amount_externalin v0 orders and v1 choices.Merchant backend tests rejecting
amount_externalwithtalerentries, mixed currencies or invalid method names.Merchant backend tests preserving
amount_externalentries with flat method-specific fields.Merchant backend tests keeping orders with settled external payments and a failed Taler payment in a manual-resolution status.
Merchant backend tests rejecting normal deletion of such orders unless an explicit force operation is used.
Merchant backend tests collecting an unclaimed zero-Taler v0 order and a zero-amount v1 choice via the private collect endpoint.
Merchant backend tests rejecting collect for orders with nonzero Taler amounts, for v1 choices with token inputs or outputs, and for orders already claimed by a wallet.
Merchant backend tests for collect idempotency.
Merchant backend tests recording external refunds, including refunds through a method different from the original payment methods.
Merchant backend tests rejecting external refunds that would exceed the order total minus the amount refunded through Taler, or that use a different currency or the method name
taler.Merchant backend tests exposing recorded external refunds through order status APIs.
Wallet core tests for paying the existing
amountfield and rendering the full total fromamount_externalwhen present.POS integration tests for a successful cash/card-first and Taler-last flow.
POS integration tests for Taler failure after a non-Taler payment succeeded.
20.96.6. Definition of Done#
Merchant backend supports the new additive
amount_externalfield for order creation, contract terms, order status and history.Merchant backend keeps all existing
amountfields as plain Amount values.Merchant backend validates that
amount_externalhas notalerentries and that all entries use the same currency asamount.Merchant backend preserves per-method payment details in
amount_external.Merchant backend keeps orders with settled external payments and a failed Taler payment visible for manual resolution.
Merchant backend prevents normal deletion of such orders and requires an explicit force operation to remove them.
Merchant backend provides the private collect operation for zero-Taler orders, restricted to unclaimed orders and genuinely free choices, with payment notifications identical to a wallet payment.
Merchant backend records external refund entries, validates them against the combined refund cap, and exposes them through order status APIs.
Wallet core pays the existing
amountfield and does not requireamount_externalto complete the Taler payment.Wallet UIs can display the total and the selected Taler amount clearly.
POS and other accommodating applications support the required orchestration: non-Taler payments first, Taler payment last.
Merchant portal renders
amount_externalas a generic table without method-specific renderers.Merchant portal renders external refunds as a generic table without method-specific renderers.
Documentation explains that external refunds are bookkeeping entries and failure recovery is owned by the integrating application.
20.96.7. Alternatives#
20.96.7.1. Change the Amount Field Type#
The initial proposal changed the existing amount fields from
Amount to Amount | AmountObject. This was rejected because it
would be a destructive protocol change: every component that currently parses
amount as a string would have to handle a new object shape. Keeping
amount unchanged and adding amount_external preserves backwards
compatibility.
20.96.7.2. Store Payment Details in Extra#
Another initial proposal stored the payment split under extra.payments.
This was rejected because extra is intended for proprietary
merchant-specific information. Official protocol fields should be explicit
top-level fields, not hidden under the merchant extension area.
20.96.7.3. Create Separate Orders#
The POS could create one Taler order only for the Taler amount and track cash or card payments in its own system. This avoids changing the contract amount type, but it loses the single-order receipt and reporting model. It also makes customer-facing order totals harder to verify. As well it looses the backup and synchronisation between device possibilities.
20.96.7.4. Let Taler Run Before Other Methods#
Running Taler before cash or card would make the Taler part successful while the external payment can still fail. That leaves the merchant with a paid Taler contract for an order that may not be otherwise settled. Requiring Taler to be last gives the POS a clearer recovery path because external payments can still be voided, refunded or used to recompute the remaining Taler amount. As well it can create problems when refund deadline for Taler option was set as 0 and other method of payment failed.
20.96.7.5. Use Templates or Mutable Payment Sessions#
Templates or a new payment-session model could allow the customer to inspect an
order before choosing how to split the payment, and could support adding,
modifying or deleting payment parts before finalization. This would be more
flexible than DD96, but it would require a larger design across merchant
backend, POS apps and wallets. DD96 is limited to the current /orders flow
where external payments are settled before the Taler order is created.
20.96.7.6. Split One Order Across Multiple Taler Wallets#
Multiple customers paying one order with multiple Taler wallets is out of scope. The current workaround is to split the sale into multiple orders or sub-orders, for example based on the products consumed by each customer.
20.96.8. Drawbacks#
POS implementations must handle partial failure and external refunds carefully.
Old wallets may only render the Taler amount and not the full mixed-payment total until they learn the new
amount_externalfield.The customer cannot inspect the order or contract terms in the wallet before externally handled payments, such as cash or card payments, have been settled.
Reporting and refund UIs must distinguish total order amount from Taler-paid amount. They must also distinguish Taler refunds, which require wallet pickup, from external refunds, which are bookkeeping entries only.
20.96.9. Open Questions#
Should money pots store full totals, per-method totals, or both? Should merchant backend auto create new pots per each new payment method found in order?
20.96.10. Discussion / Q&A#
Feedback from Florian Dold:
extramust remain reserved for proprietary merchant fields and must not carry official protocol data. Protocol changes should be additive, so the existingamountfield should not change type. The design was updated accordingly: the existingamountremains the Taler amount, while a new additiveamount_externalfield carries the externally handled amounts and reconciliation metadata.