summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-05-12 15:44:48 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-05-12 15:44:48 +0530
commit67dd0eb06e04466ca01a03955ff8f75d40429c79 (patch)
tree5b4fe00a91d41a5cb758c4983575ec65e4331f76
parent6206b418ff88a238762a18e7b6eeaceafc5de294 (diff)
downloadwallet-core-67dd0eb06e04466ca01a03955ff8f75d40429c79.tar.gz
wallet-core-67dd0eb06e04466ca01a03955ff8f75d40429c79.tar.bz2
wallet-core-67dd0eb06e04466ca01a03955ff8f75d40429c79.zip
new transactions API: purchases and refunds
-rw-r--r--src/operations/pay.ts15
-rw-r--r--src/operations/refund.ts18
-rw-r--r--src/operations/transactions.ts160
-rw-r--r--src/types/dbTypes.ts4
-rw-r--r--src/types/transactions.ts8
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<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",
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;