From 67dd0eb06e04466ca01a03955ff8f75d40429c79 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 May 2020 15:44:48 +0530 Subject: new transactions API: purchases and refunds --- src/operations/pay.ts | 15 ++-- src/operations/refund.ts | 18 +++-- src/operations/transactions.ts | 160 +++++++++++++++++++++++++++++++++-------- src/types/dbTypes.ts | 4 ++ src/types/transactions.ts | 8 +-- 5 files changed, 161 insertions(+), 44 deletions(-) diff --git a/src/operations/pay.ts b/src/operations/pay.ts index a75284393..30ccb56c1 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -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 { +): Promise { 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", diff --git a/src/operations/refund.ts b/src/operations/refund.ts index 9b18cafd4..1ffcd2da2 100644 --- a/src/operations/refund.ts +++ b/src/operations/refund.ts @@ -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)) { diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts index 8333b66c6..e5c704b03 100644 --- a/src/operations/transactions.ts +++ b/src/operations/transactions.ts @@ -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), + + }); + } + }); }, ); diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index 07c59d4d3..eae39fff3 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -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. diff --git a/src/types/transactions.ts b/src/types/transactions.ts index d2f0f6cbc..7dda46f73 100644 --- a/src/types/transactions.ts +++ b/src/types/transactions.ts @@ -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; -- cgit v1.2.3