View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012146 | Taler | wallet-core | public | 2026-11-15 17:23 | 2026-11-15 17:24 |
| Reporter | bot-cg [agent] | ||||
| Priority | normal | Severity | minor | Reproducibility | always |
| Status | new | ||||
| Product Version | git (master) | ||||
| Summary | 0012146: wallet-core: a refund URI that spells the merchant instance differently answers "no purchase found for the refund" | ||||
| Description | startRefundQueryForUri looks the purchase up under the merchant base URL exactly as the taler://refund/ URI spells it. A stored purchase is filed under the canonical (lower-case) instance URL from the contract terms, so a refund URI naming /instances/MyShop/ instead of /instances/myshop/ finds nothing. The wallet then throws WALLET_PURCHASE_NOT_FOUND (7051), the error 0010692 introduced for an order bought with a different wallet, and the refund cannot be collected through that QR code. 0011340 made instance names case-insensitive and fixed the same lookup for re-scanned taler://pay URIs (8e57ab32d, createOrReusePurchase), but the refund lookup was not covered. The backend's own taler_refund_uri and the merchant web UI's refund QR use the canonical spelling, so this hits refund URIs that an integration composes from a base URL configured with different casing. | ||||
| Steps To Reproduce | The attached patch extends the taler-harness test merchant-instance-casing: after the payment, grant a refund, replace /instances/<id>/ in taler_refund_uri with the upper-case spelling, and call startRefundQueryForUri. On master 2f657b0ba the test fails with "no purchase found for the refund"; with the patch applied it passes. | ||||
| Additional Information | Attached fix: the refund lookup tries the URI's spelling and then the canonical one, through a small helper that the existing createOrReusePurchase lookup now shares. Rebased from a July branch; the share-payment part of that branch was dropped because sharePayment has since been replaced by unclaimPayment(proposalId), which needs no URL lookup. | ||||
| Tags | No tags attached. | ||||
| Attached Files | 0001-wallet-resolve-refund-URIs-across-merchant-instance-casing.patch (5,917 bytes)
From 616d41d3abb4d84315ea43bd203ade68be620047 Mon Sep 17 00:00:00 2001
From: Florian Dold <dold@taler.net>
Date: Fri, 31 Jul 2026 16:01:41 +0200
Subject: [PATCH] wallet: resolve refund URIs across merchant instance name
casing
startRefundQueryForUri looked the purchase up under the refund URI's base URL,
so a QR spelling the instance differently reported that the order had been
paid with another wallet.
Issue: https://bugs.taler.net/n/12146
Assisted by LLM
---
.../test-merchant-instance-casing.ts | 30 ++++++++++
.../taler-wallet-core/src/pay-merchant.ts | 55 ++++++++++++-------
2 files changed, 65 insertions(+), 20 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instance-casing.ts b/packages/taler-harness/src/integrationtests/test-merchant-instance-casing.ts
index 41963b7c9..afaf25039 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instance-casing.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instance-casing.ts
@@ -378,6 +378,36 @@ export async function runMerchantInstanceCasingTest(t: GlobalTestState) {
await orderClient.getOrderDetails(instanceToken, orderResp.order_id),
);
t.assertDeepEqual(orderStatus.order_status, "paid");
+
+ {
+ // A taler://refund/ URI carries a merchant base URL too, and the wallet
+ // resolves it back to the purchase the same way. Spelling the instance
+ // differently must still find the payment -- otherwise scanning a refund
+ // QR reports that the order was paid with some other wallet.
+ const refund = succeedOrThrow(
+ await orderClient.addRefund(instanceToken, orderResp.order_id, {
+ refund: "TESTKUDOS:2",
+ reason: "instance casing test",
+ }),
+ );
+ const uncanonicalRefundUri = refund.taler_refund_uri.replace(
+ `/instances/${CANONICAL_ID}/`,
+ `/instances/${UPPER_ID}/`,
+ );
+ t.assertTrue(uncanonicalRefundUri !== refund.taler_refund_uri);
+ console.log(`refund query via: ${uncanonicalRefundUri}`);
+
+ const refundQuery = await walletClient.call(
+ WalletApiOperation.StartRefundQueryForUri,
+ {
+ talerRefundUri: uncanonicalRefundUri,
+ },
+ );
+ t.assertDeepEqual(
+ refundQuery.transactionId,
+ preparePayResult.transactionId,
+ );
+ }
}
runMerchantInstanceCasingTest.suites = ["merchant", "wallet"];
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index c67d03b65..1d86dddcb 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -1961,6 +1961,24 @@ async function generateSlate(
});
}
+/**
+ * The spellings of a merchant base URL under which a stored purchase might be
+ * filed: the one we were handed, plus -- when it differs -- the one with the
+ * instance segment folded to lower case.
+ *
+ * A base URL taken from a taler:// URI or passed in by an API caller may spell
+ * the merchant instance in any case, since the merchant accepts all of them.
+ * A stored purchase, however, always holds the canonical spelling that came
+ * back in the contract terms, so a lookup that only tries the caller's
+ * spelling silently misses it.
+ */
+function merchantBaseUrlSpellings(merchantBaseUrl: string): string[] {
+ const canonical = canonicalizeMerchantInstanceUrl(merchantBaseUrl);
+ return canonical === merchantBaseUrl
+ ? [merchantBaseUrl]
+ : [merchantBaseUrl, canonical];
+}
+
/**
* Create a new purchase transaction if necessary. If a purchase
* record for the provided arguments already exists,
@@ -2027,25 +2045,19 @@ export async function createOrReusePurchase(
proposalId: string;
transactionId: TransactionIdStr;
}> {
- // Find existing proposals from the same merchant
- // with the same order ID.
- //
- // Once a proposal has been downloaded its merchantBaseUrl is the one from
- // the contract terms, which always spells the instance in lower case. A
- // taler:// URI may spell it differently -- the merchant accepts any casing
- // -- so look the record up under the canonical spelling as well, or
- // re-scanning such a URI would claim the order a second time instead of
+ // Find existing proposals from the same merchant with the same order ID.
+ // Once a proposal has been downloaded it is filed under the base URL from
+ // the contract terms, so re-scanning a URI that spells the instance
+ // differently would otherwise claim the order a second time instead of
// returning the transaction that already exists.
- const canonicalBaseUrl = canonicalizeMerchantInstanceUrl(merchantBaseUrl);
const oldProposals = await wex.runWalletDbTx(async (tx) => {
- const found = await tx.getPurchasesByUrlAndOrderId(
- merchantBaseUrl,
- orderId,
- );
- if (found.length > 0 || canonicalBaseUrl === merchantBaseUrl) {
- return found;
+ for (const baseUrl of merchantBaseUrlSpellings(merchantBaseUrl)) {
+ const found = await tx.getPurchasesByUrlAndOrderId(baseUrl, orderId);
+ if (found.length > 0) {
+ return found;
+ }
}
- return tx.getPurchasesByUrlAndOrderId(canonicalBaseUrl, orderId);
+ return [];
});
if (oldProposals.length > 1) {
@@ -6156,10 +6168,13 @@ export async function startRefundQueryForUri(
);
}
const purchaseRecord = await wex.runWalletDbTx(async (tx) => {
- return tx.getPurchaseByUrlAndOrderId(
- parsedUri.merchantBaseUrl,
- parsedUri.orderId,
- );
+ for (const baseUrl of merchantBaseUrlSpellings(parsedUri.merchantBaseUrl)) {
+ const p = await tx.getPurchaseByUrlAndOrderId(baseUrl, parsedUri.orderId);
+ if (p) {
+ return p;
+ }
+ }
+ return undefined;
});
if (!purchaseRecord) {
// Scanning a refund QR code for an order that was paid with a different
--
2.47.3
| ||||
| related to | 0011340 | closed | Florian Dold | instance names are/were case-sensitive (but should be case-insensitive) |
| related to | 0010692 | closed | Florian Dold | wallet-core should return proper error code when no purchase is found for refund |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-15 17:23 | bot-cg | New Issue | |
| 2026-11-15 17:23 | bot-cg | Relationship added | related to 0010692 |
| 2026-11-15 17:23 | bot-cg | Relationship added | related to 0011340 |
| 2026-11-15 17:23 | bot-cg | Implementer | => avalos |
| 2026-11-15 17:23 | bot-cg | Reviewer | => oec |
| 2026-11-15 17:23 | bot-cg | Release signed by | => Florian Dold |
| 2026-11-15 17:23 | bot-cg | Deployed by | => oec |
| 2026-11-15 17:23 | bot-cg | Verifier | => oec |
| 2026-11-15 17:23 | bot-cg | Communicator | => CM_7 |
| 2026-11-15 17:24 | bot-cg | File Added: 0001-wallet-resolve-refund-URIs-across-merchant-instance-casing.patch |