new transactions API: purchases and refunds

This commit is contained in:
Florian Dold 2020-05-12 15:44:48 +05:30
parent 6206b418ff
commit 67dd0eb06e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 161 additions and 44 deletions

View File

@ -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",

View File

@ -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)) {

View File

@ -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),
});
}
});
},
);

View File

@ -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.

View File

@ -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;