commit 7dc0e5a0f4e3ae3207a6554110d12055396b4f5d
parent 2615380f936607485ae9ba6dd151bd080e7c7247
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 10 Oct 2026 17:08:04 +0200
specify vDonauReceipts API change
Diffstat:
6 files changed, 163 insertions(+), 23 deletions(-)
diff --git a/core/api-donau.rst b/core/api-donau.rst
@@ -67,7 +67,8 @@ The current protocol version is **v1**.
**Upcoming versions:**
-* none anticipated
+* ``vLimitConflict``: answers a ``/batch-issue`` request that exceeds the
+ charity's remaining yearly limit with 409 instead of 400
**Ideas for future version:**
diff --git a/core/api-merchant.rst b/core/api-merchant.rst
@@ -92,6 +92,11 @@ The currently implemented protocol version is **v42**.
* ``vTokenFountains``: adds fountains for distributing promotional
merchant tokens
(see :doc:`DD 98 </design-documents/098-token-fountains>`)
+* ``vDonauReceipts``: returns donation receipts from ``/pay`` in the new
+ ``donau_sigs`` field instead of inside ``token_sigs`` (breaking for orders
+ with a donation receipt output), answers a Donau failure after a
+ successful payment with a 502 that carries the payment confirmation, and
+ refuses to replay ``/pay`` after a refund
**Ideas for future version:**
diff --git a/core/donau/post-batch-issue-CHARITY_ID.rst b/core/donau/post-batch-issue-CHARITY_ID.rst
@@ -11,11 +11,11 @@
Also returned for idempotent re-requests of an already-processed batch.
:http:statuscode:`400 Bad Request`:
The request was malformed. This can occur when the ``$CHARITY_ID`` cannot be
- parsed, the ``budikeypairs`` array is empty or contains malformed entries,
- or the total donation amount exceeds the charity's per-year limit.
- Returned with error code ``TALER_EC_GENERIC_PARAMETER_MALFORMED``,
- ``TALER_EC_GENERIC_JSON_INVALID``, or
- ``TALER_EC_DONAU_EXCEEDING_DONATION_LIMIT``.
+ parsed, or the ``budikeypairs`` array is empty or contains malformed entries.
+ Returned with error code ``TALER_EC_GENERIC_PARAMETER_MALFORMED`` or
+ ``TALER_EC_GENERIC_JSON_INVALID``.
+ Before protocol **vLimitConflict**, an exceeded per-year limit was also
+ reported with this status (see 409).
:http:statuscode:`403 Forbidden`:
The charity signature is invalid.
Returned with error code ``TALER_EC_DONAU_CHARITY_SIGNATURE_INVALID``.
@@ -25,9 +25,14 @@
Returned with error code ``TALER_EC_DONAU_CHARITY_NOT_FOUND`` or
``TALER_EC_DONAU_GENERIC_DONATION_UNIT_UNKNOWN``.
:http:statuscode:`409 Conflict`:
- The year specified does not match the validity year for the given
- denomination unit key. Returned with an error code of
- ``TALER_EC_DONAU_GENERIC_DONATION_UNIT_WRONG_YEAR``.
+ The request conflicts with the state of the Donau. Clients must
+ distinguish the cases by the error code:
+
+ * ``TALER_EC_DONAU_GENERIC_DONATION_UNIT_WRONG_YEAR``: the year specified
+ does not match the validity year for the given donation unit key.
+ * ``TALER_EC_DONAU_EXCEEDING_DONATION_LIMIT``: the total donation amount
+ exceeds what remains of the charity's per-year limit.
+ Since protocol **vLimitConflict**; before, this was a 400.
:http:statuscode:`410 Gone`:
The year specified is in the past, and the Donau does
not permit issuing donation receipts in the past. The
diff --git a/core/merchant/post-orders-ORDER_ID-pay.rst b/core/merchant/post-orders-ORDER_ID-pay.rst
@@ -6,6 +6,14 @@
the contract is implied by the signatures of the coins. Furthermore, this
API doesn't really return useful information about the order.
+ Repeating the request for an order that is already paid (a *replay*) is
+ answered from the state stored with the original payment: the token
+ signatures are the ones created for the original payment, and donation
+ receipts that are still missing are requested from the Donau with the
+ ``budikeypairs`` of the original payment, regardless of the ``wallet_data``
+ of the replay. Since protocol **vDonauReceipts**, a replay is refused
+ once the order has been refunded, even if only in part.
+
**Request:**
The request must be a `pay request <PayRequest>`.
@@ -15,6 +23,8 @@
:http:statuscode:`200 OK`:
The exchange accepted all of the coins.
The body is a `payment response <PaymentResponse>`.
+ If the wallet requested donation receipts, the Donau issued them
+ and they are part of the response.
The ``frontend`` should now fulfill the contract.
Note that it is possible that refunds have been granted.
:http:statuscode:`400 Bad request`:
@@ -94,7 +104,8 @@
The exchange rejected the payment because a coin was already spent (or
used in a different way for the same purchase previously), or
the merchant rejected the payment because the order was already fully paid
- (and then return signatures with refunds). If a coin was already spent
+ (and then return signatures with refunds) or has been refunded since it
+ was paid. If a coin was already spent
(this includes re-using the same coin after a refund),
the response will include the ``exchange_url`` for which the payment failed,
in addition to the response from the exchange to the :http:post:`/batch-deposit </batch-deposit>` request.
@@ -114,7 +125,21 @@
* ``MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID``: A token was already used
in a previous payment.
* ``MERCHANT_POST_ORDERS_ID_PAY_DONATION_AMOUNT_MISMATCH``: The donation
- amount does not match the expected amount or exceeds the yearly limit.
+ amount does not match the expected amount or exceeds what remains of
+ the charity's yearly limit. Since protocol **vDonauReceipts**, the
+ donations whose receipts are still pending count against that limit.
+ * ``MERCHANT_POST_ORDERS_ID_PAY_DONATION_YEAR_INVALID``: The ``year`` in
+ the ``donau`` field of the wallet data is not the current year (UTC).
+ Nothing was paid. The response is a `DonationYearInvalidResponse`;
+ the wallet should create donation receipt requests for the returned
+ ``current_year`` and pay again.
+ Since protocol **vDonauReceipts**.
+ * ``MERCHANT_POST_ORDERS_ID_PAY_REPLAY_AFTER_REFUND``: The order was
+ already paid and has since been refunded, possibly only in part, so the
+ request cannot be replayed. The response carries no payment
+ signature, token signatures or donation receipts; the wallet should
+ query the refunds instead.
+ Since protocol **vDonauReceipts**.
* ``MERCHANT_GENERIC_CURRENCY_MISMATCH``: Coin currency does not match
the expected payment currency.
@@ -178,15 +203,37 @@
* ``MERCHANT_GENERIC_FEATURE_NOT_AVAILABLE``: usually returned if a token type is not supported
:http:statuscode:`502 Bad gateway`:
- The merchant's interaction with the exchange failed in some way.
- The client might want to try again later.
+ The merchant's interaction with the exchange or the Donau failed in
+ some way.
+ For the exchange, the client might want to try again later.
This includes failures such as the denomination key of a coin not being
known to the exchange as far as the merchant can tell.
+ The merchant only contacts the Donau after the payment succeeded, so
+ a Donau failure does not undo the payment: the response then carries the
+ payment confirmation.
Applicable error codes:
* ``MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS``
* ``MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED``
- * ``MERCHANT_GENERIC_DONAU_INVALID_RESPONSE``
+ * ``MERCHANT_POST_ORDERS_ID_PAY_DONAU_RECEIPTS_DELAYED``: The payment
+ succeeded, but the Donau could not issue the donation receipts at this
+ time (it was unreachable, timed out, failed internally or replied with
+ something malformed). The body is a `PaymentDonauFailureResponse`.
+ The wallet should treat the order as paid, keep its donation receipt
+ requests and replay the payment request later. The merchant keeps
+ reporting this code until the Donau issues the receipts or stops
+ issuing receipts for the donation year; from then on it reports
+ ``MERCHANT_POST_ORDERS_ID_PAY_DONAU_RECEIPTS_REFUSED``.
+ Since protocol **vDonauReceipts**.
+ * ``MERCHANT_POST_ORDERS_ID_PAY_DONAU_RECEIPTS_REFUSED``: The payment
+ succeeded, but the Donau refused to issue the donation receipts. The
+ body is a `PaymentDonauFailureResponse`. The wallet should treat the
+ order as paid, tell the user that the promised donation receipts will
+ not be issued, and not replay the request.
+ Since protocol **vDonauReceipts**.
+ * ``MERCHANT_GENERIC_DONAU_INVALID_RESPONSE``: Only before protocol
+ **vDonauReceipts**, for any Donau failure; the response then carried
+ no payment confirmation.
:http:statuscode:`504 Gateway timeout`:
The merchant's interaction with the exchange took too long.
@@ -230,18 +277,92 @@
// Since protocol **vChallengeConfirmation**.
pos_confirmation?: string;
- // Signed tokens. Returned in the same order as the
- // token envelopes were provided in the request. Specifically,
- // the order will follow the order of the outputs from the
- // contract terms, and then within each output follow the
- // order in which the ``wallet_data`` contained the respective
- // blinded envelopes. The donation tokens will be present
- // at the offset matching the place where a donation receipt
- // was indicated in the outputs array, and of course be skipped
- // if the `PayWalletData` did not have a ``donau`` field.
+ // Signed tokens for the outputs of type ``token``. Returned in
+ // the same order as the token envelopes were provided in the
+ // request. Specifically, the order will follow the order of the
+ // outputs from the contract terms, and then within each output
+ // follow the order in which the ``wallet_data`` contained the
+ // respective blinded envelopes.
+ // Since protocol **vDonauReceipts**, donation receipts are never
+ // part of this array, see ``donau_sigs``. Before, they were
+ // present at the offset matching the place where a donation
+ // receipt was indicated in the outputs array, and ``donau_sigs``
+ // was absent; clients must still handle that layout when
+ // ``donau_sigs`` is missing although they requested donation
+ // receipts.
// @since protocol **v21**
token_sigs?: SignedTokenEnvelope[];
+ // Donation receipts blindly signed by the Donau, in the order of
+ // the ``budikeypairs`` in the ``donau`` field of the
+ // `PayWalletData`. Present if the wallet requested donation
+ // receipts.
+ // @since protocol **vDonauReceipts**
+ donau_sigs?: SignedDonationReceipt[];
+
+ }
+
+ .. ts:def:: SignedDonationReceipt
+
+ interface SignedDonationReceipt {
+
+ // Blind signature made by the Donau over one of the
+ // `BlindedDonationReceiptKeyPair` entries of the request.
+ blind_sig: BlindedDonationReceiptSignature;
+
+ }
+
+ .. ts:def:: PaymentDonauFailureResponse
+
+ interface PaymentDonauFailureResponse {
+
+ // Numeric `error code <error-codes>`, either
+ // ``MERCHANT_POST_ORDERS_ID_PAY_DONAU_RECEIPTS_DELAYED`` or
+ // ``MERCHANT_POST_ORDERS_ID_PAY_DONAU_RECEIPTS_REFUSED``.
+ code: Integer;
+
+ // Human-readable description of the failure.
+ hint?: string;
+
+ // The order is paid: as in `PaymentResponse`.
+ sig: EddsaSignature;
+
+ // As in `PaymentResponse`.
+ pos_confirmation?: string;
+
+ // As in `PaymentResponse`. The merchant's own token
+ // signatures are returned even though the donation
+ // receipts are missing.
+ token_sigs?: SignedTokenEnvelope[];
+
+ // Base URL of the Donau that was asked for the receipts.
+ donau_url: WebURL;
+
+ // HTTP status code of the Donau's reply, or 0 if there was
+ // no reply (Donau unreachable, timeout, or the request could
+ // not be created). A malformed reply keeps its status code.
+ donau_http_status: Integer;
+
+ // Error code of the Donau's reply, if it had one.
+ donau_ec?: Integer;
+
+ // Hint of the Donau's reply, if it had one.
+ donau_hint?: string;
+
+ }
+
+ .. ts:def:: DonationYearInvalidResponse
+
+ interface DonationYearInvalidResponse {
+
+ // Error code, always
+ // ``MERCHANT_POST_ORDERS_ID_PAY_DONATION_YEAR_INVALID``.
+ code: Integer;
+
+ // Current year (UTC), the only year for which the merchant
+ // accepts donation receipt requests.
+ current_year: Integer;
+
}
.. ts:def:: PayRequest
diff --git a/core/merchant/post-private-orders-ORDER_ID-refund-external.rst b/core/merchant/post-private-orders-ORDER_ID-refund-external.rst
@@ -6,6 +6,10 @@
funds, and no wallet pickup step exists or is needed. The recorded entries
are exposed through the order status as ``refunds_external``.
+ Since protocol **vDonauReceipts**, an external refund, like a Taler refund,
+ ends the wallet's ability to replay its payment request. Donation receipts
+ that the Donau had not yet issued for the order are then never issued.
+
External refunds are available for any *paid* order, including pure Taler
orders without ``amount_external``. This is relevant because Taler refunds
are constrained by the contract's refund deadline; once it has passed, the
diff --git a/core/merchant/post-private-orders-ORDER_ID-refund.rst b/core/merchant/post-private-orders-ORDER_ID-refund.rst
@@ -7,6 +7,10 @@
methods, the cumulative Taler refund must not exceed the full order total
minus those external refunds.
+ Since protocol **vDonauReceipts**, any refund, even a partial one, ends the
+ wallet's ability to replay its payment request. Donation receipts that the
+ Donau had not yet issued for the order are then never issued.
+
**Required permission:** ``orders-refund`` (see :ref:`Scopes <merchant-api-scopes>`)
**Request:**