summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-05-25 11:13:19 +0200
committerFlorian Dold <florian@dold.me>2023-05-25 11:13:25 +0200
commit0406160869e7f9aa9e863acad58a160a14014467 (patch)
treed2b40b632184fe1aae0759563135a230dd801057 /packages/taler-wallet-core/src
parent4859883c9aa124541c5f5cbeaca4b836449d3893 (diff)
downloadwallet-core-0406160869e7f9aa9e863acad58a160a14014467.tar.gz
wallet-core-0406160869e7f9aa9e863acad58a160a14014467.tar.bz2
wallet-core-0406160869e7f9aa9e863acad58a160a14014467.zip
wallet-core: DD37 fixes and FIXME comments for merchant payments
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts128
1 files changed, 96 insertions, 32 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index 54953246d..13fb2cb18 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019-2022 Taler Systems S.A.
+ (C) 2019-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -40,7 +40,6 @@ import {
CoinRefreshRequest,
ConfirmPayResult,
ConfirmPayResultType,
- constructPayUri,
ContractTermsUtil,
Duration,
encodeCrock,
@@ -63,6 +62,7 @@ import {
randomBytes,
RefreshReason,
StartRefundQueryForUriResponse,
+ stringifyTalerUri,
TalerError,
TalerErrorCode,
TalerErrorDetail,
@@ -197,16 +197,25 @@ async function failProposalPermanently(
proposalId: string,
err: TalerErrorDetail,
): Promise<void> {
- await ws.db
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ const transitionInfo = await ws.db
.mktx((x) => [x.purchases])
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
return;
}
+ // FIXME: We don't store the error detail here?!
+ const oldTxState = computePayMerchantTransactionState(p);
p.purchaseStatus = PurchaseStatus.FailedClaim;
+ const newTxState = computePayMerchantTransactionState(p);
await tx.purchases.put(p);
+ return { oldTxState, newTxState };
});
+ notifyTransition(ws, transactionId, transitionInfo);
}
function getProposalRequestTimeout(retryInfo?: RetryInfo): Duration {
@@ -226,8 +235,6 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
/**
* Return the proposal download data for a purchase, throw if not available.
- *
- * (Async since in the future this will query the DB.)
*/
export async function expectProposalDownload(
ws: InternalWalletState,
@@ -314,10 +321,9 @@ export function extractContractData(
};
}
-export async function processDownloadProposal(
+async function processDownloadProposal(
ws: InternalWalletState,
proposalId: string,
- options: object = {},
): Promise<OperationAttemptResult> {
const proposal = await ws.db
.mktx((x) => [x.purchases])
@@ -339,6 +345,11 @@ export async function processDownloadProposal(
};
}
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
const orderClaimUrl = new URL(
`orders/${proposal.orderId}/claim`,
proposal.merchantBaseUrl,
@@ -363,7 +374,8 @@ export async function processDownloadProposal(
});
// FIXME: Do this in the background using the new return value
- const httpResponse = await ws.http.postJson(orderClaimUrl, requestBody, {
+ const httpResponse = await ws.http.fetch(orderClaimUrl, {
+ body: requestBody,
timeout: getProposalRequestTimeout(retryRecord?.retryInfo),
});
const r = await readSuccessResponseJsonOrErrorCode(
@@ -388,7 +400,7 @@ export async function processDownloadProposal(
const proposalResp = r.response;
// The proposalResp contains the contract terms as raw JSON,
- // as the coded to parse them doesn't necessarily round-trip.
+ // as the code to parse them doesn't necessarily round-trip.
// We need this raw JSON to compute the contract terms hash.
// FIXME: Do better error handling, check if the
@@ -496,7 +508,7 @@ export async function processDownloadProposal(
logger.trace(`extracted contract data: ${j2s(contractData)}`);
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.purchases, x.contractTerms])
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
@@ -506,6 +518,7 @@ export async function processDownloadProposal(
if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) {
return;
}
+ const oldTxState = computePayMerchantTransactionState(p);
p.download = {
contractTermsHash,
contractTermsMerchantSig: contractData.merchantSig,
@@ -523,18 +536,28 @@ export async function processDownloadProposal(
) {
const differentPurchase =
await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
+ // FIXME: Adjust this to account for refunds, don't count as repurchase
+ // if original order is refunded.
if (differentPurchase) {
logger.warn("repurchase detected");
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
p.repurchaseProposalId = differentPurchase.proposalId;
await tx.purchases.put(p);
- return;
}
+ } else {
+ p.purchaseStatus = PurchaseStatus.Proposed;
+ await tx.purchases.put(p);
+ }
+ const newTxState = computePayMerchantTransactionState(p);
+ return {
+ oldTxState,
+ newTxState,
}
- p.purchaseStatus = PurchaseStatus.Proposed;
- await tx.purchases.put(p);
});
+ notifyTransition(ws, transactionId, transitionInfo);
+
+ // FIXME: Deprecated pre-DD37 notification, remove eventually
ws.notify({
type: NotificationType.ProposalDownloaded,
proposalId: proposal.proposalId,
@@ -547,13 +570,11 @@ export async function processDownloadProposal(
}
/**
- * Download a proposal and store it in the database.
- * Returns an id for it to retrieve it later.
- *
- * @param sessionId Current session ID, if the proposal is being
- * downloaded in the context of a session ID.
+ * Create a new purchase transaction if necessary. If a purchase
+ * record for the provided arguments already exists,
+ * return the old proposal ID.
*/
-async function startDownloadProposal(
+async function createPurchase(
ws: InternalWalletState,
merchantBaseUrl: string,
orderId: string,
@@ -619,7 +640,7 @@ async function startDownloadProposal(
posConfirmation: undefined,
};
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.purchases])
.runReadWrite(async (tx) => {
const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([
@@ -628,11 +649,25 @@ async function startDownloadProposal(
]);
if (existingRecord) {
// Created concurrently
- return;
+ return undefined;
}
await tx.purchases.put(proposalRecord);
+ const oldTxState: TransactionState = {
+ major: TransactionMajorState.None,
+ };
+ const newTxState = computePayMerchantTransactionState(proposalRecord);
+ return {
+ oldTxState,
+ newTxState,
+ }
});
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+
await processDownloadProposal(ws, proposalId);
return proposalId;
}
@@ -643,8 +678,12 @@ async function storeFirstPaySuccess(
sessionId: string | undefined,
payResponse: MerchantPayResponse,
): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.purchases, x.contractTerms])
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -658,6 +697,7 @@ async function storeFirstPaySuccess(
logger.warn("payment success already stored");
return;
}
+ const oldTxState = computePayMerchantTransactionState(purchase);
if (purchase.purchaseStatus === PurchaseStatus.Paying) {
purchase.purchaseStatus = PurchaseStatus.Done;
}
@@ -686,7 +726,13 @@ async function storeFirstPaySuccess(
);
}
await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return {
+ oldTxState,
+ newTxState,
+ }
});
+ notifyTransition(ws, transactionId, transitionInfo);
}
async function storePayReplaySuccess(
@@ -694,7 +740,11 @@ async function storePayReplaySuccess(
proposalId: string,
sessionId: string | undefined,
): Promise<void> {
- await ws.db
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+ const transitionInfo = await ws.db
.mktx((x) => [x.purchases])
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -707,6 +757,7 @@ async function storePayReplaySuccess(
if (isFirst) {
throw Error("invalid payment state");
}
+ const oldTxState = computePayMerchantTransactionState(purchase);
if (
purchase.purchaseStatus === PurchaseStatus.Paying ||
purchase.purchaseStatus === PurchaseStatus.PayingReplay
@@ -715,7 +766,10 @@ async function storePayReplaySuccess(
}
purchase.lastSessionId = sessionId;
await tx.purchases.put(purchase);
+ const newTxState = computePayMerchantTransactionState(purchase);
+ return { oldTxState, newTxState };
});
+ notifyTransition(ws, transactionId, transitionInfo);
}
/**
@@ -876,6 +930,10 @@ async function unblockBackup(
});
}
+// FIXME: Should probably not be exported in its current state
+// FIXME: Should take a transaction ID instead of a proposal ID
+// FIXME: Does way more than checking the payment
+// FIXME: Should return immediately.
export async function checkPaymentByProposalId(
ws: InternalWalletState,
proposalId: string,
@@ -918,13 +976,14 @@ export async function checkPaymentByProposalId(
proposalId,
});
- const talerUri = constructPayUri(
- proposal.merchantBaseUrl,
- proposal.orderId,
- proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
- proposal.claimToken,
- proposal.noncePriv,
- );
+ const talerUri = stringifyTalerUri({
+ type: TalerUriAction.Pay,
+ merchantBaseUrl: proposal.merchantBaseUrl,
+ orderId: proposal.orderId,
+ sessionId: proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
+ claimToken: proposal.claimToken,
+ noncePriv: proposal.noncePriv,
+ });
// First check if we already paid for it.
const purchase = await ws.db
@@ -989,17 +1048,22 @@ export async function checkPaymentByProposalId(
"automatically re-submitting payment with different session ID",
);
logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
- await ws.db
+ const transitionInfo = await ws.db
.mktx((x) => [x.purchases])
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
return;
}
+ const oldTxState = computePayMerchantTransactionState(p);
p.lastSessionId = sessionId;
p.purchaseStatus = PurchaseStatus.PayingReplay;
await tx.purchases.put(p);
+ const newTxState = computePayMerchantTransactionState(p);
+ return { oldTxState, newTxState };
});
+ notifyTransition(ws, transactionId, transitionInfo);
+ // FIXME: What about error handling?! This doesn't properly store errors in the DB.
const r = await processPurchasePay(ws, proposalId, { forceNow: true });
if (r.type !== OperationAttemptResultType.Finished) {
// FIXME: This does not surface the original error
@@ -1092,7 +1156,7 @@ export async function preparePayForUri(
);
}
- const proposalId = await startDownloadProposal(
+ const proposalId = await createPurchase(
ws,
uriResult.merchantBaseUrl,
uriResult.orderId,