implement freezing for payments

This commit is contained in:
Florian Dold 2021-08-24 15:08:34 +02:00
parent 408d8e9fc8
commit a09359bd39
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 45 additions and 8 deletions

View File

@ -80,6 +80,12 @@ export interface TransactionCommon {
// but its transactionId will remain unchanged // but its transactionId will remain unchanged
pending: boolean; pending: boolean;
/**
* True if the transaction encountered a problem that might be
* permanent. A frozen transaction won't be automatically retried.
*/
frozen: boolean;
// Raw amount of the transaction (exclusive of fees or other extra costs) // Raw amount of the transaction (exclusive of fees or other extra costs)
amountRaw: AmountString; amountRaw: AmountString;

View File

@ -112,13 +112,6 @@ export async function runDenomUnofferedTest(t: GlobalTestState) {
merchantErrorCode, merchantErrorCode,
TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND, TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,
); );
const purchId = makeEventId(TransactionType.Payment, preparePayResult.proposalId);
await wallet.client.call(WalletApiOperation.DeleteTransaction, {
transactionId: purchId,
});
// Now, delete the purchase and refresh operation.
} }
await wallet.client.call(WalletApiOperation.AddExchange, { await wallet.client.call(WalletApiOperation.AddExchange, {

View File

@ -1276,6 +1276,12 @@ export interface PurchaseRecord {
* Continue querying the refund status until this deadline has expired. * Continue querying the refund status until this deadline has expired.
*/ */
autoRefundDeadline: Timestamp | undefined; autoRefundDeadline: Timestamp | undefined;
/**
* Is the payment frozen? I.e. did we encounter
* an error where it doesn't make sense to retry.
*/
payFrozen?: boolean;
} }
export const WALLET_BACKUP_STATE_KEY = "walletBackupState"; export const WALLET_BACKUP_STATE_KEY = "walletBackupState";

View File

@ -93,6 +93,7 @@ import {
readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
readTalerErrorResponse, readTalerErrorResponse,
readUnexpectedResponseDetails,
throwUnexpectedRequestError, throwUnexpectedRequestError,
} from "../util/http.js"; } from "../util/http.js";
import { import {
@ -1209,6 +1210,26 @@ async function submitPay(
}; };
} }
if (resp.status === HttpResponseStatus.BadRequest) {
const errDetails = await readUnexpectedResponseDetails(resp);
logger.warn("unexpected 400 response for /pay");
logger.warn(j2s(errDetails));
await ws.db
.mktx((x) => ({ purchases: x.purchases }))
.runReadWrite(async (tx) => {
const purch = await tx.purchases.get(proposalId);
if (!purch) {
return;
}
purch.payFrozen = true;
purch.lastPayError = errDetails;
delete purch.payRetryInfo;
await tx.purchases.put(purch);
});
// FIXME: Maybe introduce a new return type for this instead of throwing?
throw new OperationFailedAndReportedError(errDetails);
}
if (resp.status === HttpResponseStatus.Conflict) { if (resp.status === HttpResponseStatus.Conflict) {
const err = await readTalerErrorResponse(resp); const err = await readTalerErrorResponse(resp);
if ( if (

View File

@ -235,7 +235,11 @@ async function gatherPurchasePending(
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
await tx.purchases.iter().forEach((pr) => { await tx.purchases.iter().forEach((pr) => {
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) { if (
pr.paymentSubmitPending &&
pr.abortStatus === AbortStatus.None &&
!pr.payFrozen
) {
const timestampDue = pr.payRetryInfo?.nextRetry ?? getTimestampNow(); const timestampDue = pr.payRetryInfo?.nextRetry ?? getTimestampNow();
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingTaskType.Pay, type: PendingTaskType.Pay,

View File

@ -168,6 +168,7 @@ export async function getTransactions(
TransactionType.Withdrawal, TransactionType.Withdrawal,
wsr.withdrawalGroupId, wsr.withdrawalGroupId,
), ),
frozen: false,
...(wsr.lastError ? { error: wsr.lastError } : {}), ...(wsr.lastError ? { error: wsr.lastError } : {}),
}); });
}); });
@ -215,6 +216,7 @@ export async function getTransactions(
TransactionType.Withdrawal, TransactionType.Withdrawal,
r.initialWithdrawalGroupId, r.initialWithdrawalGroupId,
), ),
frozen: false,
...(r.lastError ? { error: r.lastError } : {}), ...(r.lastError ? { error: r.lastError } : {}),
}); });
}); });
@ -230,6 +232,7 @@ export async function getTransactions(
amountRaw: Amounts.stringify(dg.effectiveDepositAmount), amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
amountEffective: Amounts.stringify(dg.totalPayCost), amountEffective: Amounts.stringify(dg.totalPayCost),
pending: !dg.timestampFinished, pending: !dg.timestampFinished,
frozen: false,
timestamp: dg.timestampCreated, timestamp: dg.timestampCreated,
targetPaytoUri: dg.wire.payto_uri, targetPaytoUri: dg.wire.payto_uri,
transactionId: makeEventId( transactionId: makeEventId(
@ -288,6 +291,7 @@ export async function getTransactions(
transactionId: paymentTransactionId, transactionId: paymentTransactionId,
proposalId: pr.proposalId, proposalId: pr.proposalId,
info: info, info: info,
frozen: pr.payFrozen ?? false,
...(err ? { error: err } : {}), ...(err ? { error: err } : {}),
}); });
@ -351,6 +355,7 @@ export async function getTransactions(
amountEffective: Amounts.stringify(amountEffective), amountEffective: Amounts.stringify(amountEffective),
amountRaw: Amounts.stringify(amountRaw), amountRaw: Amounts.stringify(amountRaw),
pending: false, pending: false,
frozen: false,
}); });
} }
}); });
@ -372,6 +377,7 @@ export async function getTransactions(
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), amountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
pending: !tipRecord.pickedUpTimestamp, pending: !tipRecord.pickedUpTimestamp,
frozen: false,
timestamp: tipRecord.acceptedTimestamp, timestamp: tipRecord.acceptedTimestamp,
transactionId: makeEventId( transactionId: makeEventId(
TransactionType.Tip, TransactionType.Tip,

View File

@ -64,6 +64,7 @@ export enum HttpResponseStatus {
NoContent = 204, NoContent = 204,
Gone = 210, Gone = 210,
NotModified = 304, NotModified = 304,
BadRequest = 400,
PaymentRequired = 402, PaymentRequired = 402,
NotFound = 404, NotFound = 404,
Conflict = 409, Conflict = 409,