new transactions API: purchases and refunds
This commit is contained in:
parent
6206b418ff
commit
67dd0eb06e
@ -122,6 +122,10 @@ export interface AvailableCoinInfo {
|
|||||||
feeDeposit: AmountJson;
|
feeDeposit: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PayCostInfo {
|
||||||
|
totalCost: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the total cost of a payment to the customer.
|
* Compute the total cost of a payment to the customer.
|
||||||
*
|
*
|
||||||
@ -132,7 +136,7 @@ export interface AvailableCoinInfo {
|
|||||||
export async function getTotalPaymentCost(
|
export async function getTotalPaymentCost(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
pcs: PayCoinSelection,
|
pcs: PayCoinSelection,
|
||||||
): Promise<AmountJson> {
|
): Promise<PayCostInfo> {
|
||||||
const costs = [
|
const costs = [
|
||||||
pcs.paymentAmount,
|
pcs.paymentAmount,
|
||||||
pcs.customerDepositFees,
|
pcs.customerDepositFees,
|
||||||
@ -163,7 +167,9 @@ export async function getTotalPaymentCost(
|
|||||||
const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
|
const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
|
||||||
costs.push(refreshCost);
|
costs.push(refreshCost);
|
||||||
}
|
}
|
||||||
return Amounts.sum(costs).amount;
|
return {
|
||||||
|
totalCost: Amounts.sum(costs).amount
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -434,6 +440,7 @@ async function recordConfirmPay(
|
|||||||
contractTermsRaw: d.contractTermsRaw,
|
contractTermsRaw: d.contractTermsRaw,
|
||||||
contractData: d.contractData,
|
contractData: d.contractData,
|
||||||
lastSessionId: sessionId,
|
lastSessionId: sessionId,
|
||||||
|
payCoinSelection: coinSelection,
|
||||||
payReq,
|
payReq,
|
||||||
timestampAccept: getTimestampNow(),
|
timestampAccept: getTimestampNow(),
|
||||||
timestampLastRefundStatus: undefined,
|
timestampLastRefundStatus: undefined,
|
||||||
@ -903,8 +910,8 @@ export async function preparePayForUri(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCost = await getTotalPaymentCost(ws, res);
|
const costInfo = await getTotalPaymentCost(ws, res);
|
||||||
const totalFees = Amounts.sub(totalCost, res.paymentAmount).amount;
|
const totalFees = Amounts.sub(costInfo.totalCost, res.paymentAmount).amount;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "payment-possible",
|
status: "payment-possible",
|
||||||
|
@ -36,7 +36,6 @@ import {
|
|||||||
CoinStatus,
|
CoinStatus,
|
||||||
RefundReason,
|
RefundReason,
|
||||||
RefundEventRecord,
|
RefundEventRecord,
|
||||||
RefundInfo,
|
|
||||||
} 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";
|
||||||
@ -48,7 +47,7 @@ import {
|
|||||||
codecForMerchantRefundResponse,
|
codecForMerchantRefundResponse,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import { guardOperationException, OperationFailedError } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
import { encodeCrock } from "../crypto/talerCrypto";
|
import { encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { getTimestampNow } from "../util/time";
|
import { getTimestampNow } from "../util/time";
|
||||||
@ -159,6 +158,8 @@ async function acceptRefundResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = getTimestampNow();
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.purchases, Stores.coins, Stores.refreshGroups, Stores.refundEvents],
|
[Stores.purchases, Stores.coins, Stores.refreshGroups, Stores.refundEvents],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
@ -253,10 +254,16 @@ async function acceptRefundResponse(
|
|||||||
if (numNewRefunds === 0) {
|
if (numNewRefunds === 0) {
|
||||||
if (
|
if (
|
||||||
p.autoRefundDeadline &&
|
p.autoRefundDeadline &&
|
||||||
p.autoRefundDeadline.t_ms > getTimestampNow().t_ms
|
p.autoRefundDeadline.t_ms > now.t_ms
|
||||||
) {
|
) {
|
||||||
queryDone = false;
|
queryDone = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
p.refundGroups.push({
|
||||||
|
reason: RefundReason.NormalRefund,
|
||||||
|
refundGroupId,
|
||||||
|
timestampQueried: getTimestampNow(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(unfinishedRefunds).length != 0) {
|
if (Object.keys(unfinishedRefunds).length != 0) {
|
||||||
@ -264,14 +271,14 @@ async function acceptRefundResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (queryDone) {
|
if (queryDone) {
|
||||||
p.timestampLastRefundStatus = getTimestampNow();
|
p.timestampLastRefundStatus = now;
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo(false);
|
p.refundStatusRetryInfo = initRetryInfo(false);
|
||||||
p.refundStatusRequested = false;
|
p.refundStatusRequested = false;
|
||||||
console.log("refund query done");
|
console.log("refund query done");
|
||||||
} else {
|
} else {
|
||||||
// No error, but we need to try again!
|
// No error, but we need to try again!
|
||||||
p.timestampLastRefundStatus = getTimestampNow();
|
p.timestampLastRefundStatus = now;
|
||||||
p.refundStatusRetryInfo.retryCounter++;
|
p.refundStatusRetryInfo.retryCounter++;
|
||||||
updateRetryInfoTimeout(p.refundStatusRetryInfo);
|
updateRetryInfoTimeout(p.refundStatusRetryInfo);
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
@ -291,7 +298,6 @@ async function acceptRefundResponse(
|
|||||||
|
|
||||||
// Check if any of the refund groups are done, and we
|
// Check if any of the refund groups are done, and we
|
||||||
// can emit an corresponding event.
|
// can emit an corresponding event.
|
||||||
const now = getTimestampNow();
|
|
||||||
for (const g of Object.keys(changedGroups)) {
|
for (const g of Object.keys(changedGroups)) {
|
||||||
let groupDone = true;
|
let groupDone = true;
|
||||||
for (const pk of Object.keys(p.refundsPending)) {
|
for (const pk of Object.keys(p.refundsPending)) {
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { Stores, ProposalRecord, ReserveRecordStatus } from "../types/dbTypes";
|
import { Stores, ReserveRecordStatus, PurchaseRecord } from "../types/dbTypes";
|
||||||
import { Amounts } from "../util/amounts";
|
import { Amounts, AmountJson } from "../util/amounts";
|
||||||
import { timestampCmp } from "../util/time";
|
import { timestampCmp } from "../util/time";
|
||||||
import {
|
import {
|
||||||
TransactionsRequest,
|
TransactionsRequest,
|
||||||
@ -27,7 +27,7 @@ import {
|
|||||||
Transaction,
|
Transaction,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
} from "../types/transactions";
|
} from "../types/transactions";
|
||||||
import { OrderShortInfo } from "../types/history";
|
import { getTotalPaymentCost } from "./pay";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an event ID from the type and the primary key for the event.
|
* Create an event ID from the type and the primary key for the event.
|
||||||
@ -36,21 +36,49 @@ function makeEventId(type: TransactionType, ...args: string[]): string {
|
|||||||
return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
|
return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrderShortInfo(
|
|
||||||
proposal: ProposalRecord,
|
interface RefundStats {
|
||||||
): OrderShortInfo | undefined {
|
amountInvalid: AmountJson;
|
||||||
const download = proposal.download;
|
amountEffective: AmountJson;
|
||||||
if (!download) {
|
amountRaw: AmountJson;
|
||||||
return undefined;
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
amount: Amounts.stringify(download.contractData.amount),
|
amountEffective,
|
||||||
fulfillmentUrl: download.contractData.fulfillmentUrl,
|
amountInvalid,
|
||||||
orderId: download.contractData.orderId,
|
amountRaw,
|
||||||
merchantBaseUrl: download.contractData.merchantBaseUrl,
|
}
|
||||||
proposalId: proposal.proposalId,
|
|
||||||
summary: download.contractData.summary,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,10 +110,19 @@ export async function getTransactions(
|
|||||||
],
|
],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
|
tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
|
||||||
|
if (
|
||||||
|
transactionsRequest?.currency &&
|
||||||
|
wsr.rawWithdrawalAmount.currency != transactionsRequest.currency
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wsr.rawWithdrawalAmount.currency)
|
||||||
if (wsr.timestampFinish) {
|
if (wsr.timestampFinish) {
|
||||||
transactions.push({
|
transactions.push({
|
||||||
type: TransactionType.Withdrawal,
|
type: TransactionType.Withdrawal,
|
||||||
amountEffective: Amounts.stringify(wsr.denomsSel.totalWithdrawCost),
|
amountEffective: Amounts.stringify(
|
||||||
|
wsr.denomsSel.totalWithdrawCost,
|
||||||
|
),
|
||||||
amountRaw: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
amountRaw: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||||
@ -100,6 +137,12 @@ export async function getTransactions(
|
|||||||
});
|
});
|
||||||
|
|
||||||
tx.iter(Stores.reserves).forEach((r) => {
|
tx.iter(Stores.reserves).forEach((r) => {
|
||||||
|
if (
|
||||||
|
transactionsRequest?.currency &&
|
||||||
|
r.currency != transactionsRequest.currency
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (r.reserveStatus !== ReserveRecordStatus.WAIT_CONFIRM_BANK) {
|
if (r.reserveStatus !== ReserveRecordStatus.WAIT_CONFIRM_BANK) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -121,6 +164,63 @@ export async function getTransactions(
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.purchases).forEachAsync(async (pr) => {
|
||||||
|
if (
|
||||||
|
transactionsRequest?.currency &&
|
||||||
|
pr.contractData.amount.currency != transactionsRequest.currency
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const proposal = await tx.get(Stores.proposals, pr.proposalId);
|
||||||
|
if (!proposal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cost = await getTotalPaymentCost(ws, pr.payCoinSelection);
|
||||||
|
transactions.push({
|
||||||
|
type: TransactionType.Payment,
|
||||||
|
amountRaw: Amounts.stringify(pr.contractData.amount),
|
||||||
|
amountEffective: Amounts.stringify(cost.totalCost),
|
||||||
|
failed: false,
|
||||||
|
pending: !pr.timestampFirstSuccessfulPay,
|
||||||
|
timestamp: pr.timestampAccept,
|
||||||
|
transactionId: makeEventId(TransactionType.Payment, pr.proposalId),
|
||||||
|
info: {
|
||||||
|
fulfillmentUrl: pr.contractData.fulfillmentUrl,
|
||||||
|
merchant: {},
|
||||||
|
orderId: pr.contractData.orderId,
|
||||||
|
products: [],
|
||||||
|
summary: pr.contractData.summary,
|
||||||
|
summary_i18n: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const rg of pr.refundGroups) {
|
||||||
|
const pending = Object.keys(pr.refundsDone).length > 0;
|
||||||
|
|
||||||
|
const stats = getRefundStats(pr, rg.refundGroupId);
|
||||||
|
|
||||||
|
transactions.push({
|
||||||
|
type: TransactionType.Refund,
|
||||||
|
pending,
|
||||||
|
info: {
|
||||||
|
fulfillmentUrl: pr.contractData.fulfillmentUrl,
|
||||||
|
merchant: {},
|
||||||
|
orderId: pr.contractData.orderId,
|
||||||
|
products: [],
|
||||||
|
summary: pr.contractData.summary,
|
||||||
|
summary_i18n: {},
|
||||||
|
},
|
||||||
|
timestamp: rg.timestampQueried,
|
||||||
|
transactionId: makeEventId(TransactionType.Refund, `{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),
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
ReserveRecoupTransaction,
|
ReserveRecoupTransaction,
|
||||||
} from "./ReserveTransaction";
|
} from "./ReserveTransaction";
|
||||||
import { Timestamp, Duration, getTimestampNow } from "../util/time";
|
import { Timestamp, Duration, getTimestampNow } from "../util/time";
|
||||||
|
import { PayCoinSelection } from "../operations/pay";
|
||||||
|
|
||||||
export enum ReserveRecordStatus {
|
export enum ReserveRecordStatus {
|
||||||
/**
|
/**
|
||||||
@ -1133,6 +1134,7 @@ export const enum RefundReason {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundGroupInfo {
|
export interface RefundGroupInfo {
|
||||||
|
refundGroupId: string;
|
||||||
timestampQueried: Timestamp;
|
timestampQueried: Timestamp;
|
||||||
reason: RefundReason;
|
reason: RefundReason;
|
||||||
}
|
}
|
||||||
@ -1222,6 +1224,8 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
payReq: PayReq;
|
payReq: PayReq;
|
||||||
|
|
||||||
|
payCoinSelection: PayCoinSelection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of the first time that sending a payment to the merchant
|
* Timestamp of the first time that sending a payment to the merchant
|
||||||
* for this purchase was successful.
|
* for this purchase was successful.
|
||||||
|
@ -115,7 +115,7 @@ interface TransactionPayment extends TransactionCommon {
|
|||||||
type: TransactionType.Payment;
|
type: TransactionType.Payment;
|
||||||
|
|
||||||
// Additional information about the payment.
|
// Additional information about the payment.
|
||||||
info: TransactionInfo;
|
info: PaymentShortInfo;
|
||||||
|
|
||||||
// true if the payment failed, false otherwise.
|
// true if the payment failed, false otherwise.
|
||||||
// Note that failed payments with zero effective amount will not be returned by the API.
|
// Note that failed payments with zero effective amount will not be returned by the API.
|
||||||
@ -125,11 +125,11 @@ interface TransactionPayment extends TransactionCommon {
|
|||||||
amountRaw: AmountString;
|
amountRaw: AmountString;
|
||||||
|
|
||||||
// Amount that was paid, including deposit, wire and refresh fees.
|
// Amount that was paid, including deposit, wire and refresh fees.
|
||||||
amountEffective: AmountString;
|
amountEffective?: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface TransactionInfo {
|
interface PaymentShortInfo {
|
||||||
// Order ID, uniquely identifies the order within a merchant instance
|
// Order ID, uniquely identifies the order within a merchant instance
|
||||||
orderId: string;
|
orderId: string;
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ interface TransactionRefund extends TransactionCommon {
|
|||||||
refundedTransactionId: string;
|
refundedTransactionId: string;
|
||||||
|
|
||||||
// Additional information about the refunded payment
|
// Additional information about the refunded payment
|
||||||
info: TransactionInfo;
|
info: PaymentShortInfo;
|
||||||
|
|
||||||
// Part of the refund that couldn't be applied because the refund permissions were expired
|
// Part of the refund that couldn't be applied because the refund permissions were expired
|
||||||
amountInvalid: AmountString;
|
amountInvalid: AmountString;
|
||||||
|
Loading…
Reference in New Issue
Block a user