commit afb8d939ff578f354c46a26cc6c44392bc27ef8f
parent d33158ea13e9e5c1664964f1080fd4293bcbde20
Author: Florian Dold <florian@dold.me>
Date: Thu, 13 Nov 2025 13:33:53 +0100
consider already_paid_order_id in dialog(shared) state
Diffstat:
1 file changed, 65 insertions(+), 13 deletions(-)
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -42,6 +42,7 @@ import {
codecForAbortResponse,
codecForMerchantContractTerms,
codecForMerchantOrderStatusPaid,
+ codecForMerchantOrderStatusUnpaid,
codecForMerchantPayResponse,
codecForPostOrderResponse,
codecForWalletRefundResponse,
@@ -119,6 +120,7 @@ import {
import {
getHttpResponseErrorDetails,
HttpResponse,
+ readResponseJsonOrThrow,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
@@ -4110,34 +4112,84 @@ async function checkIfOrderIsAlreadyPaid(
throw Error(`this order cant be paid: ${resp.status}`);
}
+/**
+ * While the transaction is in the dialog(shared) state,
+ * we long-poll the merchant. We do this to find out if
+ * another wallet paid for the order.
+ */
async function processPurchaseDialogShared(
wex: WalletExecutionContext,
purchase: PurchaseRecord,
): Promise<TaskRunResult> {
const proposalId = purchase.proposalId;
logger.trace(`processing dialog-shared for proposal ${proposalId}`);
- const download = await expectProposalDownload(wex, purchase);
+ const ctx = new PayMerchantTransactionContext(wex, proposalId);
+ const txRes = await wex.db.runAllStoresReadOnlyTx({}, async (tx) => {
+ const rec = await tx.purchases.get(proposalId);
+ if (!rec) {
+ return undefined;
+ }
+ const download = await expectProposalDownloadByIdInTx(wex, tx, proposalId);
+ return { download, rec };
+ });
+ if (!txRes) {
+ // Transaction longer exists.
+ return TaskRunResult.finished();
+ }
+ const { download, rec } = txRes;
if (purchase.purchaseStatus !== PurchaseStatus.DialogShared) {
return TaskRunResult.finished();
}
- const ctx = new PayMerchantTransactionContext(wex, proposalId);
+ const contractTerms = download.contractTerms;
- const paid = await checkIfOrderIsAlreadyPaid(wex, download, true);
+ let paidByOther = false;
- if (paid) {
- await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
- const [p, h] = await ctx.getRecordHandle(tx);
- if (!p) {
- return;
+ const requestUrl = new URL(
+ `orders/${contractTerms.order_id}`,
+ contractTerms.merchant_base_url,
+ );
+ requestUrl.searchParams.set("h_contract", download.contractTermsHash);
+ if (rec.lastSessionId != null) {
+ requestUrl.searchParams.set("session_id", rec.lastSessionId);
+ }
+
+ let httpResp: HttpResponse;
+
+ httpResp = await cancelableLongPoll(wex, requestUrl);
+
+ switch (httpResp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.Accepted:
+ case HttpStatusCode.Found:
+ paidByOther = true;
+ break;
+ case HttpStatusCode.PaymentRequired:
+ const resp = await readResponseJsonOrThrow(
+ httpResp,
+ codecForMerchantOrderStatusUnpaid(),
+ );
+ if (resp.already_paid_order_id != null) {
+ paidByOther = true;
}
- p.purchaseStatus = PurchaseStatus.FailedPaidByOther;
- await h.update(p);
- });
- return TaskRunResult.progress();
+ break;
}
- return TaskRunResult.backoff();
+ if (!paidByOther) {
+ return TaskRunResult.backoff();
+ }
+
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ const [p, h] = await ctx.getRecordHandle(tx);
+ switch (p?.purchaseStatus) {
+ case PurchaseStatus.DialogShared:
+ break;
+ default:
+ return;
+ }
+ await h.update(p);
+ });
+ return TaskRunResult.progress();
}
async function processPurchaseAutoRefund(