taler-docs

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

commit 5715bf8d996e3ab36f9290d7f0f48e4b78c852ef
parent 6177e079abc5f73466bc2ac7d4151640d9c981ea
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Thu, 30 Jul 2026 08:48:11 +0200

updates to mixed payments

Diffstat:
Mcore/merchant/post-orders-ORDER_ID-pay.rst | 20++++++++++++++++++++
Mcore/merchant/post-private-orders-ORDER_ID-collect.rst | 9+++++++--
Mcore/merchant/post-private-orders-ORDER_ID-refund-external.rst | 4++--
Mdesign-documents/096-partial-payments.rst | 40+++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 11 deletions(-)

diff --git a/core/merchant/post-orders-ORDER_ID-pay.rst b/core/merchant/post-orders-ORDER_ID-pay.rst @@ -102,6 +102,12 @@ * ``MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS``: Exchange reported insufficient funds for one of the coins. + * ``MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISMATCH``: The order was + already paid, but completing a different choice than the one given in + this request, so the request does not describe the payment that was + made. The response is an `AlreadyPaidChoiceResponse` and contains the + ``choice_index`` the order was actually paid with. + Since protocol **vMixedPayments**. * ``MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID``: The order was already fully paid by another wallet. The response includes refund signatures for the coins. @@ -188,6 +194,20 @@ **Details:** + .. ts:def:: AlreadyPaidChoiceResponse + + interface AlreadyPaidChoiceResponse { + // Error code identifying the conflict, always + // ``MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISMATCH``. + code: number; + + // Index of the choice within the ``choices`` array of the + // contract terms that the order was actually paid with. A client + // that wants to refer to the payment that was made has to use + // this choice instead of the one it asked for. + choice_index: Integer; + } + .. ts:def:: PaymentResponse interface PaymentResponse { diff --git a/core/merchant/post-private-orders-ORDER_ID-collect.rst b/core/merchant/post-private-orders-ORDER_ID-collect.rst @@ -56,10 +56,15 @@ The order cannot be collected by the backend. Returned with ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_ALREADY_CLAIMED`` - if the order was already claimed by a customer wallet, or with + if the order was already claimed by a customer wallet, with ``TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_COLLECT_NOT_FREE`` if the Taler amount is not zero or the selected choice redeems or - issues tokens. + issues tokens, or with + ``TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISMATCH`` + if the order was already collected, but completing a different + choice than the one given in this request. As for a payment, the + response then is an `AlreadyPaidChoiceResponse` naming the choice + the order was actually paid with. :http:statuscode:`500 Internal Server Error`: The server experienced an internal failure. Returned with ``TALER_EC_GENERIC_DB_COMMIT_FAILED`` or diff --git a/core/merchant/post-private-orders-ORDER_ID-refund-external.rst b/core/merchant/post-private-orders-ORDER_ID-refund-external.rst @@ -110,7 +110,7 @@ interface ExternalRefundResponse { // Identifier of the recorded external refund within the order, - // either the one given in the request or the one assigned by the - // backend. Can be used to reference the refund later. + // as given in the request. Can be used to reference the refund + // later. refund_id: string; } diff --git a/design-documents/096-partial-payments.rst b/design-documents/096-partial-payments.rst @@ -298,6 +298,20 @@ determines the amount, the tokens consumed and issued, and the fulfillment, so it is a decision the client must make explicitly rather than one the backend guesses. +For the same reason the choice is part of what makes a repeated request a +repetition. Collecting an order that is already paid, but naming a different +choice than the one it was paid with, describes a payment that never happened +and fails with +``TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISMATCH``; the response +names the choice the order was actually paid with, so that a client can +correct its request. This is not specific to collecting: deciding that a +request repeats an earlier payment means comparing it against that payment, +and the payment logic compared the coins and the input tokens but not the +choice. For a choice that costs nothing in Taler both are empty, so paying +such an order a second time while naming another choice used to be reported +as success. The comparison was completed rather than worked around in the +collect endpoint, so wallets benefit from it as well. + Failure Handling ---------------- @@ -318,8 +332,10 @@ to merchant-facing applications. No dedicated order status value is introduced for this: the order simply remains in its regular claimed or unpaid state, and merchant-facing applications can recognize the situation from the presence of ``amount_external`` in an unpaid order. Such orders -must not be deleted through normal order deletion. Any order carrying -``amount_external`` can only be deleted by explicitly passing ``force=yes`` +must not be deleted through normal order deletion. Any order that records at +least one settled external payment in ``amount_external`` -- an empty array +records none and does not block anything -- can only be deleted by explicitly +passing ``force=yes`` to ``DELETE /private/orders/$ORDER_ID``; a normal deletion attempt fails with ``TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_EXTERNALLY_PAID``. The force requirement makes it clear that externally handled payments may already @@ -484,8 +500,8 @@ Test Plan any choice uses a different currency than the external entries. * Merchant backend tests preserving ``amount_external`` entries with flat method-specific fields. -* Merchant backend tests rejecting normal deletion of orders carrying - ``amount_external`` unless an explicit force operation is used. +* Merchant backend tests rejecting normal deletion of orders that record + settled external payments 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 @@ -495,6 +511,9 @@ Test Plan a choice or selects one that does not exist, and of a v0 order that selects a choice although it has none. * Merchant backend tests for collect idempotency. +* Merchant backend tests rejecting a second payment of an already paid v1 + order that names a different choice, both through the collect endpoint and + through the wallet-facing payment endpoint. * Merchant backend tests rejecting normal deletion of an order claimed by a wallet unless an explicit force operation is used. * Merchant backend tests recording external refunds, including refunds through @@ -665,7 +684,14 @@ Discussion / Q&A deterministic backend claim nonce, with a mandatory ``choice_index`` for v1 orders. No dedicated manual-resolution status value was introduced; blocking non-forced deletion was deemed sufficient. Error - codes 2535-2538 (``..._COLLECT_NOT_FREE``, ``..._COLLECT_ALREADY_CLAIMED``, + codes 2535-2539 (``..._COLLECT_NOT_FREE``, ``..._COLLECT_ALREADY_CLAIMED``, ``..._DELETE_ORDERS_EXTERNALLY_PAID``, - ``..._REFUND_EXTERNAL_INCONSISTENT_AMOUNT``) were registered in GANA for - the new failure modes. + ``..._REFUND_EXTERNAL_INCONSISTENT_AMOUNT``, + ``..._REFUND_EXTERNAL_ALREADY_EXISTS``) were registered in GANA for + the new failure modes, and 2187 + (``MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISMATCH``) for paying or + collecting an order that is already paid with another choice. The latter + was given its own code rather than reusing ``..._PAY_ALREADY_PAID``, + because it tells the client something it can act on -- which choice the + order was actually paid with -- while ``ALREADY_PAID`` means the payment + belongs to somebody else and there is nothing to correct.