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:
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.