wallet-core: DD37 fixes and FIXME comments for merchant payments
This commit is contained in:
parent
4859883c9a
commit
0406160869
@ -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 {
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user