taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-wallet-core/src/pay-merchant.ts | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
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(