wallet-core: DD37 fixes and FIXME comments for merchant payments

This commit is contained in:
Florian Dold 2023-05-25 11:13:19 +02:00
parent 4859883c9a
commit 0406160869
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
2 changed files with 96 additions and 34 deletions

View File

@ -86,8 +86,6 @@ export enum TransactionMajorState {
Failed = "failed",
// Only used for the notification, never in the transaction history
Deleted = "deleted",
// Placeholder until D37 is fully implemented
Unknown = "unknown",
}
export enum TransactionMinorState {

View File

@ -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,