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