towards refunds with updated protocol
This commit is contained in:
parent
e60563fb54
commit
d88829cfa8
@ -421,66 +421,66 @@ export async function getHistory(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.iter(Stores.refundEvents).forEachAsync(async (re) => {
|
// tx.iter(Stores.refundEvents).forEachAsync(async (re) => {
|
||||||
const proposal = await tx.get(Stores.proposals, re.proposalId);
|
// const proposal = await tx.get(Stores.proposals, re.proposalId);
|
||||||
if (!proposal) {
|
// if (!proposal) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const purchase = await tx.get(Stores.purchases, re.proposalId);
|
// const purchase = await tx.get(Stores.purchases, re.proposalId);
|
||||||
if (!purchase) {
|
// if (!purchase) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const orderShortInfo = getOrderShortInfo(proposal);
|
// const orderShortInfo = getOrderShortInfo(proposal);
|
||||||
if (!orderShortInfo) {
|
// if (!orderShortInfo) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const purchaseAmount = purchase.contractData.amount;
|
// const purchaseAmount = purchase.contractData.amount;
|
||||||
let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
|
// let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
|
||||||
let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
|
// let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
|
||||||
let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
|
// let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
|
||||||
Object.keys(purchase.refundsDone).forEach((x, i) => {
|
// Object.keys(purchase.refundsDone).forEach((x, i) => {
|
||||||
const r = purchase.refundsDone[x];
|
// const r = purchase.refundsDone[x];
|
||||||
if (r.refundGroupId !== re.refundGroupId) {
|
// if (r.refundGroupId !== re.refundGroupId) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount);
|
// const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount);
|
||||||
const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
|
// const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
|
||||||
amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount)
|
// amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount)
|
||||||
.amount;
|
// .amount;
|
||||||
amountRefundedEffective = Amounts.add(
|
// amountRefundedEffective = Amounts.add(
|
||||||
amountRefundedEffective,
|
// amountRefundedEffective,
|
||||||
refundAmount,
|
// refundAmount,
|
||||||
).amount;
|
// ).amount;
|
||||||
amountRefundedEffective = Amounts.sub(
|
// amountRefundedEffective = Amounts.sub(
|
||||||
amountRefundedEffective,
|
// amountRefundedEffective,
|
||||||
refundFee,
|
// refundFee,
|
||||||
).amount;
|
// ).amount;
|
||||||
});
|
// });
|
||||||
Object.keys(purchase.refundsFailed).forEach((x, i) => {
|
// Object.keys(purchase.refundsFailed).forEach((x, i) => {
|
||||||
const r = purchase.refundsFailed[x];
|
// const r = purchase.refundsFailed[x];
|
||||||
if (r.refundGroupId !== re.refundGroupId) {
|
// if (r.refundGroupId !== re.refundGroupId) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const ra = Amounts.parseOrThrow(r.perm.refund_amount);
|
// const ra = Amounts.parseOrThrow(r.perm.refund_amount);
|
||||||
const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
|
// const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
|
||||||
amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount;
|
// amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount;
|
||||||
amountRefundedInvalid = Amounts.add(amountRefundedInvalid, ra).amount;
|
// amountRefundedInvalid = Amounts.add(amountRefundedInvalid, ra).amount;
|
||||||
amountRefundedEffective = Amounts.sub(
|
// amountRefundedEffective = Amounts.sub(
|
||||||
amountRefundedEffective,
|
// amountRefundedEffective,
|
||||||
refundFee,
|
// refundFee,
|
||||||
).amount;
|
// ).amount;
|
||||||
});
|
// });
|
||||||
history.push({
|
// history.push({
|
||||||
type: HistoryEventType.Refund,
|
// type: HistoryEventType.Refund,
|
||||||
eventId: makeEventId(HistoryEventType.Refund, re.refundGroupId),
|
// eventId: makeEventId(HistoryEventType.Refund, re.refundGroupId),
|
||||||
refundGroupId: re.refundGroupId,
|
// refundGroupId: re.refundGroupId,
|
||||||
orderShortInfo,
|
// orderShortInfo,
|
||||||
timestamp: re.timestamp,
|
// timestamp: re.timestamp,
|
||||||
amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
|
// amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
|
||||||
amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
|
// amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
|
||||||
amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
|
// amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
||||||
tx.iter(Stores.recoupGroups).forEach((rg) => {
|
tx.iter(Stores.recoupGroups).forEach((rg) => {
|
||||||
if (rg.timestampFinished) {
|
if (rg.timestampFinished) {
|
||||||
|
@ -59,7 +59,6 @@ import { InternalWalletState } from "./state";
|
|||||||
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
||||||
import { strcmp, canonicalJson } from "../util/helpers";
|
import { strcmp, canonicalJson } from "../util/helpers";
|
||||||
import {
|
import {
|
||||||
readSuccessResponseJsonOrErrorCode,
|
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
} from "../util/http";
|
} from "../util/http";
|
||||||
|
|
||||||
@ -455,11 +454,7 @@ async function recordConfirmPay(
|
|||||||
timestampFirstSuccessfulPay: undefined,
|
timestampFirstSuccessfulPay: undefined,
|
||||||
autoRefundDeadline: undefined,
|
autoRefundDeadline: undefined,
|
||||||
paymentSubmitPending: true,
|
paymentSubmitPending: true,
|
||||||
refundGroups: [],
|
refunds: {},
|
||||||
refundsDone: {},
|
|
||||||
refundsFailed: {},
|
|
||||||
refundsPending: {},
|
|
||||||
refundsRefreshCost: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
@ -492,7 +487,7 @@ async function recordConfirmPay(
|
|||||||
const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
|
const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
|
||||||
coinPub: x,
|
coinPub: x,
|
||||||
}));
|
}));
|
||||||
await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
|
await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ async function putGroupAsFinished(
|
|||||||
recoupGroup.lastError = undefined;
|
recoupGroup.lastError = undefined;
|
||||||
if (recoupGroup.scheduleRefreshCoins.length > 0) {
|
if (recoupGroup.scheduleRefreshCoins.length > 0) {
|
||||||
const refreshGroupId = await createRefreshGroup(
|
const refreshGroupId = await createRefreshGroup(
|
||||||
|
ws,
|
||||||
tx,
|
tx,
|
||||||
recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
|
recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
|
||||||
RefreshReason.Recoup,
|
RefreshReason.Recoup,
|
||||||
|
@ -535,6 +535,7 @@ async function processRefreshSession(
|
|||||||
* Create a refresh group for a list of coins.
|
* Create a refresh group for a list of coins.
|
||||||
*/
|
*/
|
||||||
export async function createRefreshGroup(
|
export async function createRefreshGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
tx: TransactionHandle,
|
tx: TransactionHandle,
|
||||||
oldCoinPubs: CoinPublicKey[],
|
oldCoinPubs: CoinPublicKey[],
|
||||||
reason: RefreshReason,
|
reason: RefreshReason,
|
||||||
@ -554,6 +555,17 @@ export async function createRefreshGroup(
|
|||||||
};
|
};
|
||||||
|
|
||||||
await tx.put(Stores.refreshGroups, refreshGroup);
|
await tx.put(Stores.refreshGroups, refreshGroup);
|
||||||
|
|
||||||
|
const processAsync = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await processRefreshGroup(ws, refreshGroupId);
|
||||||
|
} catch (e) {
|
||||||
|
logger.trace(`Error during refresh: ${e}`)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processAsync();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
};
|
};
|
||||||
|
@ -36,23 +36,24 @@ import {
|
|||||||
CoinStatus,
|
CoinStatus,
|
||||||
RefundReason,
|
RefundReason,
|
||||||
RefundEventRecord,
|
RefundEventRecord,
|
||||||
|
RefundState,
|
||||||
|
PurchaseRecord,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { parseRefundUri } from "../util/taleruri";
|
import { parseRefundUri } from "../util/taleruri";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
||||||
import { Amounts } from "../util/amounts";
|
import { Amounts } from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
MerchantRefundDetails,
|
codecForMerchantOrderStatus,
|
||||||
MerchantRefundResponse,
|
MerchantCoinRefundStatus,
|
||||||
codecForMerchantRefundResponse,
|
MerchantCoinRefundSuccessStatus,
|
||||||
|
MerchantCoinRefundFailureStatus,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import { AmountJson } from "../util/amounts";
|
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
|
||||||
import { encodeCrock } from "../crypto/talerCrypto";
|
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
import { TransactionHandle } from "../util/query";
|
||||||
|
|
||||||
const logger = new Logger("refund.ts");
|
const logger = new Logger("refund.ts");
|
||||||
|
|
||||||
@ -85,80 +86,122 @@ async function incrementPurchaseQueryRefundRetry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRefundKey(d: MerchantRefundDetails): string {
|
function getRefundKey(d: MerchantCoinRefundStatus): string {
|
||||||
return `${d.coin_pub}-${d.rtransaction_id}`;
|
return `${d.coin_pub}-${d.rtransaction_id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptRefundResponse(
|
async function applySuccessfulRefund(
|
||||||
ws: InternalWalletState,
|
tx: TransactionHandle,
|
||||||
proposalId: string,
|
p: PurchaseRecord,
|
||||||
refundResponse: MerchantRefundResponse,
|
refreshCoinsMap: Record<string, { coinPub: string }>,
|
||||||
reason: RefundReason,
|
r: MerchantCoinRefundSuccessStatus,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refunds = refundResponse.refunds;
|
// FIXME: check signature before storing it as valid!
|
||||||
|
|
||||||
const refundGroupId = encodeCrock(randomBytes(32));
|
const refundKey = getRefundKey(r);
|
||||||
|
const coin = await tx.get(Stores.coins, r.coin_pub);
|
||||||
let numNewRefunds = 0;
|
|
||||||
|
|
||||||
const finishedRefunds: MerchantRefundDetails[] = [];
|
|
||||||
const unfinishedRefunds: MerchantRefundDetails[] = [];
|
|
||||||
const failedRefunds: MerchantRefundDetails[] = [];
|
|
||||||
|
|
||||||
console.log("handling refund response", refundResponse);
|
|
||||||
|
|
||||||
const refundsRefreshCost: { [refundKey: string]: AmountJson } = {};
|
|
||||||
|
|
||||||
for (const rd of refunds) {
|
|
||||||
logger.trace(
|
|
||||||
`Refund ${rd.rtransaction_id} has HTTP status ${rd.exchange_http_status}`,
|
|
||||||
);
|
|
||||||
if (rd.exchange_http_status === 200) {
|
|
||||||
// FIXME: also verify signature if necessary.
|
|
||||||
finishedRefunds.push(rd);
|
|
||||||
} else if (
|
|
||||||
rd.exchange_http_status >= 400 &&
|
|
||||||
rd.exchange_http_status < 400
|
|
||||||
) {
|
|
||||||
failedRefunds.push(rd);
|
|
||||||
} else {
|
|
||||||
unfinishedRefunds.push(rd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute cost.
|
|
||||||
// FIXME: Optimize, don't always recompute.
|
|
||||||
for (const rd of [...finishedRefunds, ...unfinishedRefunds]) {
|
|
||||||
const key = getRefundKey(rd);
|
|
||||||
const coin = await ws.db.get(Stores.coins, rd.coin_pub);
|
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
continue;
|
console.warn("coin not found, can't apply refund");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const denom = await ws.db.getIndexed(
|
const denom = await tx.getIndexed(
|
||||||
Stores.denominations.denomPubHashIndex,
|
Stores.denominations.denomPubHashIndex,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
);
|
);
|
||||||
if (!denom) {
|
if (!denom) {
|
||||||
throw Error("inconsistent database");
|
throw Error("inconsistent database");
|
||||||
}
|
}
|
||||||
const amountLeft = Amounts.sub(
|
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
|
||||||
Amounts.add(coin.currentAmount, Amounts.parseOrThrow(rd.refund_amount))
|
const refundAmount = Amounts.parseOrThrow(r.refund_amount);
|
||||||
.amount,
|
const refundFee = denom.feeRefund;
|
||||||
Amounts.parseOrThrow(rd.refund_fee),
|
coin.status = CoinStatus.Dormant;
|
||||||
).amount;
|
coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
|
||||||
const allDenoms = await ws.db
|
coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
|
||||||
.iterIndex(
|
logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`);
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
await tx.put(Stores.coins, coin);
|
||||||
coin.exchangeBaseUrl,
|
|
||||||
)
|
const allDenoms = await tx
|
||||||
|
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, coin.exchangeBaseUrl)
|
||||||
.toArray();
|
.toArray();
|
||||||
refundsRefreshCost[key] = getTotalRefreshCost(allDenoms, denom, amountLeft);
|
|
||||||
|
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.Applied,
|
||||||
|
executionTime: r.execution_time,
|
||||||
|
refundAmount: Amounts.parseOrThrow(r.refund_amount),
|
||||||
|
refundFee: denom.feeRefund,
|
||||||
|
totalRefreshCostBound,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function storePendingRefund(
|
||||||
|
tx: TransactionHandle,
|
||||||
|
p: PurchaseRecord,
|
||||||
|
r: MerchantCoinRefundFailureStatus,
|
||||||
|
): Promise<void> {
|
||||||
|
const refundKey = getRefundKey(r);
|
||||||
|
|
||||||
|
const coin = await tx.get(Stores.coins, r.coin_pub);
|
||||||
|
if (!coin) {
|
||||||
|
console.warn("coin not found, can't apply refund");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const denom = await tx.getIndexed(
|
||||||
|
Stores.denominations.denomPubHashIndex,
|
||||||
|
coin.denomPubHash,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!denom) {
|
||||||
|
throw Error("inconsistent database");
|
||||||
|
}
|
||||||
|
|
||||||
|
const allDenoms = await tx
|
||||||
|
.iterIndexed(Stores.denominations.exchangeBaseUrlIndex, coin.exchangeBaseUrl)
|
||||||
|
.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,
|
||||||
|
executionTime: r.execution_time,
|
||||||
|
refundAmount: Amounts.parseOrThrow(r.refund_amount),
|
||||||
|
refundFee: denom.feeRefund,
|
||||||
|
totalRefreshCostBound,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptRefunds(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
refunds: MerchantCoinRefundStatus[],
|
||||||
|
reason: RefundReason,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log("handling refunds", refunds);
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.purchases, Stores.coins, Stores.refreshGroups, Stores.refundEvents],
|
[Stores.purchases, Stores.coins, Stores.denominations, Stores.refreshGroups, Stores.refundEvents],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
@ -166,103 +209,60 @@ async function acceptRefundResponse(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups that newly failed/succeeded
|
const refreshCoinsMap: Record<string, CoinPublicKey> = {};
|
||||||
const changedGroups: { [refundGroupId: string]: boolean } = {};
|
|
||||||
|
|
||||||
for (const rd of failedRefunds) {
|
for (const refundStatus of refunds) {
|
||||||
const refundKey = getRefundKey(rd);
|
const refundKey = getRefundKey(refundStatus);
|
||||||
if (p.refundsFailed[refundKey]) {
|
const existingRefundInfo = p.refunds[refundKey];
|
||||||
|
|
||||||
|
// Already failed.
|
||||||
|
if (existingRefundInfo?.type === RefundState.Failed) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!p.refundsFailed[refundKey]) {
|
|
||||||
p.refundsFailed[refundKey] = {
|
|
||||||
perm: rd,
|
|
||||||
refundGroupId,
|
|
||||||
};
|
|
||||||
numNewRefunds++;
|
|
||||||
changedGroups[refundGroupId] = true;
|
|
||||||
}
|
|
||||||
const oldPending = p.refundsPending[refundKey];
|
|
||||||
if (oldPending) {
|
|
||||||
delete p.refundsPending[refundKey];
|
|
||||||
changedGroups[oldPending.refundGroupId] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rd of unfinishedRefunds) {
|
// Already applied.
|
||||||
const refundKey = getRefundKey(rd);
|
if (existingRefundInfo?.type === RefundState.Applied) {
|
||||||
if (!p.refundsPending[refundKey]) {
|
|
||||||
p.refundsPending[refundKey] = {
|
|
||||||
perm: rd,
|
|
||||||
refundGroupId,
|
|
||||||
};
|
|
||||||
numNewRefunds++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid duplicates
|
|
||||||
const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {};
|
|
||||||
|
|
||||||
for (const rd of finishedRefunds) {
|
|
||||||
const refundKey = getRefundKey(rd);
|
|
||||||
if (p.refundsDone[refundKey]) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
p.refundsDone[refundKey] = {
|
|
||||||
perm: rd,
|
// Still pending.
|
||||||
refundGroupId,
|
if (
|
||||||
};
|
refundStatus.success === false &&
|
||||||
const oldPending = p.refundsPending[refundKey];
|
existingRefundInfo?.type === RefundState.Pending
|
||||||
if (oldPending) {
|
) {
|
||||||
delete p.refundsPending[refundKey];
|
continue;
|
||||||
changedGroups[oldPending.refundGroupId] = true;
|
}
|
||||||
|
|
||||||
|
// Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
|
||||||
|
|
||||||
|
if (refundStatus.success === true) {
|
||||||
|
await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
|
||||||
} else {
|
} else {
|
||||||
numNewRefunds++;
|
await storePendingRefund(tx, p, refundStatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = await tx.get(Stores.coins, rd.coin_pub);
|
const refreshCoinsPubs = Object.values(refreshCoinsMap);
|
||||||
|
await createRefreshGroup(ws, tx, refreshCoinsPubs, RefreshReason.Refund);
|
||||||
if (!c) {
|
|
||||||
console.warn("coin not found, can't apply refund");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub };
|
|
||||||
logger.trace(`commiting refund ${refundKey} to coin ${c.coinPub}`);
|
|
||||||
logger.trace(
|
|
||||||
`coin amount before is ${Amounts.stringify(c.currentAmount)}`,
|
|
||||||
);
|
|
||||||
logger.trace(`refund amount (via merchant) is ${refundKey}`);
|
|
||||||
logger.trace(`refund fee (via merchant) is ${refundKey}`);
|
|
||||||
const refundAmount = Amounts.parseOrThrow(rd.refund_amount);
|
|
||||||
const refundFee = Amounts.parseOrThrow(rd.refund_fee);
|
|
||||||
c.status = CoinStatus.Dormant;
|
|
||||||
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
|
|
||||||
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
|
|
||||||
logger.trace(
|
|
||||||
`coin amount after is ${Amounts.stringify(c.currentAmount)}`,
|
|
||||||
);
|
|
||||||
await tx.put(Stores.coins, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Are we done with querying yet, or do we need to do another round
|
// Are we done with querying yet, or do we need to do another round
|
||||||
// after a retry delay?
|
// after a retry delay?
|
||||||
let queryDone = true;
|
let queryDone = true;
|
||||||
|
|
||||||
logger.trace(`got ${numNewRefunds} new refund permissions`);
|
|
||||||
|
|
||||||
if (numNewRefunds === 0) {
|
|
||||||
if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms > now.t_ms) {
|
if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms > now.t_ms) {
|
||||||
queryDone = false;
|
queryDone = false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
p.refundGroups.push({
|
let numPendingRefunds = 0;
|
||||||
reason: RefundReason.NormalRefund,
|
for (const ri of Object.values(p.refunds)) {
|
||||||
refundGroupId,
|
switch (ri.type) {
|
||||||
timestampQueried: getTimestampNow(),
|
case RefundState.Pending:
|
||||||
});
|
numPendingRefunds++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(unfinishedRefunds).length != 0) {
|
if (numPendingRefunds > 0) {
|
||||||
queryDone = false;
|
queryDone = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,38 +281,7 @@ async function acceptRefundResponse(
|
|||||||
logger.trace("refund query not done");
|
logger.trace("refund query not done");
|
||||||
}
|
}
|
||||||
|
|
||||||
p.refundsRefreshCost = { ...p.refundsRefreshCost, ...refundsRefreshCost };
|
|
||||||
|
|
||||||
await tx.put(Stores.purchases, p);
|
await tx.put(Stores.purchases, p);
|
||||||
|
|
||||||
const coinsPubsToBeRefreshed = Object.values(refreshCoinsMap);
|
|
||||||
if (coinsPubsToBeRefreshed.length > 0) {
|
|
||||||
await createRefreshGroup(
|
|
||||||
tx,
|
|
||||||
coinsPubsToBeRefreshed,
|
|
||||||
RefreshReason.Refund,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any of the refund groups are done, and we
|
|
||||||
// can emit an corresponding event.
|
|
||||||
for (const g of Object.keys(changedGroups)) {
|
|
||||||
let groupDone = true;
|
|
||||||
for (const pk of Object.keys(p.refundsPending)) {
|
|
||||||
const r = p.refundsPending[pk];
|
|
||||||
if (r.refundGroupId == g) {
|
|
||||||
groupDone = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (groupDone) {
|
|
||||||
const refundEvent: RefundEventRecord = {
|
|
||||||
proposalId,
|
|
||||||
refundGroupId: g,
|
|
||||||
timestamp: now,
|
|
||||||
};
|
|
||||||
await tx.put(Stores.refundEvents, refundEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -430,22 +399,33 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = await ws.http.get(
|
const requestUrl = new URL(
|
||||||
new URL(
|
|
||||||
`orders/${purchase.contractData.orderId}`,
|
`orders/${purchase.contractData.orderId}`,
|
||||||
purchase.contractData.merchantBaseUrl,
|
purchase.contractData.merchantBaseUrl,
|
||||||
).href,
|
|
||||||
);
|
);
|
||||||
|
requestUrl.searchParams.set(
|
||||||
|
"h_contract",
|
||||||
|
purchase.contractData.contractTermsHash,
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = await ws.http.get(requestUrl.href);
|
||||||
|
|
||||||
|
console.log("got json", JSON.stringify(await request.json(), undefined, 2));
|
||||||
|
|
||||||
const refundResponse = await readSuccessResponseJsonOrThrow(
|
const refundResponse = await readSuccessResponseJsonOrThrow(
|
||||||
request,
|
request,
|
||||||
codecForMerchantRefundResponse(),
|
codecForMerchantOrderStatus(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await acceptRefundResponse(
|
if (!refundResponse.paid) {
|
||||||
|
logger.error("can't refund unpaid order");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await acceptRefunds(
|
||||||
ws,
|
ws,
|
||||||
proposalId,
|
proposalId,
|
||||||
refundResponse,
|
refundResponse.refunds,
|
||||||
RefundReason.NormalRefund,
|
RefundReason.NormalRefund,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,68 +44,6 @@ function makeEventId(type: TransactionType, ...args: string[]): string {
|
|||||||
return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
|
return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RefundStats {
|
|
||||||
amountInvalid: AmountJson;
|
|
||||||
amountEffective: AmountJson;
|
|
||||||
amountRaw: AmountJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRefundStats(
|
|
||||||
pr: PurchaseRecord,
|
|
||||||
refundGroupId: string,
|
|
||||||
): RefundStats {
|
|
||||||
let amountEffective = Amounts.getZero(pr.contractData.amount.currency);
|
|
||||||
let amountInvalid = Amounts.getZero(pr.contractData.amount.currency);
|
|
||||||
let amountRaw = Amounts.getZero(pr.contractData.amount.currency);
|
|
||||||
|
|
||||||
for (const rk of Object.keys(pr.refundsDone)) {
|
|
||||||
const perm = pr.refundsDone[rk].perm;
|
|
||||||
if (pr.refundsDone[rk].refundGroupId !== refundGroupId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
amountEffective = Amounts.add(
|
|
||||||
amountEffective,
|
|
||||||
Amounts.parseOrThrow(perm.refund_amount),
|
|
||||||
).amount;
|
|
||||||
amountRaw = Amounts.add(amountRaw, Amounts.parseOrThrow(perm.refund_amount))
|
|
||||||
.amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract fees from effective refund amount
|
|
||||||
|
|
||||||
for (const rk of Object.keys(pr.refundsDone)) {
|
|
||||||
const perm = pr.refundsDone[rk].perm;
|
|
||||||
if (pr.refundsDone[rk].refundGroupId !== refundGroupId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
amountEffective = Amounts.sub(
|
|
||||||
amountEffective,
|
|
||||||
Amounts.parseOrThrow(perm.refund_fee),
|
|
||||||
).amount;
|
|
||||||
if (pr.refundsRefreshCost[rk]) {
|
|
||||||
amountEffective = Amounts.sub(amountEffective, pr.refundsRefreshCost[rk])
|
|
||||||
.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rk of Object.keys(pr.refundsFailed)) {
|
|
||||||
const perm = pr.refundsDone[rk].perm;
|
|
||||||
if (pr.refundsDone[rk].refundGroupId !== refundGroupId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
amountInvalid = Amounts.add(
|
|
||||||
amountInvalid,
|
|
||||||
Amounts.parseOrThrow(perm.refund_fee),
|
|
||||||
).amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
amountEffective,
|
|
||||||
amountInvalid,
|
|
||||||
amountRaw,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldSkipCurrency(
|
function shouldSkipCurrency(
|
||||||
transactionsRequest: TransactionsRequest | undefined,
|
transactionsRequest: TransactionsRequest | undefined,
|
||||||
currency: string,
|
currency: string,
|
||||||
@ -319,36 +257,37 @@ export async function getTransactions(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const rg of pr.refundGroups) {
|
// for (const rg of pr.refundGroups) {
|
||||||
const pending = Object.keys(pr.refundsPending).length > 0;
|
// const pending = Object.keys(pr.refundsPending).length > 0;
|
||||||
const stats = getRefundStats(pr, rg.refundGroupId);
|
// const stats = getRefundStats(pr, rg.refundGroupId);
|
||||||
|
|
||||||
|
// transactions.push({
|
||||||
|
// type: TransactionType.Refund,
|
||||||
|
// pending,
|
||||||
|
// info: {
|
||||||
|
// fulfillmentUrl: pr.contractData.fulfillmentUrl,
|
||||||
|
// merchant: pr.contractData.merchant,
|
||||||
|
// orderId: pr.contractData.orderId,
|
||||||
|
// products: pr.contractData.products,
|
||||||
|
// summary: pr.contractData.summary,
|
||||||
|
// summary_i18n: pr.contractData.summaryI18n,
|
||||||
|
// },
|
||||||
|
// timestamp: rg.timestampQueried,
|
||||||
|
// transactionId: makeEventId(
|
||||||
|
// TransactionType.Refund,
|
||||||
|
// pr.proposalId,
|
||||||
|
// `${rg.timestampQueried.t_ms}`,
|
||||||
|
// ),
|
||||||
|
// refundedTransactionId: makeEventId(
|
||||||
|
// TransactionType.Payment,
|
||||||
|
// pr.proposalId,
|
||||||
|
// ),
|
||||||
|
// amountEffective: Amounts.stringify(stats.amountEffective),
|
||||||
|
// amountInvalid: Amounts.stringify(stats.amountInvalid),
|
||||||
|
// amountRaw: Amounts.stringify(stats.amountRaw),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
transactions.push({
|
|
||||||
type: TransactionType.Refund,
|
|
||||||
pending,
|
|
||||||
info: {
|
|
||||||
fulfillmentUrl: pr.contractData.fulfillmentUrl,
|
|
||||||
merchant: pr.contractData.merchant,
|
|
||||||
orderId: pr.contractData.orderId,
|
|
||||||
products: pr.contractData.products,
|
|
||||||
summary: pr.contractData.summary,
|
|
||||||
summary_i18n: pr.contractData.summaryI18n,
|
|
||||||
},
|
|
||||||
timestamp: rg.timestampQueried,
|
|
||||||
transactionId: makeEventId(
|
|
||||||
TransactionType.Refund,
|
|
||||||
pr.proposalId,
|
|
||||||
`${rg.timestampQueried.t_ms}`,
|
|
||||||
),
|
|
||||||
refundedTransactionId: makeEventId(
|
|
||||||
TransactionType.Payment,
|
|
||||||
pr.proposalId,
|
|
||||||
),
|
|
||||||
amountEffective: Amounts.stringify(stats.amountEffective),
|
|
||||||
amountInvalid: Amounts.stringify(stats.amountInvalid),
|
|
||||||
amountRaw: Amounts.stringify(stats.amountRaw),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -27,7 +27,6 @@ import { AmountJson } from "../util/amounts";
|
|||||||
import {
|
import {
|
||||||
Auditor,
|
Auditor,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
MerchantRefundDetails,
|
|
||||||
TipResponse,
|
TipResponse,
|
||||||
ExchangeSignKeyJson,
|
ExchangeSignKeyJson,
|
||||||
MerchantInfo,
|
MerchantInfo,
|
||||||
@ -1140,13 +1139,54 @@ export interface WireFee {
|
|||||||
*/
|
*/
|
||||||
export interface RefundEventRecord {
|
export interface RefundEventRecord {
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
|
merchantExecutionTimestamp: Timestamp;
|
||||||
refundGroupId: string;
|
refundGroupId: string;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundInfo {
|
export const enum RefundState {
|
||||||
refundGroupId: string;
|
Failed = "failed",
|
||||||
perm: MerchantRefundDetails;
|
Applied = "applied",
|
||||||
|
Pending = "pending",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of one refund from the merchant, maintained by the wallet.
|
||||||
|
*/
|
||||||
|
export type WalletRefundItem =
|
||||||
|
| WalletRefundFailedItem
|
||||||
|
| WalletRefundPendingItem
|
||||||
|
| WalletRefundAppliedItem;
|
||||||
|
|
||||||
|
export interface WalletRefundItemCommon {
|
||||||
|
executionTime: Timestamp;
|
||||||
|
refundAmount: AmountJson;
|
||||||
|
refundFee: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upper bound on the refresh cost incurred by
|
||||||
|
* applying this refund.
|
||||||
|
*
|
||||||
|
* Might be lower in practice when two refunds on the same
|
||||||
|
* coin are refreshed in the same refresh operation.
|
||||||
|
*/
|
||||||
|
totalRefreshCostBound: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Failed refund, either because the merchant did
|
||||||
|
* something wrong or it expired.
|
||||||
|
*/
|
||||||
|
export interface WalletRefundFailedItem extends WalletRefundItemCommon {
|
||||||
|
type: RefundState.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletRefundPendingItem extends WalletRefundItemCommon {
|
||||||
|
type: RefundState.Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
|
||||||
|
type: RefundState.Applied;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum RefundReason {
|
export const enum RefundReason {
|
||||||
@ -1160,12 +1200,6 @@ export const enum RefundReason {
|
|||||||
AbortRefund = "abort-pay-refund",
|
AbortRefund = "abort-pay-refund",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundGroupInfo {
|
|
||||||
refundGroupId: string;
|
|
||||||
timestampQueried: Timestamp;
|
|
||||||
reason: RefundReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record stored for every time we successfully submitted
|
* Record stored for every time we successfully submitted
|
||||||
* a payment to the merchant (both first time and re-play).
|
* a payment to the merchant (both first time and re-play).
|
||||||
@ -1269,31 +1303,11 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
timestampAccept: Timestamp;
|
timestampAccept: Timestamp;
|
||||||
|
|
||||||
/**
|
|
||||||
* Information regarding each group of refunds we receive at once.
|
|
||||||
*/
|
|
||||||
refundGroups: RefundGroupInfo[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending refunds for the purchase. A refund is pending
|
* Pending refunds for the purchase. A refund is pending
|
||||||
* when the merchant reports a transient error from the exchange.
|
* when the merchant reports a transient error from the exchange.
|
||||||
*/
|
*/
|
||||||
refundsPending: { [refundKey: string]: RefundInfo };
|
refunds: { [refundKey: string]: WalletRefundItem };
|
||||||
|
|
||||||
/**
|
|
||||||
* Applied refunds for the purchase.
|
|
||||||
*/
|
|
||||||
refundsDone: { [refundKey: string]: RefundInfo };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refunds that permanently failed.
|
|
||||||
*/
|
|
||||||
refundsFailed: { [refundKey: string]: RefundInfo };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh cost for each refund permission.
|
|
||||||
*/
|
|
||||||
refundsRefreshCost: { [refundKey: string]: AmountJson };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When was the last refund made?
|
* When was the last refund made?
|
||||||
|
@ -37,6 +37,10 @@ import {
|
|||||||
codecForBoolean,
|
codecForBoolean,
|
||||||
makeCodecForMap,
|
makeCodecForMap,
|
||||||
Codec,
|
Codec,
|
||||||
|
makeCodecForConstNumber,
|
||||||
|
makeCodecForUnion,
|
||||||
|
makeCodecForConstTrue,
|
||||||
|
makeCodecForConstFalse,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
import {
|
import {
|
||||||
Timestamp,
|
Timestamp,
|
||||||
@ -436,7 +440,7 @@ export class ContractTerms {
|
|||||||
/**
|
/**
|
||||||
* Refund permission in the format that the merchant gives it to us.
|
* Refund permission in the format that the merchant gives it to us.
|
||||||
*/
|
*/
|
||||||
export class MerchantRefundDetails {
|
export class MerchantAbortPayRefundDetails {
|
||||||
/**
|
/**
|
||||||
* Amount to be refunded.
|
* Amount to be refunded.
|
||||||
*/
|
*/
|
||||||
@ -502,7 +506,7 @@ export class MerchantRefundResponse {
|
|||||||
/**
|
/**
|
||||||
* The signed refund permissions, to be sent to the exchange.
|
* The signed refund permissions, to be sent to the exchange.
|
||||||
*/
|
*/
|
||||||
refunds: MerchantRefundDetails[];
|
refunds: MerchantAbortPayRefundDetails[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -834,6 +838,115 @@ export interface ExchangeRevealResponse {
|
|||||||
ev_sigs: ExchangeRevealItem[];
|
ev_sigs: ExchangeRevealItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MerchantOrderStatus =
|
||||||
|
| MerchantOrderStatusPaid
|
||||||
|
| MerchantOrderStatusUnpaid;
|
||||||
|
|
||||||
|
interface MerchantOrderStatusPaid {
|
||||||
|
/**
|
||||||
|
* Has the payment for this order (ever) been completed?
|
||||||
|
*/
|
||||||
|
paid: true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was the payment refunded (even partially, via refund or abort)?
|
||||||
|
*/
|
||||||
|
refunded: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that was refunded in total.
|
||||||
|
*/
|
||||||
|
refund_amount: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful refunds for this payment, empty array for none.
|
||||||
|
*/
|
||||||
|
refunds: MerchantCoinRefundStatus[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key of the merchant.
|
||||||
|
*/
|
||||||
|
merchant_pub: EddsaPublicKeyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MerchantCoinRefundStatus =
|
||||||
|
| MerchantCoinRefundSuccessStatus
|
||||||
|
| MerchantCoinRefundFailureStatus;
|
||||||
|
|
||||||
|
export interface MerchantCoinRefundSuccessStatus {
|
||||||
|
success: true;
|
||||||
|
|
||||||
|
// HTTP status of the exchange request, 200 (integer) required for refund confirmations.
|
||||||
|
exchange_status: 200;
|
||||||
|
|
||||||
|
// the EdDSA :ref:signature (binary-only) with purpose
|
||||||
|
// TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
|
||||||
|
// exchange affirming the successful refund
|
||||||
|
exchange_sig: EddsaSignatureString;
|
||||||
|
|
||||||
|
// public EdDSA key of the exchange that was used to generate the signature.
|
||||||
|
// Should match one of the exchange's signing keys from /keys. It is given
|
||||||
|
// explicitly as the client might otherwise be confused by clock skew as to
|
||||||
|
// which signing key was used.
|
||||||
|
exchange_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// Refund transaction ID.
|
||||||
|
rtransaction_id: number;
|
||||||
|
|
||||||
|
// public key of a coin that was refunded
|
||||||
|
coin_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// Amount that was refunded, including refund fee charged by the exchange
|
||||||
|
// to the customer.
|
||||||
|
refund_amount: AmountString;
|
||||||
|
|
||||||
|
execution_time: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantCoinRefundFailureStatus {
|
||||||
|
success: false;
|
||||||
|
|
||||||
|
// HTTP status of the exchange request, must NOT be 200.
|
||||||
|
exchange_status: number;
|
||||||
|
|
||||||
|
// Taler error code from the exchange reply, if available.
|
||||||
|
exchange_code?: number;
|
||||||
|
|
||||||
|
// If available, HTTP reply from the exchange.
|
||||||
|
exchange_reply?: any;
|
||||||
|
|
||||||
|
// Refund transaction ID.
|
||||||
|
rtransaction_id: number;
|
||||||
|
|
||||||
|
// public key of a coin that was refunded
|
||||||
|
coin_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// Amount that was refunded, including refund fee charged by the exchange
|
||||||
|
// to the customer.
|
||||||
|
refund_amount: AmountString;
|
||||||
|
|
||||||
|
execution_time: Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantOrderStatusUnpaid {
|
||||||
|
/**
|
||||||
|
* Has the payment for this order (ever) been completed?
|
||||||
|
*/
|
||||||
|
paid: false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI that the wallet must process to complete the payment.
|
||||||
|
*/
|
||||||
|
taler_pay_uri: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative order ID which was paid for already in the same session.
|
||||||
|
*
|
||||||
|
* Only given if the same product was purchased before in the same session.
|
||||||
|
*/
|
||||||
|
already_paid_order_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type AmountString = string;
|
export type AmountString = string;
|
||||||
export type Base32String = string;
|
export type Base32String = string;
|
||||||
export type EddsaSignatureString = string;
|
export type EddsaSignatureString = string;
|
||||||
@ -940,9 +1053,9 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
|
|||||||
.build("ContractTerms");
|
.build("ContractTerms");
|
||||||
|
|
||||||
export const codecForMerchantRefundPermission = (): Codec<
|
export const codecForMerchantRefundPermission = (): Codec<
|
||||||
MerchantRefundDetails
|
MerchantAbortPayRefundDetails
|
||||||
> =>
|
> =>
|
||||||
makeCodecForObject<MerchantRefundDetails>()
|
makeCodecForObject<MerchantAbortPayRefundDetails>()
|
||||||
.property("refund_amount", codecForString)
|
.property("refund_amount", codecForString)
|
||||||
.property("refund_fee", codecForString)
|
.property("refund_fee", codecForString)
|
||||||
.property("coin_pub", codecForString)
|
.property("coin_pub", codecForString)
|
||||||
@ -1094,3 +1207,67 @@ export const codecForExchangeRevealResponse = (): Codec<
|
|||||||
makeCodecForObject<ExchangeRevealResponse>()
|
makeCodecForObject<ExchangeRevealResponse>()
|
||||||
.property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
|
.property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
|
||||||
.build("ExchangeRevealResponse");
|
.build("ExchangeRevealResponse");
|
||||||
|
|
||||||
|
export const codecForMerchantCoinRefundSuccessStatus = (): Codec<
|
||||||
|
MerchantCoinRefundSuccessStatus
|
||||||
|
> =>
|
||||||
|
makeCodecForObject<MerchantCoinRefundSuccessStatus>()
|
||||||
|
.property("success", makeCodecForConstTrue())
|
||||||
|
.property("coin_pub", codecForString)
|
||||||
|
.property("exchange_status", makeCodecForConstNumber(200))
|
||||||
|
.property("exchange_sig", codecForString)
|
||||||
|
.property("rtransaction_id", codecForNumber)
|
||||||
|
.property("refund_amount", codecForString)
|
||||||
|
.property("exchange_pub", codecForString)
|
||||||
|
.property("execution_time", codecForTimestamp)
|
||||||
|
.build("MerchantCoinRefundSuccessStatus");
|
||||||
|
|
||||||
|
export const codecForMerchantCoinRefundFailureStatus = (): Codec<
|
||||||
|
MerchantCoinRefundFailureStatus
|
||||||
|
> =>
|
||||||
|
makeCodecForObject<MerchantCoinRefundFailureStatus>()
|
||||||
|
.property("success", makeCodecForConstFalse())
|
||||||
|
.property("coin_pub", codecForString)
|
||||||
|
.property("exchange_status", makeCodecForConstNumber(200))
|
||||||
|
.property("rtransaction_id", codecForNumber)
|
||||||
|
.property("refund_amount", codecForString)
|
||||||
|
.property("exchange_code", makeCodecOptional(codecForNumber))
|
||||||
|
.property("exchange_reply", makeCodecOptional(codecForAny))
|
||||||
|
.property("execution_time", codecForTimestamp)
|
||||||
|
.build("MerchantCoinRefundSuccessStatus");
|
||||||
|
|
||||||
|
export const codecForMerchantCoinRefundStatus = (): Codec<
|
||||||
|
MerchantCoinRefundStatus
|
||||||
|
> =>
|
||||||
|
makeCodecForUnion<MerchantCoinRefundStatus>()
|
||||||
|
.discriminateOn("success")
|
||||||
|
.alternative(true, codecForMerchantCoinRefundSuccessStatus())
|
||||||
|
.alternative(false, codecForMerchantCoinRefundFailureStatus())
|
||||||
|
.build("MerchantCoinRefundStatus");
|
||||||
|
|
||||||
|
export const codecForMerchantOrderStatusPaid = (): Codec<
|
||||||
|
MerchantOrderStatusPaid
|
||||||
|
> =>
|
||||||
|
makeCodecForObject<MerchantOrderStatusPaid>()
|
||||||
|
.property("paid", makeCodecForConstTrue())
|
||||||
|
.property("merchant_pub", codecForString)
|
||||||
|
.property("refund_amount", codecForString)
|
||||||
|
.property("refunded", codecForBoolean)
|
||||||
|
.property("refunds", makeCodecForList(codecForMerchantCoinRefundStatus()))
|
||||||
|
.build("MerchantOrderStatusPaid");
|
||||||
|
|
||||||
|
export const codecForMerchantOrderStatusUnpaid = (): Codec<
|
||||||
|
MerchantOrderStatusUnpaid
|
||||||
|
> =>
|
||||||
|
makeCodecForObject<MerchantOrderStatusUnpaid>()
|
||||||
|
.property("paid", makeCodecForConstFalse())
|
||||||
|
.property("taler_pay_uri", codecForString)
|
||||||
|
.property("already_paid_order_id", makeCodecOptional(codecForString))
|
||||||
|
.build("MerchantOrderStatusUnpaid");
|
||||||
|
|
||||||
|
export const codecForMerchantOrderStatus = (): Codec<MerchantOrderStatus> =>
|
||||||
|
makeCodecForUnion<MerchantOrderStatus>()
|
||||||
|
.discriminateOn("paid")
|
||||||
|
.alternative(true, codecForMerchantOrderStatusPaid())
|
||||||
|
.alternative(false, codecForMerchantOrderStatusUnpaid())
|
||||||
|
.build("MerchantOrderStatus");
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
* Type-safe codecs for converting from/to JSON.
|
* Type-safe codecs for converting from/to JSON.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error thrown when decoding fails.
|
* Error thrown when decoding fails.
|
||||||
*/
|
*/
|
||||||
@ -335,6 +337,60 @@ export function makeCodecForConstString<V extends string>(s: V): Codec<V> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a boolean true constant.
|
||||||
|
*/
|
||||||
|
export function makeCodecForConstTrue(): Codec<true> {
|
||||||
|
return {
|
||||||
|
decode(x: any, c?: Context): true {
|
||||||
|
if (x === true) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
throw new DecodingError(
|
||||||
|
`expected boolean true at ${renderContext(
|
||||||
|
c,
|
||||||
|
)} but got ${typeof x}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a boolean true constant.
|
||||||
|
*/
|
||||||
|
export function makeCodecForConstFalse(): Codec<false> {
|
||||||
|
return {
|
||||||
|
decode(x: any, c?: Context): false {
|
||||||
|
if (x === false) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
throw new DecodingError(
|
||||||
|
`expected boolean false at ${renderContext(
|
||||||
|
c,
|
||||||
|
)} but got ${typeof x}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a value that must be a constant number.
|
||||||
|
*/
|
||||||
|
export function makeCodecForConstNumber<V extends number>(n: V): Codec<V> {
|
||||||
|
return {
|
||||||
|
decode(x: any, c?: Context): V {
|
||||||
|
if (x === n) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
throw new DecodingError(
|
||||||
|
`expected number constant "${n}" at ${renderContext(
|
||||||
|
c,
|
||||||
|
)} but got ${typeof x}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function makeCodecOptional<V>(
|
export function makeCodecOptional<V>(
|
||||||
innerCodec: Codec<V>,
|
innerCodec: Codec<V>,
|
||||||
): Codec<V | undefined> {
|
): Codec<V | undefined> {
|
||||||
|
@ -220,7 +220,7 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (maybePath === "-") {
|
if (maybePath === "-") {
|
||||||
maybePath = "public/";
|
maybePath = "";
|
||||||
} else {
|
} else {
|
||||||
maybePath = decodeURIComponent(maybePath) + "/";
|
maybePath = decodeURIComponent(maybePath) + "/";
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
Stores,
|
Stores,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
|
RefundState,
|
||||||
} from "./types/dbTypes";
|
} from "./types/dbTypes";
|
||||||
import { CoinDumpJson } from "./types/talerTypes";
|
import { CoinDumpJson } from "./types/talerTypes";
|
||||||
import {
|
import {
|
||||||
@ -534,6 +535,7 @@ export class Wallet {
|
|||||||
[Stores.refreshGroups],
|
[Stores.refreshGroups],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
return await createRefreshGroup(
|
return await createRefreshGroup(
|
||||||
|
this.ws,
|
||||||
tx,
|
tx,
|
||||||
[{ coinPub: oldCoinPub }],
|
[{ coinPub: oldCoinPub }],
|
||||||
RefreshReason.Manual,
|
RefreshReason.Manual,
|
||||||
@ -785,22 +787,23 @@ export class Wallet {
|
|||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error("unknown purchase");
|
throw Error("unknown purchase");
|
||||||
}
|
}
|
||||||
const refundsDoneAmounts = Object.values(purchase.refundsDone).map((x) =>
|
const refundsDoneAmounts = Object.values(purchase.refunds)
|
||||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
.filter((x) => x.type === RefundState.Applied)
|
||||||
);
|
.map((x) => x.refundAmount);
|
||||||
const refundsPendingAmounts = Object.values(
|
|
||||||
purchase.refundsPending,
|
const refundsPendingAmounts = Object.values(purchase.refunds)
|
||||||
).map((x) => Amounts.parseOrThrow(x.perm.refund_amount));
|
.filter((x) => x.type === RefundState.Pending)
|
||||||
|
.map((x) => x.refundAmount);
|
||||||
const totalRefundAmount = Amounts.sum([
|
const totalRefundAmount = Amounts.sum([
|
||||||
...refundsDoneAmounts,
|
...refundsDoneAmounts,
|
||||||
...refundsPendingAmounts,
|
...refundsPendingAmounts,
|
||||||
]).amount;
|
]).amount;
|
||||||
const refundsDoneFees = Object.values(purchase.refundsDone).map((x) =>
|
const refundsDoneFees = Object.values(purchase.refunds)
|
||||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
.filter((x) => x.type === RefundState.Applied)
|
||||||
);
|
.map((x) => x.refundFee);
|
||||||
const refundsPendingFees = Object.values(purchase.refundsPending).map((x) =>
|
const refundsPendingFees = Object.values(purchase.refunds)
|
||||||
Amounts.parseOrThrow(x.perm.refund_amount),
|
.filter((x) => x.type === RefundState.Pending)
|
||||||
);
|
.map((x) => x.refundFee);
|
||||||
const totalRefundFees = Amounts.sum([
|
const totalRefundFees = Amounts.sum([
|
||||||
...refundsDoneFees,
|
...refundsDoneFees,
|
||||||
...refundsPendingFees,
|
...refundsPendingFees,
|
||||||
|
Loading…
Reference in New Issue
Block a user