fix refunds
This commit is contained in:
parent
f67d7f54f9
commit
8115ac660c
@ -980,7 +980,7 @@ export enum PurchaseStatus {
|
||||
QueryRefund = "query-refund",
|
||||
ProcessRefund = "process-refund",
|
||||
Abort = "abort",
|
||||
Done = "done",
|
||||
Dormant = "dormant",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,12 +89,15 @@ export class MerchantBackendConnection {
|
||||
summary: string,
|
||||
fulfillmentUrl: string,
|
||||
): Promise<{ orderId: string }> {
|
||||
const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
|
||||
const reqUrl = new URL("order", this.merchantBaseUrl).href;
|
||||
const orderReq = {
|
||||
order: {
|
||||
amount,
|
||||
summary,
|
||||
fulfillment_url: fulfillmentUrl,
|
||||
refund_deadline: `/Date(${t})/`,
|
||||
wire_transfer_deadline: `/Date(${t})/`,
|
||||
},
|
||||
};
|
||||
const resp = await axios({
|
||||
|
@ -137,7 +137,7 @@ async function withWallet<T>(
|
||||
console.error("Operation failed: " + e.message);
|
||||
console.log("Hint: check pending operations for details.");
|
||||
} else {
|
||||
console.error("caught exception:", e);
|
||||
console.error("caught unhandled exception (bug?):", e);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
|
@ -52,8 +52,9 @@ export async function guardOperationException<T>(
|
||||
onOpError: (e: OperationError) => Promise<void>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return op();
|
||||
return await op();
|
||||
} catch (e) {
|
||||
console.log("guard: caught exception");
|
||||
if (e instanceof OperationFailedAndReportedError) {
|
||||
throw e;
|
||||
}
|
||||
@ -62,6 +63,7 @@ export async function guardOperationException<T>(
|
||||
throw new OperationFailedAndReportedError(e.message);
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
console.log("guard: caught Error");
|
||||
await onOpError({
|
||||
type: "exception",
|
||||
message: e.message,
|
||||
@ -69,6 +71,7 @@ export async function guardOperationException<T>(
|
||||
});
|
||||
throw new OperationFailedAndReportedError(e.message);
|
||||
}
|
||||
console.log("guard: caught something else");
|
||||
await onOpError({
|
||||
type: "exception",
|
||||
message: "non-error exception thrown",
|
||||
|
@ -365,6 +365,8 @@ async function recordConfirmPay(
|
||||
const p = await tx.get(Stores.proposals, proposal.proposalId);
|
||||
if (p) {
|
||||
p.proposalStatus = ProposalStatus.ACCEPTED;
|
||||
p.lastError = undefined;
|
||||
p.retryInfo = initRetryInfo(false);
|
||||
await tx.put(Stores.proposals, p);
|
||||
}
|
||||
await tx.put(Stores.purchases, t);
|
||||
@ -467,6 +469,7 @@ async function incrementPurchaseRetry(
|
||||
proposalId: string,
|
||||
err: OperationError | undefined,
|
||||
): Promise<void> {
|
||||
console.log("incrementing purchase retry with error", err);
|
||||
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
|
||||
const pr = await tx.get(Stores.purchases, proposalId);
|
||||
if (!pr) {
|
||||
@ -650,6 +653,8 @@ export async function submitPay(
|
||||
throw Error("merchant payment signature invalid");
|
||||
}
|
||||
purchase.finished = true;
|
||||
purchase.status = PurchaseStatus.Dormant;
|
||||
purchase.lastError = undefined;
|
||||
purchase.retryInfo = initRetryInfo(false);
|
||||
const modifiedCoins: CoinRecord[] = [];
|
||||
for (const pc of purchase.payReq.coins) {
|
||||
@ -992,6 +997,7 @@ async function submitRefundsToExchange(
|
||||
}
|
||||
const pendingKeys = Object.keys(purchase.refundsPending);
|
||||
if (pendingKeys.length === 0) {
|
||||
console.log("no pending refunds");
|
||||
return;
|
||||
}
|
||||
for (const pk of pendingKeys) {
|
||||
@ -1010,28 +1016,34 @@ async function submitRefundsToExchange(
|
||||
const exchangeUrl = purchase.payReq.coins[0].exchange_url;
|
||||
const reqUrl = new URL("refund", exchangeUrl);
|
||||
const resp = await ws.http.postJson(reqUrl.href, req);
|
||||
console.log("sent refund permission");
|
||||
if (resp.status !== 200) {
|
||||
console.error("refund failed", resp);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transactionally mark successful refunds as done
|
||||
const transformPurchase = (
|
||||
t: PurchaseRecord | undefined,
|
||||
): PurchaseRecord | undefined => {
|
||||
if (!t) {
|
||||
console.warn("purchase not found, not updating refund");
|
||||
let allRefundsProcessed = false;
|
||||
|
||||
await runWithWriteTransaction(
|
||||
ws.db,
|
||||
[Stores.purchases, Stores.coins],
|
||||
async tx => {
|
||||
const p = await tx.get(Stores.purchases, proposalId);
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
if (t.refundsPending[pk]) {
|
||||
t.refundsDone[pk] = t.refundsPending[pk];
|
||||
delete t.refundsPending[pk];
|
||||
if (p.refundsPending[pk]) {
|
||||
p.refundsDone[pk] = p.refundsPending[pk];
|
||||
delete p.refundsPending[pk];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
const transformCoin = (
|
||||
c: CoinRecord | undefined,
|
||||
): CoinRecord | undefined => {
|
||||
if (Object.keys(p.refundsPending).length === 0) {
|
||||
p.retryInfo = initRetryInfo();
|
||||
p.lastError = undefined;
|
||||
p.status = PurchaseStatus.Dormant;
|
||||
allRefundsProcessed = true;
|
||||
}
|
||||
await tx.put(Stores.purchases, p);
|
||||
const c = await tx.get(Stores.coins, perm.coin_pub);
|
||||
if (!c) {
|
||||
console.warn("coin not found, can't apply refund");
|
||||
return;
|
||||
@ -1041,19 +1053,15 @@ async function submitRefundsToExchange(
|
||||
c.status = CoinStatus.Dirty;
|
||||
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
||||
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
await runWithWriteTransaction(
|
||||
ws.db,
|
||||
[Stores.purchases, Stores.coins],
|
||||
async tx => {
|
||||
await tx.mutate(Stores.purchases, proposalId, transformPurchase);
|
||||
await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
|
||||
await tx.put(Stores.coins, c);
|
||||
},
|
||||
);
|
||||
refresh(ws, perm.coin_pub);
|
||||
if (allRefundsProcessed) {
|
||||
ws.notify({
|
||||
type: NotificationType.RefundFinished,
|
||||
})
|
||||
}
|
||||
await refresh(ws, perm.coin_pub);
|
||||
}
|
||||
|
||||
ws.notify({
|
||||
@ -1062,7 +1070,6 @@ async function submitRefundsToExchange(
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function acceptRefundResponse(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
@ -1086,6 +1093,8 @@ async function acceptRefundResponse(
|
||||
|
||||
t.lastRefundTimestamp = getTimestampNow();
|
||||
t.status = PurchaseStatus.ProcessRefund;
|
||||
t.lastError = undefined;
|
||||
t.retryInfo = initRetryInfo();
|
||||
|
||||
for (const perm of refundPermissions) {
|
||||
if (
|
||||
@ -1102,14 +1111,21 @@ async function acceptRefundResponse(
|
||||
await submitRefundsToExchange(ws, proposalId);
|
||||
}
|
||||
|
||||
|
||||
async function queryRefund(ws: InternalWalletState, proposalId: string): Promise<void> {
|
||||
async function queryRefund(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
|
||||
if (purchase?.status !== PurchaseStatus.QueryRefund) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refundUrl = new URL("refund", purchase.contractTerms.merchant_base_url).href
|
||||
const refundUrlObj = new URL(
|
||||
"refund",
|
||||
purchase.contractTerms.merchant_base_url,
|
||||
);
|
||||
refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
|
||||
const refundUrl = refundUrlObj.href;
|
||||
let resp;
|
||||
try {
|
||||
resp = await ws.http.get(refundUrl);
|
||||
@ -1122,22 +1138,45 @@ async function queryRefund(ws: InternalWalletState, proposalId: string): Promise
|
||||
await acceptRefundResponse(ws, proposalId, refundResponse);
|
||||
}
|
||||
|
||||
async function startRefundQuery(ws: InternalWalletState, proposalId: string): Promise<void> {
|
||||
const success = await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
|
||||
async function startRefundQuery(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
const success = await runWithWriteTransaction(
|
||||
ws.db,
|
||||
[Stores.purchases],
|
||||
async tx => {
|
||||
const p = await tx.get(Stores.purchases, proposalId);
|
||||
if (p?.status !== PurchaseStatus.Done) {
|
||||
if (!p) {
|
||||
console.log("no purchase found for refund URL");
|
||||
return false;
|
||||
}
|
||||
p.status = PurchaseStatus.QueryRefund;
|
||||
if (p.status === PurchaseStatus.QueryRefund) {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (p.status === PurchaseStatus.ProcessRefund) {
|
||||
return true;
|
||||
}
|
||||
if (p.status !== PurchaseStatus.Dormant) {
|
||||
console.log(
|
||||
`can't apply refund, as payment isn't done (status ${p.status})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
p.lastError = undefined;
|
||||
p.status = PurchaseStatus.QueryRefund;
|
||||
p.retryInfo = initRetryInfo();
|
||||
await tx.put(Stores.purchases, p);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
await queryRefund(ws, proposalId);
|
||||
}
|
||||
|
||||
await processPurchase(ws, proposalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a refund, return the contract hash for the contract
|
||||
@ -1149,6 +1188,8 @@ export async function applyRefund(
|
||||
): Promise<string> {
|
||||
const parseResult = parseRefundUri(talerRefundUri);
|
||||
|
||||
console.log("applying refund");
|
||||
|
||||
if (!parseResult) {
|
||||
throw Error("invalid refund URI");
|
||||
}
|
||||
@ -1163,6 +1204,7 @@ export async function applyRefund(
|
||||
throw Error("no purchase for the taler://refund/ URI was found");
|
||||
}
|
||||
|
||||
console.log("processing purchase for refund");
|
||||
await startRefundQuery(ws, purchase.proposalId);
|
||||
|
||||
return purchase.contractTermsHash;
|
||||
@ -1180,7 +1222,7 @@ export async function processPurchase(
|
||||
);
|
||||
}
|
||||
|
||||
export async function processPurchaseImpl(
|
||||
async function processPurchaseImpl(
|
||||
ws: InternalWalletState,
|
||||
proposalId: string,
|
||||
): Promise<void> {
|
||||
@ -1188,8 +1230,9 @@ export async function processPurchaseImpl(
|
||||
if (!purchase) {
|
||||
return;
|
||||
}
|
||||
logger.trace(`processing purchase ${proposalId}`);
|
||||
switch (purchase.status) {
|
||||
case PurchaseStatus.Done:
|
||||
case PurchaseStatus.Dormant:
|
||||
return;
|
||||
case PurchaseStatus.Abort:
|
||||
// FIXME
|
||||
@ -1200,7 +1243,9 @@ export async function processPurchaseImpl(
|
||||
await queryRefund(ws, proposalId);
|
||||
break;
|
||||
case PurchaseStatus.ProcessRefund:
|
||||
console.log("submitting refunds to exchange (toplvl)");
|
||||
await submitRefundsToExchange(ws, proposalId);
|
||||
console.log("after submitting refunds to exchange (toplvl)");
|
||||
break;
|
||||
default:
|
||||
throw assertUnreachable(purchase.status);
|
||||
|
@ -32,7 +32,9 @@ import {
|
||||
ReserveRecordStatus,
|
||||
CoinStatus,
|
||||
ProposalStatus,
|
||||
PurchaseStatus,
|
||||
} from "../dbTypes";
|
||||
import { assertUnreachable } from "../util/assertUnreachable";
|
||||
|
||||
function updateRetryDelay(
|
||||
oldDelay: Duration,
|
||||
@ -353,7 +355,7 @@ async function gatherPurchasePending(
|
||||
onlyDue: boolean = false,
|
||||
): Promise<void> {
|
||||
await tx.iter(Stores.purchases).forEach((pr) => {
|
||||
if (pr.finished) {
|
||||
if (pr.status === PurchaseStatus.Dormant) {
|
||||
return;
|
||||
}
|
||||
resp.nextRetryDelay = updateRetryDelay(
|
||||
@ -369,6 +371,9 @@ async function gatherPurchasePending(
|
||||
givesLifeness: true,
|
||||
isReplay: false,
|
||||
proposalId: pr.proposalId,
|
||||
status: pr.status,
|
||||
retryInfo: pr.retryInfo,
|
||||
lastError: pr.lastError,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -282,6 +282,7 @@ async function processPlanchet(
|
||||
}
|
||||
if (numDone === ws.denoms.length) {
|
||||
ws.finishTimestamp = getTimestampNow();
|
||||
ws.lastError = undefined;
|
||||
ws.retryInfo = initRetryInfo(false);
|
||||
withdrawSessionFinished = true;
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ import {
|
||||
processDownloadProposal,
|
||||
applyRefund,
|
||||
getFullRefundFees,
|
||||
processPurchaseImpl,
|
||||
processPurchase,
|
||||
} from "./wallet-impl/pay";
|
||||
|
||||
import {
|
||||
@ -180,6 +180,7 @@ export class Wallet {
|
||||
pending: PendingOperationInfo,
|
||||
forceNow: boolean = false,
|
||||
): Promise<void> {
|
||||
console.log("running pending", pending);
|
||||
switch (pending.type) {
|
||||
case "bug":
|
||||
// Nothing to do, will just be displayed to the user
|
||||
@ -209,7 +210,7 @@ export class Wallet {
|
||||
await processTip(this.ws, pending.tipId);
|
||||
break;
|
||||
case "pay":
|
||||
await processPurchaseImpl(this.ws, pending.proposalId);
|
||||
await processPurchase(this.ws, pending.proposalId);
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(pending);
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
ExchangeWireInfo,
|
||||
WithdrawalSource,
|
||||
RetryInfo,
|
||||
PurchaseStatus,
|
||||
} from "./dbTypes";
|
||||
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
|
||||
|
||||
@ -520,6 +521,7 @@ export const enum NotificationType {
|
||||
ReserveDepleted = "reserve-depleted",
|
||||
WithdrawSessionFinished = "withdraw-session-finished",
|
||||
WaitingForRetry = "waiting-for-retry",
|
||||
RefundFinished = "refund-finished",
|
||||
}
|
||||
|
||||
export interface ProposalAcceptedNotification {
|
||||
@ -585,6 +587,10 @@ export interface WaitingForRetryNotification {
|
||||
numGivingLiveness: number;
|
||||
}
|
||||
|
||||
export interface RefundFinishedNotification {
|
||||
type: NotificationType.RefundFinished;
|
||||
}
|
||||
|
||||
export type WalletNotification =
|
||||
| ProposalAcceptedNotification
|
||||
| ProposalDownloadedNotification
|
||||
@ -599,7 +605,8 @@ export type WalletNotification =
|
||||
| ReserveConfirmedNotification
|
||||
| WithdrawSessionFinishedNotification
|
||||
| ReserveDepletedNotification
|
||||
| WaitingForRetryNotification;
|
||||
| WaitingForRetryNotification
|
||||
| RefundFinishedNotification;
|
||||
|
||||
export interface OperationError {
|
||||
type: string;
|
||||
@ -612,7 +619,7 @@ export interface PendingExchangeUpdateOperation {
|
||||
stage: string;
|
||||
reason: string;
|
||||
exchangeBaseUrl: string;
|
||||
lastError?: OperationError;
|
||||
lastError: OperationError | undefined;
|
||||
}
|
||||
|
||||
export interface PendingBugOperation {
|
||||
@ -674,6 +681,9 @@ export interface PendingPayOperation {
|
||||
type: "pay";
|
||||
proposalId: string;
|
||||
isReplay: boolean;
|
||||
status: PurchaseStatus;
|
||||
retryInfo: RetryInfo,
|
||||
lastError: OperationError | undefined;
|
||||
}
|
||||
|
||||
export interface PendingOperationInfoCommon {
|
||||
|
Loading…
Reference in New Issue
Block a user