taler-docs

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

commit 65d12a8cff99956baf3164e225a50e5d01ba24bf
parent a85665de799643db194894b2b7a28eafc1493b00
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sun, 12 Jul 2026 14:13:11 +0200

[DD96] refining the design document, with new details  (collect, refund, and general updates from team discussion)

Diffstat:
Mdesign-documents/096-partial-payments.rst | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 184 insertions(+), 19 deletions(-)

diff --git a/design-documents/096-partial-payments.rst b/design-documents/096-partial-payments.rst @@ -47,6 +47,9 @@ Requirements * 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 @@ -134,6 +137,12 @@ plain :ts:type:`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. + Payment Method Names -------------------- @@ -141,8 +150,8 @@ The initial reserved method name is: * ``cash`` for cash accepted by the merchant or cashier -Other names are allowed for integrations, but they should be stable ASCII -identifiers. +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. @@ -161,6 +170,15 @@ 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. + Payment Flow ------------ @@ -173,13 +191,51 @@ payments: authorization. 3. Start the Taler payment as the final step. 4. Complete the sale only after the merchant backend confirms the Taler - payment, unless the ``taler`` amount is zero. + 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. + +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. + Failure Handling ---------------- @@ -195,6 +251,13 @@ paths: * 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. + Receipt Handling ---------------- @@ -224,14 +287,73 @@ 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. -Refunds -------- +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: + +.. ts:def:: ExternalRefundInfo + + 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; -Taler refunds can only refund the amount actually paid with Taler. Refunds for -cash, card or other methods remain the responsibility of the POS or external -payment integration. When an order has a split amount, APIs and UIs should -avoid wording that implies the merchant backend can refund the whole order -through Taler. + // 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. + +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. Test Plan ========= @@ -244,6 +366,23 @@ Test Plan mixed currencies or invalid method names. * Merchant backend tests preserving ``amount_external`` entries 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 ``amount`` field and rendering the full total from ``amount_external`` when present. * POS integration tests for a successful cash/card-first and Taler-last flow. @@ -259,6 +398,15 @@ Definition of Done * Merchant backend validates that ``amount_external`` has no ``taler`` entries and that all entries use the same currency as ``amount``. * 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 ``amount`` field and does not require ``amount_external`` to complete the Taler payment. * Wallet UIs can display the total and the selected Taler amount clearly. @@ -266,8 +414,10 @@ Definition of Done non-Taler payments first, Taler payment last. * Merchant portal renders ``amount_external`` as a generic table without method-specific renderers. -* Documentation explains that non-Taler refunds and failure recovery are owned - by the integrating application. +* 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. Alternatives ============ @@ -310,6 +460,23 @@ 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. +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. + +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. + Drawbacks ========= @@ -317,8 +484,12 @@ Drawbacks carefully. * Old wallets may only render the Taler amount and not the full mixed-payment total until they learn the new ``amount_external`` field. +* 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. + amount. They must also distinguish Taler refunds, which require wallet + pickup, from external refunds, which are bookkeeping entries only. Open Questions ============== @@ -326,12 +497,6 @@ 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? -* Should payment method names be centrally registered, or is validation of - stable ASCII identifiers sufficient? -* Should there be a dedicated status for orders where non-Taler payments - succeeded but the final Taler payment failed? or we can just delete them? -* Should the merchant backend expose explicit receipt self-pickup endpoints for - POS devices, or are existing private order status APIs sufficient? Discussion / Q&A ================