wallet-core/packages/taler-wallet-core/src/operations/refund.ts

778 lines
21 KiB
TypeScript
Raw Normal View History

2019-12-15 19:08:07 +01:00
/*
This file is part of GNU Taler
2020-01-19 17:14:43 +01:00
(C) 2019-2019 Taler Systems S.A.
2019-12-15 19:08:07 +01:00
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
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Implementation of the refund operation.
*
* @author Florian Dold
*/
/**
* Imports.
*/
import {
2021-03-17 17:56:37 +01:00
AbortingCoin,
AbortRequest,
AmountJson,
Amounts,
ApplyRefundResponse,
codecForAbortResponse,
codecForMerchantOrderRefundPickupResponse,
CoinPublicKey,
getTimestampNow,
Logger,
2021-03-17 17:56:37 +01:00
MerchantCoinRefundFailureStatus,
MerchantCoinRefundStatus,
MerchantCoinRefundSuccessStatus,
NotificationType,
parseRefundUri,
RefreshReason,
TalerErrorCode,
TalerErrorDetails,
URL,
timestampAddDuration,
codecForMerchantOrderStatusPaid,
isTimestampExpired,
2021-03-17 17:56:37 +01:00
} from "@gnu-taler/taler-util";
2021-06-09 15:14:17 +02:00
import {
AbortStatus,
CoinStatus,
PurchaseRecord,
2021-06-09 15:14:17 +02:00
RefundReason,
RefundState,
2021-06-09 15:14:17 +02:00
WalletStoresV1,
} from "../db.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { checkDbInvariant } from "../util/invariants.js";
2021-06-09 15:14:17 +02:00
import { GetReadWriteAccess } from "../util/query.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { guardOperationException } from "../errors.js";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
import { InternalWalletState } from "../common.js";
2020-01-19 20:41:51 +01:00
const logger = new Logger("refund.ts");
2019-12-15 19:08:07 +01:00
/**
* Retry querying and applying refunds for an order later.
*/
2019-12-15 19:08:07 +01:00
async function incrementPurchaseQueryRefundRetry(
ws: InternalWalletState,
proposalId: string,
2020-09-01 14:57:22 +02:00
err: TalerErrorDetails | undefined,
2019-12-15 19:08:07 +01:00
): Promise<void> {
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const pr = await tx.purchases.get(proposalId);
if (!pr) {
return;
}
if (!pr.refundStatusRetryInfo) {
return;
}
pr.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(pr.refundStatusRetryInfo);
pr.lastRefundStatusError = err;
await tx.purchases.put(pr);
});
if (err) {
ws.notify({
type: NotificationType.RefundStatusOperationError,
error: err,
});
2019-12-15 19:08:07 +01:00
}
}
2020-07-23 14:05:17 +02:00
function getRefundKey(d: MerchantCoinRefundStatus): string {
2020-05-15 19:24:39 +02:00
return `${d.coin_pub}-${d.rtransaction_id}`;
2020-04-27 17:41:20 +02:00
}
2020-07-23 14:05:17 +02:00
async function applySuccessfulRefund(
2021-06-09 15:14:17 +02:00
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
2020-07-23 14:05:17 +02:00
p: PurchaseRecord,
refreshCoinsMap: Record<string, { coinPub: string }>,
r: MerchantCoinRefundSuccessStatus,
2019-12-15 19:08:07 +01:00
): Promise<void> {
2020-07-23 14:05:17 +02:00
// FIXME: check signature before storing it as valid!
2019-12-15 19:08:07 +01:00
2020-07-23 14:05:17 +02:00
const refundKey = getRefundKey(r);
2021-06-09 15:14:17 +02:00
const coin = await tx.coins.get(r.coin_pub);
2020-07-23 14:05:17 +02:00
if (!coin) {
logger.warn("coin not found, can't apply refund");
2020-07-23 14:05:17 +02:00
return;
}
2021-06-09 15:14:17 +02:00
const denom = await tx.denominations.get([
2020-09-08 17:33:10 +02:00
coin.exchangeBaseUrl,
2020-07-23 14:05:17 +02:00
coin.denomPubHash,
2020-09-08 17:33:10 +02:00
]);
2020-07-23 14:05:17 +02:00
if (!denom) {
throw Error("inconsistent database");
}
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
const refundAmount = Amounts.parseOrThrow(r.refund_amount);
const refundFee = denom.feeRefund;
coin.status = CoinStatus.Dormant;
coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`);
2021-06-09 15:14:17 +02:00
await tx.coins.put(coin);
2020-07-23 14:05:17 +02:00
2021-06-09 15:14:17 +02:00
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
2020-07-23 14:05:17 +02:00
.toArray();
const amountLeft = Amounts.sub(
Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
.amount,
denom.feeRefund,
).amount;
const totalRefreshCostBound = getTotalRefreshCost(
allDenoms,
denom,
amountLeft,
);
2020-04-27 17:41:20 +02:00
2020-07-23 14:05:17 +02:00
p.refunds[refundKey] = {
type: RefundState.Applied,
obtainedTime: getTimestampNow(),
2020-07-23 14:05:17 +02:00
executionTime: r.execution_time,
refundAmount: Amounts.parseOrThrow(r.refund_amount),
refundFee: denom.feeRefund,
totalRefreshCostBound,
coinPub: r.coin_pub,
rtransactionId: r.rtransaction_id,
2020-07-23 14:05:17 +02:00
};
}
2020-05-15 19:24:39 +02:00
2020-07-23 14:05:17 +02:00
async function storePendingRefund(
2021-06-09 15:14:17 +02:00
tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
coins: typeof WalletStoresV1.coins;
}>,
2020-07-23 14:05:17 +02:00
p: PurchaseRecord,
r: MerchantCoinRefundFailureStatus,
): Promise<void> {
const refundKey = getRefundKey(r);
2021-06-09 15:14:17 +02:00
const coin = await tx.coins.get(r.coin_pub);
2020-07-23 14:05:17 +02:00
if (!coin) {
logger.warn("coin not found, can't apply refund");
2020-07-23 14:05:17 +02:00
return;
2020-04-27 17:41:20 +02:00
}
2021-06-09 15:14:17 +02:00
const denom = await tx.denominations.get([
2020-09-08 17:33:10 +02:00
coin.exchangeBaseUrl,
2020-07-23 14:05:17 +02:00
coin.denomPubHash,
2020-09-08 17:33:10 +02:00
]);
2019-12-15 19:08:07 +01:00
2020-07-23 14:05:17 +02:00
if (!denom) {
throw Error("inconsistent database");
}
2021-06-09 15:14:17 +02:00
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
2020-07-23 14:05:17 +02:00
.toArray();
const amountLeft = Amounts.sub(
Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
.amount,
denom.feeRefund,
).amount;
const totalRefreshCostBound = getTotalRefreshCost(
allDenoms,
denom,
amountLeft,
);
p.refunds[refundKey] = {
type: RefundState.Pending,
obtainedTime: getTimestampNow(),
2020-07-23 14:05:17 +02:00
executionTime: r.execution_time,
refundAmount: Amounts.parseOrThrow(r.refund_amount),
refundFee: denom.feeRefund,
totalRefreshCostBound,
coinPub: r.coin_pub,
rtransactionId: r.rtransaction_id,
2020-07-23 14:05:17 +02:00
};
}
2020-09-06 14:47:12 +02:00
async function storeFailedRefund(
2021-06-09 15:14:17 +02:00
tx: GetReadWriteAccess<{
coins: typeof WalletStoresV1.coins;
denominations: typeof WalletStoresV1.denominations;
}>,
2020-09-06 14:47:12 +02:00
p: PurchaseRecord,
refreshCoinsMap: Record<string, { coinPub: string }>,
2020-09-06 14:47:12 +02:00
r: MerchantCoinRefundFailureStatus,
): Promise<void> {
const refundKey = getRefundKey(r);
2021-06-09 15:14:17 +02:00
const coin = await tx.coins.get(r.coin_pub);
2020-09-06 14:47:12 +02:00
if (!coin) {
logger.warn("coin not found, can't apply refund");
2020-09-06 14:47:12 +02:00
return;
}
2021-06-09 15:14:17 +02:00
const denom = await tx.denominations.get([
2020-09-08 17:33:10 +02:00
coin.exchangeBaseUrl,
2020-09-06 14:47:12 +02:00
coin.denomPubHash,
2020-09-08 17:33:10 +02:00
]);
2020-09-06 14:47:12 +02:00
if (!denom) {
throw Error("inconsistent database");
}
2021-06-09 15:14:17 +02:00
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
2020-09-06 14:47:12 +02:00
.toArray();
const amountLeft = Amounts.sub(
Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
.amount,
denom.feeRefund,
).amount;
const totalRefreshCostBound = getTotalRefreshCost(
allDenoms,
denom,
amountLeft,
);
p.refunds[refundKey] = {
type: RefundState.Failed,
obtainedTime: getTimestampNow(),
executionTime: r.execution_time,
refundAmount: Amounts.parseOrThrow(r.refund_amount),
refundFee: denom.feeRefund,
totalRefreshCostBound,
coinPub: r.coin_pub,
rtransactionId: r.rtransaction_id,
2020-09-06 14:47:12 +02:00
};
if (p.abortStatus === AbortStatus.AbortRefund) {
// Refund failed because the merchant didn't even try to deposit
// the coin yet, so we try to refresh.
2020-11-08 01:20:50 +01:00
if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
2021-06-09 15:14:17 +02:00
const coin = await tx.coins.get(r.coin_pub);
if (!coin) {
logger.warn("coin not found, can't apply refund");
return;
}
2021-06-09 15:14:17 +02:00
const denom = await tx.denominations.get([
coin.exchangeBaseUrl,
coin.denomPubHash,
]);
if (!denom) {
logger.warn("denomination for coin missing");
return;
}
let contrib: AmountJson | undefined;
for (let i = 0; i < p.payCoinSelection.coinPubs.length; i++) {
if (p.payCoinSelection.coinPubs[i] === r.coin_pub) {
contrib = p.payCoinSelection.coinContributions[i];
}
}
if (contrib) {
coin.currentAmount = Amounts.add(coin.currentAmount, contrib).amount;
2020-12-14 16:45:15 +01:00
coin.currentAmount = Amounts.sub(
coin.currentAmount,
denom.feeRefund,
).amount;
}
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
2021-06-09 15:14:17 +02:00
await tx.coins.put(coin);
}
}
2020-09-06 14:47:12 +02:00
}
2020-07-23 14:05:17 +02:00
async function acceptRefunds(
ws: InternalWalletState,
proposalId: string,
refunds: MerchantCoinRefundStatus[],
reason: RefundReason,
): Promise<void> {
logger.trace("handling refunds", refunds);
const now = getTimestampNow();
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => ({
purchases: x.purchases,
coins: x.coins,
denominations: x.denominations,
refreshGroups: x.refreshGroups,
}))
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
2020-04-27 17:41:20 +02:00
if (!p) {
logger.error("purchase not found, not adding refunds");
2020-04-27 17:41:20 +02:00
return;
}
2020-07-23 14:05:17 +02:00
const refreshCoinsMap: Record<string, CoinPublicKey> = {};
2020-04-27 17:41:20 +02:00
2020-07-23 14:05:17 +02:00
for (const refundStatus of refunds) {
const refundKey = getRefundKey(refundStatus);
const existingRefundInfo = p.refunds[refundKey];
2020-09-06 14:47:12 +02:00
const isPermanentFailure =
refundStatus.type === "failure" &&
2020-12-14 16:45:15 +01:00
refundStatus.exchange_status >= 400 &&
refundStatus.exchange_status < 500;
2020-09-06 14:47:12 +02:00
2020-07-23 14:05:17 +02:00
// Already failed.
if (existingRefundInfo?.type === RefundState.Failed) {
2020-04-27 17:41:20 +02:00
continue;
}
2020-07-23 14:05:17 +02:00
// Already applied.
if (existingRefundInfo?.type === RefundState.Applied) {
continue;
2020-04-27 17:41:20 +02:00
}
2020-07-23 14:05:17 +02:00
// Still pending.
if (
2020-09-08 17:33:10 +02:00
refundStatus.type === "failure" &&
!isPermanentFailure &&
2020-07-23 14:05:17 +02:00
existingRefundInfo?.type === RefundState.Pending
) {
2020-04-27 17:41:20 +02:00
continue;
}
2020-07-23 14:05:17 +02:00
// Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
2020-04-27 17:41:20 +02:00
2020-07-24 11:12:35 +02:00
if (refundStatus.type === "success") {
2020-07-23 14:05:17 +02:00
await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
2020-09-06 14:47:12 +02:00
} else if (isPermanentFailure) {
await storeFailedRefund(tx, p, refreshCoinsMap, refundStatus);
2020-07-23 14:05:17 +02:00
} else {
await storePendingRefund(tx, p, refundStatus);
2020-04-27 17:41:20 +02:00
}
2019-12-15 19:08:07 +01:00
}
2020-07-23 14:05:17 +02:00
const refreshCoinsPubs = Object.values(refreshCoinsMap);
if (refreshCoinsPubs.length > 0) {
2020-09-01 15:37:14 +02:00
await createRefreshGroup(
ws,
tx,
refreshCoinsPubs,
RefreshReason.Refund,
);
}
2020-07-23 14:05:17 +02:00
2020-04-27 17:41:20 +02:00
// Are we done with querying yet, or do we need to do another round
// after a retry delay?
let queryDone = true;
2019-12-15 19:08:07 +01:00
if (
p.timestampFirstSuccessfulPay &&
p.autoRefundDeadline &&
p.autoRefundDeadline.t_ms > now.t_ms
) {
2020-07-23 14:05:17 +02:00
queryDone = false;
}
2020-05-15 19:24:39 +02:00
2020-07-23 14:05:17 +02:00
let numPendingRefunds = 0;
for (const ri of Object.values(p.refunds)) {
switch (ri.type) {
case RefundState.Pending:
numPendingRefunds++;
break;
2020-04-27 17:41:20 +02:00
}
}
2020-07-23 14:05:17 +02:00
if (numPendingRefunds > 0) {
2019-12-15 19:08:07 +01:00
queryDone = false;
}
2020-04-27 17:41:20 +02:00
if (queryDone) {
p.timestampLastRefundStatus = now;
2020-04-27 17:41:20 +02:00
p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo(false);
p.refundQueryRequested = false;
if (p.abortStatus === AbortStatus.AbortRefund) {
p.abortStatus = AbortStatus.AbortFinished;
}
logger.trace("refund query done");
2020-04-27 17:41:20 +02:00
} else {
// No error, but we need to try again!
p.timestampLastRefundStatus = now;
2020-04-27 17:41:20 +02:00
p.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(p.refundStatusRetryInfo);
p.lastRefundStatusError = undefined;
logger.trace("refund query not done");
2020-04-27 17:41:20 +02:00
}
2019-12-15 19:08:07 +01:00
2021-06-09 15:14:17 +02:00
await tx.purchases.put(p);
});
2019-12-15 19:08:07 +01:00
ws.notify({
type: NotificationType.RefundQueried,
});
}
2020-08-11 14:25:45 +02:00
/**
* Summary of the refund status of a purchase.
*/
export interface RefundSummary {
pendingAtExchange: boolean;
amountEffectivePaid: AmountJson;
amountRefundGranted: AmountJson;
amountRefundGone: AmountJson;
}
2019-12-15 19:08:07 +01:00
/**
* Accept a refund, return the contract hash for the contract
* that was involved in the refund.
*/
export async function applyRefund(
ws: InternalWalletState,
talerRefundUri: string,
2020-08-11 14:25:45 +02:00
): Promise<ApplyRefundResponse> {
2019-12-15 19:08:07 +01:00
const parseResult = parseRefundUri(talerRefundUri);
logger.trace("applying refund", parseResult);
2019-12-15 19:08:07 +01:00
if (!parseResult) {
throw Error("invalid refund URI");
}
2021-06-09 15:14:17 +02:00
let purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
parseResult.merchantBaseUrl,
parseResult.orderId,
]);
});
2019-12-15 19:08:07 +01:00
if (!purchase) {
2020-03-30 12:34:16 +02:00
throw Error(
`no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
);
2019-12-15 19:08:07 +01:00
}
const proposalId = purchase.proposalId;
2020-03-30 12:42:28 +02:00
logger.info("processing purchase for refund");
2021-06-09 15:14:17 +02:00
const success = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId);
if (!p) {
logger.error("no purchase found for refund URL");
return false;
}
p.refundQueryRequested = true;
p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo();
2021-06-09 15:14:17 +02:00
await tx.purchases.put(p);
return true;
2021-06-09 15:14:17 +02:00
});
if (success) {
ws.notify({
type: NotificationType.RefundStarted,
});
await processPurchaseQueryRefundImpl(ws, proposalId, true, false);
}
2019-12-15 19:08:07 +01:00
2021-06-09 15:14:17 +02:00
purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
2020-08-11 14:25:45 +02:00
if (!purchase) {
throw Error("purchase no longer exists");
}
const p = purchase;
let amountRefundGranted = Amounts.getZero(
2021-01-04 13:30:38 +01:00
purchase.download.contractData.amount.currency,
2020-08-11 14:25:45 +02:00
);
2021-01-13 00:51:30 +01:00
let amountRefundGone = Amounts.getZero(
purchase.download.contractData.amount.currency,
);
2020-08-11 14:25:45 +02:00
let pendingAtExchange = false;
Object.keys(purchase.refunds).forEach((rk) => {
const refund = p.refunds[rk];
if (refund.type === RefundState.Pending) {
pendingAtExchange = true;
}
if (
refund.type === RefundState.Applied ||
refund.type === RefundState.Pending
) {
amountRefundGranted = Amounts.add(
amountRefundGranted,
Amounts.sub(
refund.refundAmount,
refund.refundFee,
refund.totalRefreshCostBound,
).amount,
).amount;
} else {
2020-08-12 09:11:00 +02:00
amountRefundGone = Amounts.add(amountRefundGone, refund.refundAmount)
.amount;
2020-08-11 14:25:45 +02:00
}
});
return {
2021-01-04 13:30:38 +01:00
contractTermsHash: purchase.download.contractData.contractTermsHash,
proposalId: purchase.proposalId,
2020-09-08 17:15:33 +02:00
amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
2020-08-11 14:25:45 +02:00
amountRefundGone: Amounts.stringify(amountRefundGone),
amountRefundGranted: Amounts.stringify(amountRefundGranted),
pendingAtExchange,
info: {
2021-01-04 13:30:38 +01:00
contractTermsHash: purchase.download.contractData.contractTermsHash,
merchant: purchase.download.contractData.merchant,
orderId: purchase.download.contractData.orderId,
products: purchase.download.contractData.products,
summary: purchase.download.contractData.summary,
fulfillmentMessage: purchase.download.contractData.fulfillmentMessage,
summary_i18n: purchase.download.contractData.summaryI18n,
2021-01-13 00:51:30 +01:00
fulfillmentMessage_i18n:
purchase.download.contractData.fulfillmentMessageI18n,
2020-12-14 16:45:15 +01:00
},
};
2019-12-15 19:08:07 +01:00
}
export async function processPurchaseQueryRefund(
ws: InternalWalletState,
proposalId: string,
2020-04-06 17:45:41 +02:00
forceNow = false,
2019-12-15 19:08:07 +01:00
): Promise<void> {
2020-09-01 14:57:22 +02:00
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
2019-12-15 19:08:07 +01:00
incrementPurchaseQueryRefundRetry(ws, proposalId, e);
await guardOperationException(
() => processPurchaseQueryRefundImpl(ws, proposalId, forceNow, true),
2019-12-15 19:08:07 +01:00
onOpErr,
);
}
async function resetPurchaseQueryRefundRetry(
ws: InternalWalletState,
proposalId: string,
2020-04-06 20:02:01 +02:00
): Promise<void> {
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const x = await tx.purchases.get(proposalId);
2021-06-11 11:15:08 +02:00
if (x) {
2021-06-09 15:14:17 +02:00
x.refundStatusRetryInfo = initRetryInfo();
await tx.purchases.put(x);
}
});
2019-12-15 19:08:07 +01:00
}
async function processPurchaseQueryRefundImpl(
ws: InternalWalletState,
proposalId: string,
forceNow: boolean,
waitForAutoRefund: boolean,
2019-12-15 19:08:07 +01:00
): Promise<void> {
if (forceNow) {
await resetPurchaseQueryRefundRetry(ws, proposalId);
}
2021-06-09 15:14:17 +02:00
const purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadOnly(async (tx) => {
return tx.purchases.get(proposalId);
});
2019-12-15 19:08:07 +01:00
if (!purchase) {
return;
}
if (!purchase.refundQueryRequested) {
2019-12-15 19:08:07 +01:00
return;
}
if (purchase.timestampFirstSuccessfulPay) {
if (
waitForAutoRefund &&
purchase.autoRefundDeadline &&
!isTimestampExpired(purchase.autoRefundDeadline)
) {
const requestUrl = new URL(
`orders/${purchase.download.contractData.orderId}`,
purchase.download.contractData.merchantBaseUrl,
);
requestUrl.searchParams.set(
"h_contract",
purchase.download.contractData.contractTermsHash,
);
// Long-poll for one second
requestUrl.searchParams.set("timeout_ms", "1000");
requestUrl.searchParams.set("await_refund_obtained", "yes");
logger.trace("making long-polling request for auto-refund");
const resp = await ws.http.get(requestUrl.href);
const orderStatus = await readSuccessResponseJsonOrThrow(
resp,
codecForMerchantOrderStatusPaid(),
);
if (!orderStatus.refunded) {
incrementPurchaseQueryRefundRetry(ws, proposalId, undefined);
return;
}
}
const requestUrl = new URL(
2021-01-04 13:30:38 +01:00
`orders/${purchase.download.contractData.orderId}/refund`,
purchase.download.contractData.merchantBaseUrl,
);
2019-12-15 19:08:07 +01:00
logger.trace(`making refund request to ${requestUrl.href}`);
2020-08-12 17:41:54 +02:00
const request = await ws.http.postJson(requestUrl.href, {
2021-01-04 13:30:38 +01:00
h_contract: purchase.download.contractData.contractTermsHash,
});
logger.trace(
"got json",
JSON.stringify(await request.json(), undefined, 2),
);
2020-07-23 14:05:17 +02:00
const refundResponse = await readSuccessResponseJsonOrThrow(
request,
codecForMerchantOrderRefundPickupResponse(),
);
2020-07-23 14:05:17 +02:00
await acceptRefunds(
ws,
proposalId,
refundResponse.refunds,
RefundReason.NormalRefund,
);
} else if (purchase.abortStatus === AbortStatus.AbortRefund) {
const requestUrl = new URL(
2021-01-04 13:30:38 +01:00
`orders/${purchase.download.contractData.orderId}/abort`,
purchase.download.contractData.merchantBaseUrl,
);
const abortingCoins: AbortingCoin[] = [];
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => ({
coins: x.coins,
}))
.runReadOnly(async (tx) => {
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
const coinPub = purchase.payCoinSelection.coinPubs[i];
const coin = await tx.coins.get(coinPub);
checkDbInvariant(!!coin, "expected coin to be present");
abortingCoins.push({
coin_pub: coinPub,
contribution: Amounts.stringify(
purchase.payCoinSelection.coinContributions[i],
),
exchange_url: coin.exchangeBaseUrl,
});
}
});
const abortReq: AbortRequest = {
2021-01-04 13:30:38 +01:00
h_contract: purchase.download.contractData.contractTermsHash,
coins: abortingCoins,
};
logger.trace(`making order abort request to ${requestUrl.href}`);
const request = await ws.http.postJson(requestUrl.href, abortReq);
const abortResp = await readSuccessResponseJsonOrThrow(
request,
codecForAbortResponse(),
);
const refunds: MerchantCoinRefundStatus[] = [];
if (abortResp.refunds.length != abortingCoins.length) {
// FIXME: define error code!
throw Error("invalid order abort response");
}
for (let i = 0; i < abortResp.refunds.length; i++) {
const r = abortResp.refunds[i];
refunds.push({
...r,
coin_pub: purchase.payCoinSelection.coinPubs[i],
refund_amount: Amounts.stringify(
purchase.payCoinSelection.coinContributions[i],
),
rtransaction_id: 0,
2021-01-13 00:51:30 +01:00
execution_time: timestampAddDuration(
purchase.download.contractData.timestamp,
{
d_ms: 1000,
},
),
});
}
await acceptRefunds(ws, proposalId, refunds, RefundReason.AbortRefund);
}
}
export async function abortFailedPayWithRefund(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
2021-06-09 15:14:17 +02:00
await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
.runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId);
if (!purchase) {
throw Error("purchase not found");
}
if (purchase.timestampFirstSuccessfulPay) {
// No point in aborting it. We don't even report an error.
logger.warn(`tried to abort successful payment`);
return;
}
if (purchase.abortStatus !== AbortStatus.None) {
return;
}
purchase.refundQueryRequested = true;
purchase.paymentSubmitPending = false;
purchase.abortStatus = AbortStatus.AbortRefund;
purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false);
await tx.purchases.put(purchase);
});
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {
logger.trace(`error during refund processing after abort pay: ${e}`);
});
2019-12-15 19:08:07 +01:00
}