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",
|
Failed = "failed",
|
||||||
// Only used for the notification, never in the transaction history
|
// Only used for the notification, never in the transaction history
|
||||||
Deleted = "deleted",
|
Deleted = "deleted",
|
||||||
// Placeholder until D37 is fully implemented
|
|
||||||
Unknown = "unknown",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TransactionMinorState {
|
export enum TransactionMinorState {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
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
|
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
|
terms of the GNU General Public License as published by the Free Software
|
||||||
@ -40,7 +40,6 @@ import {
|
|||||||
CoinRefreshRequest,
|
CoinRefreshRequest,
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
ConfirmPayResultType,
|
ConfirmPayResultType,
|
||||||
constructPayUri,
|
|
||||||
ContractTermsUtil,
|
ContractTermsUtil,
|
||||||
Duration,
|
Duration,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
@ -63,6 +62,7 @@ import {
|
|||||||
randomBytes,
|
randomBytes,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
StartRefundQueryForUriResponse,
|
StartRefundQueryForUriResponse,
|
||||||
|
stringifyTalerUri,
|
||||||
TalerError,
|
TalerError,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
@ -197,16 +197,25 @@ async function failProposalPermanently(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
err: TalerErrorDetail,
|
err: TalerErrorDetail,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Payment,
|
||||||
|
proposalId,
|
||||||
|
});
|
||||||
|
const transitionInfo = await ws.db
|
||||||
.mktx((x) => [x.purchases])
|
.mktx((x) => [x.purchases])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const p = await tx.purchases.get(proposalId);
|
const p = await tx.purchases.get(proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// FIXME: We don't store the error detail here?!
|
||||||
|
const oldTxState = computePayMerchantTransactionState(p);
|
||||||
p.purchaseStatus = PurchaseStatus.FailedClaim;
|
p.purchaseStatus = PurchaseStatus.FailedClaim;
|
||||||
|
const newTxState = computePayMerchantTransactionState(p);
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
|
return { oldTxState, newTxState };
|
||||||
});
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProposalRequestTimeout(retryInfo?: RetryInfo): Duration {
|
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.
|
* 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(
|
export async function expectProposalDownload(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -314,10 +321,9 @@ export function extractContractData(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processDownloadProposal(
|
async function processDownloadProposal(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
options: object = {},
|
|
||||||
): Promise<OperationAttemptResult> {
|
): Promise<OperationAttemptResult> {
|
||||||
const proposal = await ws.db
|
const proposal = await ws.db
|
||||||
.mktx((x) => [x.purchases])
|
.mktx((x) => [x.purchases])
|
||||||
@ -339,6 +345,11 @@ export async function processDownloadProposal(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Payment,
|
||||||
|
proposalId,
|
||||||
|
});
|
||||||
|
|
||||||
const orderClaimUrl = new URL(
|
const orderClaimUrl = new URL(
|
||||||
`orders/${proposal.orderId}/claim`,
|
`orders/${proposal.orderId}/claim`,
|
||||||
proposal.merchantBaseUrl,
|
proposal.merchantBaseUrl,
|
||||||
@ -363,7 +374,8 @@ export async function processDownloadProposal(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: Do this in the background using the new return value
|
// 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),
|
timeout: getProposalRequestTimeout(retryRecord?.retryInfo),
|
||||||
});
|
});
|
||||||
const r = await readSuccessResponseJsonOrErrorCode(
|
const r = await readSuccessResponseJsonOrErrorCode(
|
||||||
@ -388,7 +400,7 @@ export async function processDownloadProposal(
|
|||||||
const proposalResp = r.response;
|
const proposalResp = r.response;
|
||||||
|
|
||||||
// The proposalResp contains the contract terms as raw JSON,
|
// 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.
|
// We need this raw JSON to compute the contract terms hash.
|
||||||
|
|
||||||
// FIXME: Do better error handling, check if the
|
// FIXME: Do better error handling, check if the
|
||||||
@ -496,7 +508,7 @@ export async function processDownloadProposal(
|
|||||||
|
|
||||||
logger.trace(`extracted contract data: ${j2s(contractData)}`);
|
logger.trace(`extracted contract data: ${j2s(contractData)}`);
|
||||||
|
|
||||||
await ws.db
|
const transitionInfo = await ws.db
|
||||||
.mktx((x) => [x.purchases, x.contractTerms])
|
.mktx((x) => [x.purchases, x.contractTerms])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const p = await tx.purchases.get(proposalId);
|
const p = await tx.purchases.get(proposalId);
|
||||||
@ -506,6 +518,7 @@ export async function processDownloadProposal(
|
|||||||
if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) {
|
if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const oldTxState = computePayMerchantTransactionState(p);
|
||||||
p.download = {
|
p.download = {
|
||||||
contractTermsHash,
|
contractTermsHash,
|
||||||
contractTermsMerchantSig: contractData.merchantSig,
|
contractTermsMerchantSig: contractData.merchantSig,
|
||||||
@ -523,18 +536,28 @@ export async function processDownloadProposal(
|
|||||||
) {
|
) {
|
||||||
const differentPurchase =
|
const differentPurchase =
|
||||||
await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
|
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) {
|
if (differentPurchase) {
|
||||||
logger.warn("repurchase detected");
|
logger.warn("repurchase detected");
|
||||||
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
|
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
|
||||||
p.repurchaseProposalId = differentPurchase.proposalId;
|
p.repurchaseProposalId = differentPurchase.proposalId;
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
p.purchaseStatus = PurchaseStatus.Proposed;
|
p.purchaseStatus = PurchaseStatus.Proposed;
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
|
}
|
||||||
|
const newTxState = computePayMerchantTransactionState(p);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
|
||||||
|
// FIXME: Deprecated pre-DD37 notification, remove eventually
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ProposalDownloaded,
|
type: NotificationType.ProposalDownloaded,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
@ -547,13 +570,11 @@ export async function processDownloadProposal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a proposal and store it in the database.
|
* Create a new purchase transaction if necessary. If a purchase
|
||||||
* Returns an id for it to retrieve it later.
|
* record for the provided arguments already exists,
|
||||||
*
|
* return the old proposal ID.
|
||||||
* @param sessionId Current session ID, if the proposal is being
|
|
||||||
* downloaded in the context of a session ID.
|
|
||||||
*/
|
*/
|
||||||
async function startDownloadProposal(
|
async function createPurchase(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
merchantBaseUrl: string,
|
merchantBaseUrl: string,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
@ -619,7 +640,7 @@ async function startDownloadProposal(
|
|||||||
posConfirmation: undefined,
|
posConfirmation: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ws.db
|
const transitionInfo = await ws.db
|
||||||
.mktx((x) => [x.purchases])
|
.mktx((x) => [x.purchases])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([
|
const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([
|
||||||
@ -628,11 +649,25 @@ async function startDownloadProposal(
|
|||||||
]);
|
]);
|
||||||
if (existingRecord) {
|
if (existingRecord) {
|
||||||
// Created concurrently
|
// Created concurrently
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
await tx.purchases.put(proposalRecord);
|
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);
|
await processDownloadProposal(ws, proposalId);
|
||||||
return proposalId;
|
return proposalId;
|
||||||
}
|
}
|
||||||
@ -643,8 +678,12 @@ async function storeFirstPaySuccess(
|
|||||||
sessionId: string | undefined,
|
sessionId: string | undefined,
|
||||||
payResponse: MerchantPayResponse,
|
payResponse: MerchantPayResponse,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Payment,
|
||||||
|
proposalId,
|
||||||
|
});
|
||||||
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
||||||
await ws.db
|
const transitionInfo = await ws.db
|
||||||
.mktx((x) => [x.purchases, x.contractTerms])
|
.mktx((x) => [x.purchases, x.contractTerms])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const purchase = await tx.purchases.get(proposalId);
|
const purchase = await tx.purchases.get(proposalId);
|
||||||
@ -658,6 +697,7 @@ async function storeFirstPaySuccess(
|
|||||||
logger.warn("payment success already stored");
|
logger.warn("payment success already stored");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const oldTxState = computePayMerchantTransactionState(purchase);
|
||||||
if (purchase.purchaseStatus === PurchaseStatus.Paying) {
|
if (purchase.purchaseStatus === PurchaseStatus.Paying) {
|
||||||
purchase.purchaseStatus = PurchaseStatus.Done;
|
purchase.purchaseStatus = PurchaseStatus.Done;
|
||||||
}
|
}
|
||||||
@ -686,7 +726,13 @@ async function storeFirstPaySuccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await tx.purchases.put(purchase);
|
await tx.purchases.put(purchase);
|
||||||
|
const newTxState = computePayMerchantTransactionState(purchase);
|
||||||
|
return {
|
||||||
|
oldTxState,
|
||||||
|
newTxState,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storePayReplaySuccess(
|
async function storePayReplaySuccess(
|
||||||
@ -694,7 +740,11 @@ async function storePayReplaySuccess(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
sessionId: string | undefined,
|
sessionId: string | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Payment,
|
||||||
|
proposalId,
|
||||||
|
});
|
||||||
|
const transitionInfo = await ws.db
|
||||||
.mktx((x) => [x.purchases])
|
.mktx((x) => [x.purchases])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const purchase = await tx.purchases.get(proposalId);
|
const purchase = await tx.purchases.get(proposalId);
|
||||||
@ -707,6 +757,7 @@ async function storePayReplaySuccess(
|
|||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
throw Error("invalid payment state");
|
throw Error("invalid payment state");
|
||||||
}
|
}
|
||||||
|
const oldTxState = computePayMerchantTransactionState(purchase);
|
||||||
if (
|
if (
|
||||||
purchase.purchaseStatus === PurchaseStatus.Paying ||
|
purchase.purchaseStatus === PurchaseStatus.Paying ||
|
||||||
purchase.purchaseStatus === PurchaseStatus.PayingReplay
|
purchase.purchaseStatus === PurchaseStatus.PayingReplay
|
||||||
@ -715,7 +766,10 @@ async function storePayReplaySuccess(
|
|||||||
}
|
}
|
||||||
purchase.lastSessionId = sessionId;
|
purchase.lastSessionId = sessionId;
|
||||||
await tx.purchases.put(purchase);
|
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(
|
export async function checkPaymentByProposalId(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -918,13 +976,14 @@ export async function checkPaymentByProposalId(
|
|||||||
proposalId,
|
proposalId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const talerUri = constructPayUri(
|
const talerUri = stringifyTalerUri({
|
||||||
proposal.merchantBaseUrl,
|
type: TalerUriAction.Pay,
|
||||||
proposal.orderId,
|
merchantBaseUrl: proposal.merchantBaseUrl,
|
||||||
proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
|
orderId: proposal.orderId,
|
||||||
proposal.claimToken,
|
sessionId: proposal.lastSessionId ?? proposal.downloadSessionId ?? "",
|
||||||
proposal.noncePriv,
|
claimToken: proposal.claimToken,
|
||||||
);
|
noncePriv: proposal.noncePriv,
|
||||||
|
});
|
||||||
|
|
||||||
// First check if we already paid for it.
|
// First check if we already paid for it.
|
||||||
const purchase = await ws.db
|
const purchase = await ws.db
|
||||||
@ -989,17 +1048,22 @@ export async function checkPaymentByProposalId(
|
|||||||
"automatically re-submitting payment with different session ID",
|
"automatically re-submitting payment with different session ID",
|
||||||
);
|
);
|
||||||
logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
|
logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
|
||||||
await ws.db
|
const transitionInfo = await ws.db
|
||||||
.mktx((x) => [x.purchases])
|
.mktx((x) => [x.purchases])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const p = await tx.purchases.get(proposalId);
|
const p = await tx.purchases.get(proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const oldTxState = computePayMerchantTransactionState(p);
|
||||||
p.lastSessionId = sessionId;
|
p.lastSessionId = sessionId;
|
||||||
p.purchaseStatus = PurchaseStatus.PayingReplay;
|
p.purchaseStatus = PurchaseStatus.PayingReplay;
|
||||||
await tx.purchases.put(p);
|
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 });
|
const r = await processPurchasePay(ws, proposalId, { forceNow: true });
|
||||||
if (r.type !== OperationAttemptResultType.Finished) {
|
if (r.type !== OperationAttemptResultType.Finished) {
|
||||||
// FIXME: This does not surface the original error
|
// 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,
|
ws,
|
||||||
uriResult.merchantBaseUrl,
|
uriResult.merchantBaseUrl,
|
||||||
uriResult.orderId,
|
uriResult.orderId,
|
||||||
|
Loading…
Reference in New Issue
Block a user