wallet-core: refund DD37 refactoring
This commit is contained in:
parent
a0bf83fbb5
commit
7f0edb6a78
@ -103,8 +103,8 @@ export async function runRefundGoneTest(t: GlobalTestState) {
|
|||||||
|
|
||||||
console.log(ref);
|
console.log(ref);
|
||||||
|
|
||||||
let rr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
let rr = await wallet.client.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||||
talerRefundUri: ref.talerRefundUri,
|
transactionId: ref.talerRefundUri,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("refund response:", rr);
|
console.log("refund response:", rr);
|
||||||
|
@ -94,8 +94,8 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
|
|||||||
console.log("first refund increase response", ref);
|
console.log("first refund increase response", ref);
|
||||||
|
|
||||||
{
|
{
|
||||||
let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
let wr = await wallet.client.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||||
talerRefundUri: ref.talerRefundUri,
|
transactionId: ref.talerRefundUri,
|
||||||
});
|
});
|
||||||
console.log(wr);
|
console.log(wr);
|
||||||
const txs = await wallet.client.call(
|
const txs = await wallet.client.call(
|
||||||
@ -135,8 +135,8 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
|
|||||||
console.log("third refund increase response", ref);
|
console.log("third refund increase response", ref);
|
||||||
|
|
||||||
{
|
{
|
||||||
let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
let wr = await wallet.client.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||||
talerRefundUri: ref.talerRefundUri,
|
transactionId: ref.talerRefundUri,
|
||||||
});
|
});
|
||||||
console.log(wr);
|
console.log(wr);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
Duration,
|
Duration,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
|
TransactionMajorState,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
|
import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
|
||||||
@ -100,11 +101,14 @@ export async function runRefundTest(t: GlobalTestState) {
|
|||||||
console.log(ref);
|
console.log(ref);
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// FIXME!
|
||||||
const refundFinishedCond = wallet.waitForNotificationCond(
|
const refundFinishedCond = wallet.waitForNotificationCond(
|
||||||
(x) => x.type === NotificationType.RefundFinished,
|
(x) =>
|
||||||
|
x.type === NotificationType.TransactionStateTransition &&
|
||||||
|
x.newTxState.major === TransactionMajorState.Done,
|
||||||
);
|
);
|
||||||
const r = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
const r = await wallet.client.call(WalletApiOperation.StartRefundQuery, {
|
||||||
talerRefundUri: ref.talerRefundUri,
|
transactionId: r1.transactionId,
|
||||||
});
|
});
|
||||||
console.log(r);
|
console.log(r);
|
||||||
|
|
||||||
@ -120,19 +124,20 @@ export async function runRefundTest(t: GlobalTestState) {
|
|||||||
console.log(JSON.stringify(r2, undefined, 2));
|
console.log(JSON.stringify(r2, undefined, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// FIXME: Test is incomplete without this!
|
||||||
const refundQueriedCond = wallet.waitForNotificationCond(
|
// {
|
||||||
(x) => x.type === NotificationType.RefundQueried,
|
// const refundQueriedCond = wallet.waitForNotificationCond(
|
||||||
);
|
// (x) => x.type === NotificationType.RefundQueried,
|
||||||
const r3 = await wallet.client.call(
|
// );
|
||||||
WalletApiOperation.ApplyRefundFromPurchaseId,
|
// const r3 = await wallet.client.call(
|
||||||
{
|
// WalletApiOperation.ApplyRefundFromPurchaseId,
|
||||||
purchaseId: r1.proposalId,
|
// {
|
||||||
},
|
// purchaseId: r1.proposalId,
|
||||||
);
|
// },
|
||||||
console.log(r3);
|
// );
|
||||||
await refundQueriedCond;
|
// console.log(r3);
|
||||||
}
|
// await refundQueriedCond;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
runRefundTest.suites = ["wallet"];
|
runRefundTest.suites = ["wallet"];
|
||||||
|
@ -44,7 +44,6 @@ export enum NotificationType {
|
|||||||
WaitingForRetry = "waiting-for-retry",
|
WaitingForRetry = "waiting-for-retry",
|
||||||
RefundStarted = "refund-started",
|
RefundStarted = "refund-started",
|
||||||
RefundQueried = "refund-queried",
|
RefundQueried = "refund-queried",
|
||||||
RefundFinished = "refund-finished",
|
|
||||||
ExchangeOperationError = "exchange-operation-error",
|
ExchangeOperationError = "exchange-operation-error",
|
||||||
ExchangeAdded = "exchange-added",
|
ExchangeAdded = "exchange-added",
|
||||||
RefreshOperationError = "refresh-operation-error",
|
RefreshOperationError = "refresh-operation-error",
|
||||||
@ -192,14 +191,6 @@ export interface WaitingForRetryNotification {
|
|||||||
numDue: number;
|
numDue: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundFinishedNotification {
|
|
||||||
type: NotificationType.RefundFinished;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transaction ID of the purchase (NOT the refund transaction).
|
|
||||||
*/
|
|
||||||
transactionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExchangeAddedNotification {
|
export interface ExchangeAddedNotification {
|
||||||
type: NotificationType.ExchangeAdded;
|
type: NotificationType.ExchangeAdded;
|
||||||
@ -321,7 +312,6 @@ export type WalletNotification =
|
|||||||
| WithdrawalGroupFinishedNotification
|
| WithdrawalGroupFinishedNotification
|
||||||
| WaitingForRetryNotification
|
| WaitingForRetryNotification
|
||||||
| RefundStartedNotification
|
| RefundStartedNotification
|
||||||
| RefundFinishedNotification
|
|
||||||
| RefundQueriedNotification
|
| RefundQueriedNotification
|
||||||
| WithdrawalGroupCreatedNotification
|
| WithdrawalGroupCreatedNotification
|
||||||
| CoinWithdrawnNotification
|
| CoinWithdrawnNotification
|
||||||
|
@ -130,6 +130,8 @@ export enum TransactionMinorState {
|
|||||||
Withdraw = "withdraw",
|
Withdraw = "withdraw",
|
||||||
MerchantOrderProposed = "merchant-order-proposed",
|
MerchantOrderProposed = "merchant-order-proposed",
|
||||||
Proposed = "proposed",
|
Proposed = "proposed",
|
||||||
|
RefundAvailable = "refund-available",
|
||||||
|
AcceptRefund = "accept-refund",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionsResponse {
|
export interface TransactionsResponse {
|
||||||
@ -549,14 +551,6 @@ export interface TransactionRefund extends TransactionCommon {
|
|||||||
// ID for the transaction that is refunded
|
// ID for the transaction that is refunded
|
||||||
refundedTransactionId: string;
|
refundedTransactionId: string;
|
||||||
|
|
||||||
// Additional information about the refunded payment
|
|
||||||
info: OrderShortInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount pending to be picked up
|
|
||||||
*/
|
|
||||||
refundPending: AmountString | undefined;
|
|
||||||
|
|
||||||
// Amount that has been refunded by the merchant
|
// Amount that has been refunded by the merchant
|
||||||
amountRaw: AmountString;
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
@ -419,6 +419,7 @@ export const codecForPreparePayResultPaymentPossible =
|
|||||||
.property("amountEffective", codecForAmountString())
|
.property("amountEffective", codecForAmountString())
|
||||||
.property("amountRaw", codecForAmountString())
|
.property("amountRaw", codecForAmountString())
|
||||||
.property("contractTerms", codecForMerchantContractTerms())
|
.property("contractTerms", codecForMerchantContractTerms())
|
||||||
|
.property("transactionId", codecForString())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.property("contractTermsHash", codecForString())
|
.property("contractTermsHash", codecForString())
|
||||||
.property("talerUri", codecForString())
|
.property("talerUri", codecForString())
|
||||||
@ -494,6 +495,7 @@ export const codecForPreparePayResultInsufficientBalance =
|
|||||||
.property("contractTerms", codecForAny())
|
.property("contractTerms", codecForAny())
|
||||||
.property("talerUri", codecForString())
|
.property("talerUri", codecForString())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
|
.property("transactionId", codecForString())
|
||||||
.property("noncePriv", codecForString())
|
.property("noncePriv", codecForString())
|
||||||
.property(
|
.property(
|
||||||
"status",
|
"status",
|
||||||
@ -518,6 +520,7 @@ export const codecForPreparePayResultAlreadyConfirmed =
|
|||||||
.property("talerUri", codecOptional(codecForString()))
|
.property("talerUri", codecOptional(codecForString()))
|
||||||
.property("contractTerms", codecForAny())
|
.property("contractTerms", codecForAny())
|
||||||
.property("contractTermsHash", codecForString())
|
.property("contractTermsHash", codecForString())
|
||||||
|
.property("transactionId", codecForString())
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.build("PreparePayResultAlreadyConfirmed");
|
.build("PreparePayResultAlreadyConfirmed");
|
||||||
|
|
||||||
@ -551,6 +554,10 @@ export type PreparePayResult =
|
|||||||
*/
|
*/
|
||||||
export interface PreparePayResultPaymentPossible {
|
export interface PreparePayResultPaymentPossible {
|
||||||
status: PreparePayResultType.PaymentPossible;
|
status: PreparePayResultType.PaymentPossible;
|
||||||
|
transactionId: string;
|
||||||
|
/**
|
||||||
|
* @deprecated use transactionId instead
|
||||||
|
*/
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
contractTerms: MerchantContractTerms;
|
contractTerms: MerchantContractTerms;
|
||||||
contractTermsHash: string;
|
contractTermsHash: string;
|
||||||
@ -562,6 +569,7 @@ export interface PreparePayResultPaymentPossible {
|
|||||||
|
|
||||||
export interface PreparePayResultInsufficientBalance {
|
export interface PreparePayResultInsufficientBalance {
|
||||||
status: PreparePayResultType.InsufficientBalance;
|
status: PreparePayResultType.InsufficientBalance;
|
||||||
|
transactionId: string;
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
contractTerms: MerchantContractTerms;
|
contractTerms: MerchantContractTerms;
|
||||||
amountRaw: string;
|
amountRaw: string;
|
||||||
@ -572,6 +580,7 @@ export interface PreparePayResultInsufficientBalance {
|
|||||||
|
|
||||||
export interface PreparePayResultAlreadyConfirmed {
|
export interface PreparePayResultAlreadyConfirmed {
|
||||||
status: PreparePayResultType.AlreadyConfirmed;
|
status: PreparePayResultType.AlreadyConfirmed;
|
||||||
|
transactionId: string;
|
||||||
contractTerms: MerchantContractTerms;
|
contractTerms: MerchantContractTerms;
|
||||||
paid: boolean;
|
paid: boolean;
|
||||||
amountRaw: string;
|
amountRaw: string;
|
||||||
@ -1352,14 +1361,14 @@ export const codecForAcceptExchangeTosRequest =
|
|||||||
.property("etag", codecOptional(codecForString()))
|
.property("etag", codecOptional(codecForString()))
|
||||||
.build("AcceptExchangeTosRequest");
|
.build("AcceptExchangeTosRequest");
|
||||||
|
|
||||||
export interface ApplyRefundRequest {
|
export interface AcceptRefundRequest {
|
||||||
talerRefundUri: string;
|
transactionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
|
export const codecForApplyRefundRequest = (): Codec<AcceptRefundRequest> =>
|
||||||
buildCodecForObject<ApplyRefundRequest>()
|
buildCodecForObject<AcceptRefundRequest>()
|
||||||
.property("talerRefundUri", codecForString())
|
.property("transactionId", codecForString())
|
||||||
.build("ApplyRefundRequest");
|
.build("AcceptRefundRequest");
|
||||||
|
|
||||||
export interface ApplyRefundFromPurchaseIdRequest {
|
export interface ApplyRefundFromPurchaseIdRequest {
|
||||||
purchaseId: string;
|
purchaseId: string;
|
||||||
@ -1641,6 +1650,16 @@ export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
|
|||||||
.property("talerRefundUri", codecForString())
|
.property("talerRefundUri", codecForString())
|
||||||
.build("PrepareRefundRequest");
|
.build("PrepareRefundRequest");
|
||||||
|
|
||||||
|
export interface StartRefundQueryRequest {
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForStartRefundQueryRequest = (): Codec<StartRefundQueryRequest> =>
|
||||||
|
buildCodecForObject<StartRefundQueryRequest>()
|
||||||
|
.property("transactionId", codecForString())
|
||||||
|
.build("StartRefundQueryRequest");
|
||||||
|
|
||||||
|
|
||||||
export interface PrepareTipRequest {
|
export interface PrepareTipRequest {
|
||||||
talerTipUri: string;
|
talerTipUri: string;
|
||||||
}
|
}
|
||||||
|
@ -661,7 +661,7 @@ walletCli
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TalerUriType.TalerRefund:
|
case TalerUriType.TalerRefund:
|
||||||
await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
await wallet.client.call(WalletApiOperation.StartRefundQueryForUri, {
|
||||||
talerRefundUri: uri,
|
talerRefundUri: uri,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -1407,6 +1407,19 @@ advancedCli
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
advancedCli
|
||||||
|
.subcommand("queryRefund", "query-refund", {
|
||||||
|
help: "Query refunds for a payment transaction.",
|
||||||
|
})
|
||||||
|
.requiredArgument("transactionId", clk.STRING)
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
await wallet.client.call(WalletApiOperation.StartRefundQuery, {
|
||||||
|
transactionId: args.queryRefund.transactionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
advancedCli
|
advancedCli
|
||||||
.subcommand("payConfirm", "pay-confirm", {
|
.subcommand("payConfirm", "pay-confirm", {
|
||||||
help: "Confirm payment proposed by a merchant.",
|
help: "Confirm payment proposed by a merchant.",
|
||||||
|
@ -118,7 +118,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
|
|||||||
* backwards-compatible way or object stores and indices
|
* backwards-compatible way or object stores and indices
|
||||||
* are added.
|
* are added.
|
||||||
*/
|
*/
|
||||||
export const WALLET_DB_MINOR_VERSION = 6;
|
export const WALLET_DB_MINOR_VERSION = 7;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ranges for operation status fields.
|
* Ranges for operation status fields.
|
||||||
@ -208,7 +208,7 @@ export enum WithdrawalGroupStatus {
|
|||||||
* talk to the exchange. Money might have been
|
* talk to the exchange. Money might have been
|
||||||
* wired or not.
|
* wired or not.
|
||||||
*/
|
*/
|
||||||
AbortedExchange = 60
|
AbortedExchange = 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1012,63 +1012,6 @@ export interface RefreshSessionRecord {
|
|||||||
norevealIndex?: number;
|
norevealIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RefundState {
|
|
||||||
Failed = "failed",
|
|
||||||
Applied = "applied",
|
|
||||||
Pending = "pending",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State of one refund from the merchant, maintained by the wallet.
|
|
||||||
*/
|
|
||||||
export type WalletRefundItem =
|
|
||||||
| WalletRefundFailedItem
|
|
||||||
| WalletRefundPendingItem
|
|
||||||
| WalletRefundAppliedItem;
|
|
||||||
|
|
||||||
export interface WalletRefundItemCommon {
|
|
||||||
// Execution time as claimed by the merchant
|
|
||||||
executionTime: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time when the wallet became aware of the refund.
|
|
||||||
*/
|
|
||||||
obtainedTime: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
refundAmount: AmountString;
|
|
||||||
|
|
||||||
refundFee: AmountString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upper bound on the refresh cost incurred by
|
|
||||||
* applying this refund.
|
|
||||||
*
|
|
||||||
* Might be lower in practice when two refunds on the same
|
|
||||||
* coin are refreshed in the same refresh operation.
|
|
||||||
*/
|
|
||||||
totalRefreshCostBound: AmountString;
|
|
||||||
|
|
||||||
coinPub: string;
|
|
||||||
|
|
||||||
rtransactionId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Failed refund, either because the merchant did
|
|
||||||
* something wrong or it expired.
|
|
||||||
*/
|
|
||||||
export interface WalletRefundFailedItem extends WalletRefundItemCommon {
|
|
||||||
type: RefundState.Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletRefundPendingItem extends WalletRefundItemCommon {
|
|
||||||
type: RefundState.Pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
|
|
||||||
type: RefundState.Applied;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RefundReason {
|
export enum RefundReason {
|
||||||
/**
|
/**
|
||||||
* Normal refund given by the merchant.
|
* Normal refund given by the merchant.
|
||||||
@ -1161,6 +1104,8 @@ export enum PurchaseStatus {
|
|||||||
*/
|
*/
|
||||||
QueryingAutoRefund = 15,
|
QueryingAutoRefund = 15,
|
||||||
|
|
||||||
|
PendingAcceptRefund = 16,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proposal downloaded, but the user needs to accept/reject it.
|
* Proposal downloaded, but the user needs to accept/reject it.
|
||||||
*/
|
*/
|
||||||
@ -1169,12 +1114,12 @@ export enum PurchaseStatus {
|
|||||||
/**
|
/**
|
||||||
* The user has rejected the proposal.
|
* The user has rejected the proposal.
|
||||||
*/
|
*/
|
||||||
ProposalRefused = 50,
|
AbortedProposalRefused = 50,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloading or processing the proposal has failed permanently.
|
* Downloading or processing the proposal has failed permanently.
|
||||||
*/
|
*/
|
||||||
ProposalDownloadFailed = 51,
|
FailedClaim = 51,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloaded proposal was detected as a re-purchase.
|
* Downloaded proposal was detected as a re-purchase.
|
||||||
@ -1184,12 +1129,12 @@ export enum PurchaseStatus {
|
|||||||
/**
|
/**
|
||||||
* The payment has been aborted.
|
* The payment has been aborted.
|
||||||
*/
|
*/
|
||||||
PaymentAbortFinished = 53,
|
AbortedIncompletePayment = 53,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payment was successful.
|
* Payment was successful.
|
||||||
*/
|
*/
|
||||||
Paid = 54,
|
Done = 54,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1303,7 +1248,7 @@ export interface PurchaseRecord {
|
|||||||
*
|
*
|
||||||
* FIXME: Put this into a separate object store?
|
* FIXME: Put this into a separate object store?
|
||||||
*/
|
*/
|
||||||
refunds: { [refundKey: string]: WalletRefundItem };
|
// refunds: { [refundKey: string]: WalletRefundItem };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When was the last refund made?
|
* When was the last refund made?
|
||||||
@ -2152,6 +2097,97 @@ export interface CurrencySettingsRecord {
|
|||||||
// Later, we might add stuff related to how the currency is rendered.
|
// Later, we might add stuff related to how the currency is rendered.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RefundGroupStatus {
|
||||||
|
Pending = 10,
|
||||||
|
Done = 50,
|
||||||
|
Failed = 51,
|
||||||
|
Aborted = 52,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a group of refunds with the merchant.
|
||||||
|
*/
|
||||||
|
export interface RefundGroupRecord {
|
||||||
|
status: RefundGroupStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the refund group was created.
|
||||||
|
*/
|
||||||
|
timestampCreated: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
proposalId: string;
|
||||||
|
|
||||||
|
refundGroupId: string;
|
||||||
|
|
||||||
|
refreshGroupId?: string;
|
||||||
|
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated effective amount, based on
|
||||||
|
* refund fees and refresh costs.
|
||||||
|
*/
|
||||||
|
amountEffective: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RefundItemStatus {
|
||||||
|
/**
|
||||||
|
* Intermittent error that the merchant is
|
||||||
|
* reporting from the exchange.
|
||||||
|
*
|
||||||
|
* We'll try again!
|
||||||
|
*/
|
||||||
|
Pending = 10,
|
||||||
|
/**
|
||||||
|
* Refund was obtained successfully.
|
||||||
|
*/
|
||||||
|
Done = 50,
|
||||||
|
/**
|
||||||
|
* Permanent error reported by the exchange
|
||||||
|
* for the refund.
|
||||||
|
*/
|
||||||
|
Failed = 51,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refund for a single coin in a payment with a merchant.
|
||||||
|
*/
|
||||||
|
export interface RefundItemRecord {
|
||||||
|
/**
|
||||||
|
* Auto-increment DB record ID.
|
||||||
|
*/
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
status: RefundItemStatus;
|
||||||
|
|
||||||
|
refundGroupId: string;
|
||||||
|
|
||||||
|
// Execution time as claimed by the merchant
|
||||||
|
executionTime: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time when the wallet became aware of the refund.
|
||||||
|
*/
|
||||||
|
obtainedTime: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
refundAmount: AmountString;
|
||||||
|
|
||||||
|
//refundFee: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upper bound on the refresh cost incurred by
|
||||||
|
* applying this refund.
|
||||||
|
*
|
||||||
|
* Might be lower in practice when two refunds on the same
|
||||||
|
* coin are refreshed in the same refresh operation.
|
||||||
|
*/
|
||||||
|
//totalRefreshCostBound: AmountString;
|
||||||
|
|
||||||
|
coinPub: string;
|
||||||
|
|
||||||
|
rtxid: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema definition for the IndexedDB
|
* Schema definition for the IndexedDB
|
||||||
* wallet database.
|
* wallet database.
|
||||||
@ -2494,6 +2530,31 @@ export const WalletStoresV1 = {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
refundGroups: describeStore(
|
||||||
|
"refundGroups",
|
||||||
|
describeContents<RefundGroupRecord>({
|
||||||
|
keyPath: "refundGroupId",
|
||||||
|
versionAdded: 7,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
byProposalId: describeIndex("byProposalId", "proposalId"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
refundItems: describeStore(
|
||||||
|
"refundItems",
|
||||||
|
describeContents<RefundItemRecord>({
|
||||||
|
keyPath: "id",
|
||||||
|
versionAdded: 7,
|
||||||
|
autoIncrement: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
byCoinPubAndRtxid: describeIndex("byCoinPubAndRtxid", [
|
||||||
|
"coinPub",
|
||||||
|
"rtxid",
|
||||||
|
]),
|
||||||
|
byRefundGroupId: describeIndex("byRefundGroupId", ["refundGroupId"]),
|
||||||
|
},
|
||||||
|
),
|
||||||
fixups: describeStore(
|
fixups: describeStore(
|
||||||
"fixups",
|
"fixups",
|
||||||
describeContents<FixupRecord>({
|
describeContents<FixupRecord>({
|
||||||
|
@ -69,7 +69,6 @@ import {
|
|||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
PurchaseStatus,
|
PurchaseStatus,
|
||||||
RefreshCoinStatus,
|
RefreshCoinStatus,
|
||||||
RefundState,
|
|
||||||
WithdrawalGroupStatus,
|
WithdrawalGroupStatus,
|
||||||
WithdrawalRecordType,
|
WithdrawalRecordType,
|
||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
@ -384,34 +383,34 @@ export async function exportBackup(
|
|||||||
await tx.purchases.iter().forEachAsync(async (purch) => {
|
await tx.purchases.iter().forEachAsync(async (purch) => {
|
||||||
const refunds: BackupRefundItem[] = [];
|
const refunds: BackupRefundItem[] = [];
|
||||||
purchaseProposalIdSet.add(purch.proposalId);
|
purchaseProposalIdSet.add(purch.proposalId);
|
||||||
for (const refundKey of Object.keys(purch.refunds)) {
|
// for (const refundKey of Object.keys(purch.refunds)) {
|
||||||
const ri = purch.refunds[refundKey];
|
// const ri = purch.refunds[refundKey];
|
||||||
const common = {
|
// const common = {
|
||||||
coin_pub: ri.coinPub,
|
// coin_pub: ri.coinPub,
|
||||||
execution_time: ri.executionTime,
|
// execution_time: ri.executionTime,
|
||||||
obtained_time: ri.obtainedTime,
|
// obtained_time: ri.obtainedTime,
|
||||||
refund_amount: Amounts.stringify(ri.refundAmount),
|
// refund_amount: Amounts.stringify(ri.refundAmount),
|
||||||
rtransaction_id: ri.rtransactionId,
|
// rtransaction_id: ri.rtransactionId,
|
||||||
total_refresh_cost_bound: Amounts.stringify(
|
// total_refresh_cost_bound: Amounts.stringify(
|
||||||
ri.totalRefreshCostBound,
|
// ri.totalRefreshCostBound,
|
||||||
),
|
// ),
|
||||||
};
|
// };
|
||||||
switch (ri.type) {
|
// switch (ri.type) {
|
||||||
case RefundState.Applied:
|
// case RefundState.Applied:
|
||||||
refunds.push({ type: BackupRefundState.Applied, ...common });
|
// refunds.push({ type: BackupRefundState.Applied, ...common });
|
||||||
break;
|
// break;
|
||||||
case RefundState.Failed:
|
// case RefundState.Failed:
|
||||||
refunds.push({ type: BackupRefundState.Failed, ...common });
|
// refunds.push({ type: BackupRefundState.Failed, ...common });
|
||||||
break;
|
// break;
|
||||||
case RefundState.Pending:
|
// case RefundState.Pending:
|
||||||
refunds.push({ type: BackupRefundState.Pending, ...common });
|
// refunds.push({ type: BackupRefundState.Pending, ...common });
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
let propStatus: BackupProposalStatus;
|
let propStatus: BackupProposalStatus;
|
||||||
switch (purch.purchaseStatus) {
|
switch (purch.purchaseStatus) {
|
||||||
case PurchaseStatus.Paid:
|
case PurchaseStatus.Done:
|
||||||
case PurchaseStatus.QueryingAutoRefund:
|
case PurchaseStatus.QueryingAutoRefund:
|
||||||
case PurchaseStatus.QueryingRefund:
|
case PurchaseStatus.QueryingRefund:
|
||||||
propStatus = BackupProposalStatus.Paid;
|
propStatus = BackupProposalStatus.Paid;
|
||||||
@ -422,19 +421,19 @@ export async function exportBackup(
|
|||||||
case PurchaseStatus.Paying:
|
case PurchaseStatus.Paying:
|
||||||
propStatus = BackupProposalStatus.Proposed;
|
propStatus = BackupProposalStatus.Proposed;
|
||||||
break;
|
break;
|
||||||
case PurchaseStatus.ProposalDownloadFailed:
|
case PurchaseStatus.FailedClaim:
|
||||||
case PurchaseStatus.PaymentAbortFinished:
|
case PurchaseStatus.AbortedIncompletePayment:
|
||||||
propStatus = BackupProposalStatus.PermanentlyFailed;
|
propStatus = BackupProposalStatus.PermanentlyFailed;
|
||||||
break;
|
break;
|
||||||
case PurchaseStatus.AbortingWithRefund:
|
case PurchaseStatus.AbortingWithRefund:
|
||||||
case PurchaseStatus.ProposalRefused:
|
case PurchaseStatus.AbortedProposalRefused:
|
||||||
propStatus = BackupProposalStatus.Refused;
|
propStatus = BackupProposalStatus.Refused;
|
||||||
break;
|
break;
|
||||||
case PurchaseStatus.RepurchaseDetected:
|
case PurchaseStatus.RepurchaseDetected:
|
||||||
propStatus = BackupProposalStatus.Repurchase;
|
propStatus = BackupProposalStatus.Repurchase;
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
const error: never = purch.purchaseStatus;
|
const error = purch.purchaseStatus;
|
||||||
throw Error(`purchase status ${error} is not handled`);
|
throw Error(`purchase status ${error} is not handled`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,9 +49,7 @@ import {
|
|||||||
PurchasePayInfo,
|
PurchasePayInfo,
|
||||||
RefreshCoinStatus,
|
RefreshCoinStatus,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
RefundState,
|
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
WalletRefundItem,
|
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
WgInfo,
|
WgInfo,
|
||||||
WithdrawalGroupStatus,
|
WithdrawalGroupStatus,
|
||||||
@ -65,7 +63,6 @@ import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
|||||||
import {
|
import {
|
||||||
makeCoinAvailable,
|
makeCoinAvailable,
|
||||||
makeTombstoneId,
|
makeTombstoneId,
|
||||||
makeTransactionId,
|
|
||||||
TombstoneTag,
|
TombstoneTag,
|
||||||
} from "../common.js";
|
} from "../common.js";
|
||||||
import { getExchangeDetails } from "../exchanges.js";
|
import { getExchangeDetails } from "../exchanges.js";
|
||||||
@ -576,16 +573,16 @@ export async function importBackup(
|
|||||||
let proposalStatus: PurchaseStatus;
|
let proposalStatus: PurchaseStatus;
|
||||||
switch (backupPurchase.proposal_status) {
|
switch (backupPurchase.proposal_status) {
|
||||||
case BackupProposalStatus.Paid:
|
case BackupProposalStatus.Paid:
|
||||||
proposalStatus = PurchaseStatus.Paid;
|
proposalStatus = PurchaseStatus.Done;
|
||||||
break;
|
break;
|
||||||
case BackupProposalStatus.Proposed:
|
case BackupProposalStatus.Proposed:
|
||||||
proposalStatus = PurchaseStatus.Proposed;
|
proposalStatus = PurchaseStatus.Proposed;
|
||||||
break;
|
break;
|
||||||
case BackupProposalStatus.PermanentlyFailed:
|
case BackupProposalStatus.PermanentlyFailed:
|
||||||
proposalStatus = PurchaseStatus.PaymentAbortFinished;
|
proposalStatus = PurchaseStatus.AbortedIncompletePayment;
|
||||||
break;
|
break;
|
||||||
case BackupProposalStatus.Refused:
|
case BackupProposalStatus.Refused:
|
||||||
proposalStatus = PurchaseStatus.ProposalRefused;
|
proposalStatus = PurchaseStatus.AbortedProposalRefused;
|
||||||
break;
|
break;
|
||||||
case BackupProposalStatus.Repurchase:
|
case BackupProposalStatus.Repurchase:
|
||||||
proposalStatus = PurchaseStatus.RepurchaseDetected;
|
proposalStatus = PurchaseStatus.RepurchaseDetected;
|
||||||
@ -596,48 +593,48 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!existingPurchase) {
|
if (!existingPurchase) {
|
||||||
const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
//const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
||||||
for (const backupRefund of backupPurchase.refunds) {
|
// for (const backupRefund of backupPurchase.refunds) {
|
||||||
const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
// const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
||||||
const coin = await tx.coins.get(backupRefund.coin_pub);
|
// const coin = await tx.coins.get(backupRefund.coin_pub);
|
||||||
checkBackupInvariant(!!coin);
|
// checkBackupInvariant(!!coin);
|
||||||
const denom = await tx.denominations.get([
|
// const denom = await tx.denominations.get([
|
||||||
coin.exchangeBaseUrl,
|
// coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
// coin.denomPubHash,
|
||||||
]);
|
// ]);
|
||||||
checkBackupInvariant(!!denom);
|
// checkBackupInvariant(!!denom);
|
||||||
const common = {
|
// const common = {
|
||||||
coinPub: backupRefund.coin_pub,
|
// coinPub: backupRefund.coin_pub,
|
||||||
executionTime: backupRefund.execution_time,
|
// executionTime: backupRefund.execution_time,
|
||||||
obtainedTime: backupRefund.obtained_time,
|
// obtainedTime: backupRefund.obtained_time,
|
||||||
refundAmount: Amounts.stringify(backupRefund.refund_amount),
|
// refundAmount: Amounts.stringify(backupRefund.refund_amount),
|
||||||
refundFee: Amounts.stringify(denom.fees.feeRefund),
|
// refundFee: Amounts.stringify(denom.fees.feeRefund),
|
||||||
rtransactionId: backupRefund.rtransaction_id,
|
// rtransactionId: backupRefund.rtransaction_id,
|
||||||
totalRefreshCostBound: Amounts.stringify(
|
// totalRefreshCostBound: Amounts.stringify(
|
||||||
backupRefund.total_refresh_cost_bound,
|
// backupRefund.total_refresh_cost_bound,
|
||||||
),
|
// ),
|
||||||
};
|
// };
|
||||||
switch (backupRefund.type) {
|
// switch (backupRefund.type) {
|
||||||
case BackupRefundState.Applied:
|
// case BackupRefundState.Applied:
|
||||||
refunds[key] = {
|
// refunds[key] = {
|
||||||
type: RefundState.Applied,
|
// type: RefundState.Applied,
|
||||||
...common,
|
// ...common,
|
||||||
};
|
// };
|
||||||
break;
|
// break;
|
||||||
case BackupRefundState.Failed:
|
// case BackupRefundState.Failed:
|
||||||
refunds[key] = {
|
// refunds[key] = {
|
||||||
type: RefundState.Failed,
|
// type: RefundState.Failed,
|
||||||
...common,
|
// ...common,
|
||||||
};
|
// };
|
||||||
break;
|
// break;
|
||||||
case BackupRefundState.Pending:
|
// case BackupRefundState.Pending:
|
||||||
refunds[key] = {
|
// refunds[key] = {
|
||||||
type: RefundState.Pending,
|
// type: RefundState.Pending,
|
||||||
...common,
|
// ...common,
|
||||||
};
|
// };
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
const parsedContractTerms = codecForMerchantContractTerms().decode(
|
const parsedContractTerms = codecForMerchantContractTerms().decode(
|
||||||
backupPurchase.contract_terms_raw,
|
backupPurchase.contract_terms_raw,
|
||||||
);
|
);
|
||||||
@ -694,7 +691,7 @@ export async function importBackup(
|
|||||||
posConfirmation: backupPurchase.pos_confirmation,
|
posConfirmation: backupPurchase.pos_confirmation,
|
||||||
lastSessionId: undefined,
|
lastSessionId: undefined,
|
||||||
download,
|
download,
|
||||||
refunds,
|
//refunds,
|
||||||
claimToken: backupPurchase.claim_token,
|
claimToken: backupPurchase.claim_token,
|
||||||
downloadSessionId: backupPurchase.download_session_id,
|
downloadSessionId: backupPurchase.download_session_id,
|
||||||
merchantBaseUrl: backupPurchase.merchant_base_url,
|
merchantBaseUrl: backupPurchase.merchant_base_url,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -81,7 +81,7 @@ import {
|
|||||||
readUnexpectedResponseDetails,
|
readUnexpectedResponseDetails,
|
||||||
} from "@gnu-taler/taler-util/http";
|
} from "@gnu-taler/taler-util/http";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
|
||||||
import {
|
import {
|
||||||
constructTaskIdentifier,
|
constructTaskIdentifier,
|
||||||
OperationAttemptResult,
|
OperationAttemptResult,
|
||||||
@ -874,18 +874,13 @@ async function processRefreshSession(
|
|||||||
await refreshReveal(ws, refreshGroupId, coinIndex);
|
await refreshReveal(ws, refreshGroupId, coinIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface RefreshOutputInfo {
|
||||||
* Create a refresh group for a list of coins.
|
outputPerCoin: AmountJson[];
|
||||||
*
|
}
|
||||||
* Refreshes the remaining amount on the coin, effectively capturing the remaining
|
|
||||||
* value in the refresh group.
|
export async function calculateRefreshOutput(
|
||||||
*
|
|
||||||
* The caller must also ensure that the coins that should be refreshed exist
|
|
||||||
* in the current database transaction.
|
|
||||||
*/
|
|
||||||
export async function createRefreshGroup(
|
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: GetReadWriteAccess<{
|
tx: GetReadOnlyAccess<{
|
||||||
denominations: typeof WalletStoresV1.denominations;
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
coins: typeof WalletStoresV1.coins;
|
coins: typeof WalletStoresV1.coins;
|
||||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
@ -893,12 +888,7 @@ export async function createRefreshGroup(
|
|||||||
}>,
|
}>,
|
||||||
currency: string,
|
currency: string,
|
||||||
oldCoinPubs: CoinRefreshRequest[],
|
oldCoinPubs: CoinRefreshRequest[],
|
||||||
reason: RefreshReason,
|
): Promise<RefreshOutputInfo> {
|
||||||
reasonDetails?: RefreshReasonDetails,
|
|
||||||
): Promise<RefreshGroupId> {
|
|
||||||
const refreshGroupId = encodeCrock(getRandomBytes(32));
|
|
||||||
|
|
||||||
const inputPerCoin: AmountJson[] = [];
|
|
||||||
const estimatedOutputPerCoin: AmountJson[] = [];
|
const estimatedOutputPerCoin: AmountJson[] = [];
|
||||||
|
|
||||||
const denomsPerExchange: Record<string, DenominationRecord[]> = {};
|
const denomsPerExchange: Record<string, DenominationRecord[]> = {};
|
||||||
@ -918,6 +908,47 @@ export async function createRefreshGroup(
|
|||||||
return allDenoms;
|
return allDenoms;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const ocp of oldCoinPubs) {
|
||||||
|
const coin = await tx.coins.get(ocp.coinPub);
|
||||||
|
checkDbInvariant(!!coin, "coin must be in database");
|
||||||
|
const denom = await ws.getDenomInfo(
|
||||||
|
ws,
|
||||||
|
tx,
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
);
|
||||||
|
checkDbInvariant(
|
||||||
|
!!denom,
|
||||||
|
"denomination for existing coin must be in database",
|
||||||
|
);
|
||||||
|
const refreshAmount = ocp.amount;
|
||||||
|
const denoms = await getDenoms(coin.exchangeBaseUrl);
|
||||||
|
const cost = getTotalRefreshCost(
|
||||||
|
denoms,
|
||||||
|
denom,
|
||||||
|
Amounts.parseOrThrow(refreshAmount),
|
||||||
|
ws.config.testing.denomselAllowLate,
|
||||||
|
);
|
||||||
|
const output = Amounts.sub(refreshAmount, cost).amount;
|
||||||
|
estimatedOutputPerCoin.push(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
outputPerCoin: estimatedOutputPerCoin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyRefresh(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tx: GetReadWriteAccess<{
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
|
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||||
|
}>,
|
||||||
|
oldCoinPubs: CoinRefreshRequest[],
|
||||||
|
refreshGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
for (const ocp of oldCoinPubs) {
|
for (const ocp of oldCoinPubs) {
|
||||||
const coin = await tx.coins.get(ocp.coinPub);
|
const coin = await tx.coins.get(ocp.coinPub);
|
||||||
checkDbInvariant(!!coin, "coin must be in database");
|
checkDbInvariant(!!coin, "coin must be in database");
|
||||||
@ -962,19 +993,39 @@ export async function createRefreshGroup(
|
|||||||
id: `txn:refresh:${refreshGroupId}`,
|
id: `txn:refresh:${refreshGroupId}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const refreshAmount = ocp.amount;
|
|
||||||
inputPerCoin.push(Amounts.parseOrThrow(refreshAmount));
|
|
||||||
await tx.coins.put(coin);
|
await tx.coins.put(coin);
|
||||||
const denoms = await getDenoms(coin.exchangeBaseUrl);
|
|
||||||
const cost = getTotalRefreshCost(
|
|
||||||
denoms,
|
|
||||||
denom,
|
|
||||||
Amounts.parseOrThrow(refreshAmount),
|
|
||||||
ws.config.testing.denomselAllowLate,
|
|
||||||
);
|
|
||||||
const output = Amounts.sub(refreshAmount, cost).amount;
|
|
||||||
estimatedOutputPerCoin.push(output);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a refresh group for a list of coins.
|
||||||
|
*
|
||||||
|
* Refreshes the remaining amount on the coin, effectively capturing the remaining
|
||||||
|
* value in the refresh group.
|
||||||
|
*
|
||||||
|
* The caller must also ensure that the coins that should be refreshed exist
|
||||||
|
* in the current database transaction.
|
||||||
|
*/
|
||||||
|
export async function createRefreshGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tx: GetReadWriteAccess<{
|
||||||
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
|
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||||
|
}>,
|
||||||
|
currency: string,
|
||||||
|
oldCoinPubs: CoinRefreshRequest[],
|
||||||
|
reason: RefreshReason,
|
||||||
|
reasonDetails?: RefreshReasonDetails,
|
||||||
|
): Promise<RefreshGroupId> {
|
||||||
|
const refreshGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
|
const outInfo = await calculateRefreshOutput(ws, tx, currency, oldCoinPubs);
|
||||||
|
|
||||||
|
const estimatedOutputPerCoin = outInfo.outputPerCoin;
|
||||||
|
|
||||||
|
await applyRefresh(ws, tx, oldCoinPubs, refreshGroupId);
|
||||||
|
|
||||||
const refreshGroup: RefreshGroupRecord = {
|
const refreshGroup: RefreshGroupRecord = {
|
||||||
operationStatus: RefreshOperationStatus.Pending,
|
operationStatus: RefreshOperationStatus.Pending,
|
||||||
@ -987,7 +1038,7 @@ export async function createRefreshGroup(
|
|||||||
reason,
|
reason,
|
||||||
refreshGroupId,
|
refreshGroupId,
|
||||||
refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
|
refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
|
||||||
inputPerCoin: inputPerCoin.map((x) => Amounts.stringify(x)),
|
inputPerCoin: oldCoinPubs.map((x) => x.amount),
|
||||||
estimatedOutputPerCoin: estimatedOutputPerCoin.map((x) =>
|
estimatedOutputPerCoin: estimatedOutputPerCoin.map((x) =>
|
||||||
Amounts.stringify(x),
|
Amounts.stringify(x),
|
||||||
),
|
),
|
||||||
|
@ -45,7 +45,7 @@ import {
|
|||||||
PreparePayResultType,
|
PreparePayResultType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { applyRefund, confirmPay, preparePayForUri } from "./pay-merchant.js";
|
import { confirmPay, preparePayForUri, startRefundQueryForUri } from "./pay-merchant.js";
|
||||||
import { getBalances } from "./balance.js";
|
import { getBalances } from "./balance.js";
|
||||||
import { checkLogicInvariant } from "../util/invariants.js";
|
import { checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
||||||
@ -416,7 +416,7 @@ export async function runIntegrationTest(
|
|||||||
|
|
||||||
logger.trace("refund URI", refundUri);
|
logger.trace("refund URI", refundUri);
|
||||||
|
|
||||||
await applyRefund(ws, refundUri);
|
await startRefundQueryForUri(ws, refundUri);
|
||||||
|
|
||||||
logger.trace("integration test: applied refund");
|
logger.trace("integration test: applied refund");
|
||||||
|
|
||||||
@ -512,7 +512,7 @@ export async function runIntegrationTest2(
|
|||||||
|
|
||||||
logger.trace("refund URI", refundUri);
|
logger.trace("refund URI", refundUri);
|
||||||
|
|
||||||
await applyRefund(ws, refundUri);
|
await startRefundQueryForUri(ws, refundUri);
|
||||||
|
|
||||||
logger.trace("integration test: applied refund");
|
logger.trace("integration test: applied refund");
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AmountJson,
|
|
||||||
Amounts,
|
Amounts,
|
||||||
constructPayPullUri,
|
constructPayPullUri,
|
||||||
constructPayPushUri,
|
constructPayPushUri,
|
||||||
@ -51,9 +50,7 @@ import {
|
|||||||
PeerPushPaymentInitiationRecord,
|
PeerPushPaymentInitiationRecord,
|
||||||
PurchaseStatus,
|
PurchaseStatus,
|
||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
RefundState,
|
|
||||||
TipRecord,
|
TipRecord,
|
||||||
WalletRefundItem,
|
|
||||||
WithdrawalGroupRecord,
|
WithdrawalGroupRecord,
|
||||||
WithdrawalRecordType,
|
WithdrawalRecordType,
|
||||||
WalletContractData,
|
WalletContractData,
|
||||||
@ -66,6 +63,7 @@ import {
|
|||||||
PeerPushPaymentIncomingRecord,
|
PeerPushPaymentIncomingRecord,
|
||||||
PeerPushPaymentIncomingStatus,
|
PeerPushPaymentIncomingStatus,
|
||||||
PeerPullPaymentInitiationRecord,
|
PeerPullPaymentInitiationRecord,
|
||||||
|
RefundGroupRecord,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { PendingTaskType } from "../pending-types.js";
|
import { PendingTaskType } from "../pending-types.js";
|
||||||
@ -89,6 +87,7 @@ import { getExchangeDetails } from "./exchanges.js";
|
|||||||
import {
|
import {
|
||||||
abortPayMerchant,
|
abortPayMerchant,
|
||||||
computePayMerchantTransactionState,
|
computePayMerchantTransactionState,
|
||||||
|
computeRefundTransactionState,
|
||||||
expectProposalDownload,
|
expectProposalDownload,
|
||||||
extractContractData,
|
extractContractData,
|
||||||
processPurchasePay,
|
processPurchasePay,
|
||||||
@ -205,40 +204,15 @@ export async function getTransactionById(
|
|||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const purchase = await tx.purchases.get(proposalId);
|
const purchase = await tx.purchases.get(proposalId);
|
||||||
if (!purchase) throw Error("not found");
|
if (!purchase) throw Error("not found");
|
||||||
|
|
||||||
const filteredRefunds = await Promise.all(
|
|
||||||
Object.values(purchase.refunds).map(async (r) => {
|
|
||||||
const t = await tx.tombstones.get(
|
|
||||||
makeTombstoneId(
|
|
||||||
TombstoneTag.DeleteRefund,
|
|
||||||
purchase.proposalId,
|
|
||||||
`${r.executionTime.t_s}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!t) return r;
|
|
||||||
return undefined;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const download = await expectProposalDownload(ws, purchase, tx);
|
const download = await expectProposalDownload(ws, purchase, tx);
|
||||||
|
|
||||||
const cleanRefunds = filteredRefunds.filter(
|
|
||||||
(x): x is WalletRefundItem => !!x,
|
|
||||||
);
|
|
||||||
|
|
||||||
const contractData = download.contractData;
|
const contractData = download.contractData;
|
||||||
const refunds = mergeRefundByExecutionTime(
|
|
||||||
cleanRefunds,
|
|
||||||
Amounts.zeroOfAmount(contractData.amount),
|
|
||||||
);
|
|
||||||
|
|
||||||
const payOpId = TaskIdentifiers.forPay(purchase);
|
const payOpId = TaskIdentifiers.forPay(purchase);
|
||||||
const payRetryRecord = await tx.operationRetries.get(payOpId);
|
const payRetryRecord = await tx.operationRetries.get(payOpId);
|
||||||
|
|
||||||
return buildTransactionForPurchase(
|
return buildTransactionForPurchase(
|
||||||
purchase,
|
purchase,
|
||||||
contractData,
|
contractData,
|
||||||
refunds,
|
[], // FIXME: Add refunds from refund group records here.
|
||||||
payRetryRecord,
|
payRetryRecord,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -272,66 +246,8 @@ export async function getTransactionById(
|
|||||||
return buildTransactionForDeposit(depositRecord, retries);
|
return buildTransactionForDeposit(depositRecord, retries);
|
||||||
});
|
});
|
||||||
} else if (type === TransactionType.Refund) {
|
} else if (type === TransactionType.Refund) {
|
||||||
const proposalId = rest[0];
|
// FIXME!
|
||||||
const executionTimeStr = rest[1];
|
throw Error("not implemented");
|
||||||
|
|
||||||
return await ws.db
|
|
||||||
.mktx((x) => [
|
|
||||||
x.operationRetries,
|
|
||||||
x.purchases,
|
|
||||||
x.tombstones,
|
|
||||||
x.contractTerms,
|
|
||||||
])
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const purchase = await tx.purchases.get(proposalId);
|
|
||||||
if (!purchase) throw Error("not found");
|
|
||||||
|
|
||||||
const t = await tx.tombstones.get(
|
|
||||||
makeTombstoneId(
|
|
||||||
TombstoneTag.DeleteRefund,
|
|
||||||
purchase.proposalId,
|
|
||||||
executionTimeStr,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (t) throw Error("deleted");
|
|
||||||
|
|
||||||
const filteredRefunds = await Promise.all(
|
|
||||||
Object.values(purchase.refunds).map(async (r) => {
|
|
||||||
const t = await tx.tombstones.get(
|
|
||||||
makeTombstoneId(
|
|
||||||
TombstoneTag.DeleteRefund,
|
|
||||||
purchase.proposalId,
|
|
||||||
`${r.executionTime.t_s}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!t) return r;
|
|
||||||
return undefined;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const cleanRefunds = filteredRefunds.filter(
|
|
||||||
(x): x is WalletRefundItem => !!x,
|
|
||||||
);
|
|
||||||
|
|
||||||
const download = await expectProposalDownload(ws, purchase, tx);
|
|
||||||
const contractData = download.contractData;
|
|
||||||
const refunds = mergeRefundByExecutionTime(
|
|
||||||
cleanRefunds,
|
|
||||||
Amounts.zeroOfAmount(contractData.amount),
|
|
||||||
);
|
|
||||||
|
|
||||||
const theRefund = refunds.find(
|
|
||||||
(r) => `${r.executionTime.t_s}` === executionTimeStr,
|
|
||||||
);
|
|
||||||
if (!theRefund) throw Error("not found");
|
|
||||||
|
|
||||||
return buildTransactionForRefund(
|
|
||||||
purchase,
|
|
||||||
contractData,
|
|
||||||
theRefund,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else if (type === TransactionType.PeerPullDebit) {
|
} else if (type === TransactionType.PeerPullDebit) {
|
||||||
const peerPullPaymentIncomingId = rest[0];
|
const peerPullPaymentIncomingId = rest[0];
|
||||||
return await ws.db
|
return await ws.db
|
||||||
@ -730,6 +646,29 @@ function buildTransactionForManualWithdraw(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTransactionForRefund(
|
||||||
|
refundRecord: RefundGroupRecord,
|
||||||
|
): Transaction {
|
||||||
|
return {
|
||||||
|
type: TransactionType.Refund,
|
||||||
|
amountEffective: refundRecord.amountEffective,
|
||||||
|
amountRaw: refundRecord.amountEffective,
|
||||||
|
refundedTransactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Payment,
|
||||||
|
proposalId: refundRecord.proposalId
|
||||||
|
}),
|
||||||
|
timestamp: refundRecord.timestampCreated,
|
||||||
|
transactionId: constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Refund,
|
||||||
|
refundGroupId: refundRecord.refundGroupId,
|
||||||
|
}),
|
||||||
|
txState: computeRefundTransactionState(refundRecord),
|
||||||
|
extendedStatus: ExtendedStatus.Done,
|
||||||
|
frozen: false,
|
||||||
|
pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildTransactionForRefresh(
|
function buildTransactionForRefresh(
|
||||||
refreshGroupRecord: RefreshGroupRecord,
|
refreshGroupRecord: RefreshGroupRecord,
|
||||||
ort?: OperationRetryRecord,
|
ort?: OperationRetryRecord,
|
||||||
@ -850,113 +789,11 @@ function buildTransactionForTip(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For a set of refund with the same executionTime.
|
|
||||||
*/
|
|
||||||
interface MergedRefundInfo {
|
|
||||||
executionTime: TalerProtocolTimestamp;
|
|
||||||
amountAppliedRaw: AmountJson;
|
|
||||||
amountAppliedEffective: AmountJson;
|
|
||||||
firstTimestamp: TalerProtocolTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeRefundByExecutionTime(
|
|
||||||
rs: WalletRefundItem[],
|
|
||||||
zero: AmountJson,
|
|
||||||
): MergedRefundInfo[] {
|
|
||||||
const refundByExecTime = rs.reduce((prev, refund) => {
|
|
||||||
const key = `${refund.executionTime.t_s}`;
|
|
||||||
|
|
||||||
// refunds count if applied
|
|
||||||
const effective =
|
|
||||||
refund.type === RefundState.Applied
|
|
||||||
? Amounts.sub(
|
|
||||||
refund.refundAmount,
|
|
||||||
refund.refundFee,
|
|
||||||
refund.totalRefreshCostBound,
|
|
||||||
).amount
|
|
||||||
: zero;
|
|
||||||
const raw =
|
|
||||||
refund.type === RefundState.Applied ? refund.refundAmount : zero;
|
|
||||||
|
|
||||||
const v = prev.get(key);
|
|
||||||
if (!v) {
|
|
||||||
prev.set(key, {
|
|
||||||
executionTime: refund.executionTime,
|
|
||||||
amountAppliedEffective: effective,
|
|
||||||
amountAppliedRaw: Amounts.parseOrThrow(raw),
|
|
||||||
firstTimestamp: refund.obtainedTime,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
//v.executionTime is the same
|
|
||||||
v.amountAppliedEffective = Amounts.add(
|
|
||||||
v.amountAppliedEffective,
|
|
||||||
effective,
|
|
||||||
).amount;
|
|
||||||
v.amountAppliedRaw = Amounts.add(
|
|
||||||
v.amountAppliedRaw,
|
|
||||||
refund.refundAmount,
|
|
||||||
).amount;
|
|
||||||
v.firstTimestamp = TalerProtocolTimestamp.min(
|
|
||||||
v.firstTimestamp,
|
|
||||||
refund.obtainedTime,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}, new Map<string, MergedRefundInfo>());
|
|
||||||
|
|
||||||
return Array.from(refundByExecTime.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildTransactionForRefund(
|
|
||||||
purchaseRecord: PurchaseRecord,
|
|
||||||
contractData: WalletContractData,
|
|
||||||
refundInfo: MergedRefundInfo,
|
|
||||||
ort?: OperationRetryRecord,
|
|
||||||
): Promise<Transaction> {
|
|
||||||
const info: OrderShortInfo = {
|
|
||||||
merchant: contractData.merchant,
|
|
||||||
orderId: contractData.orderId,
|
|
||||||
products: contractData.products,
|
|
||||||
summary: contractData.summary,
|
|
||||||
summary_i18n: contractData.summaryI18n,
|
|
||||||
contractTermsHash: contractData.contractTermsHash,
|
|
||||||
};
|
|
||||||
if (contractData.fulfillmentUrl !== "") {
|
|
||||||
info.fulfillmentUrl = contractData.fulfillmentUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: TransactionType.Refund,
|
|
||||||
txState: mkTxStateUnknown(),
|
|
||||||
info,
|
|
||||||
refundedTransactionId: makeTransactionId(
|
|
||||||
TransactionType.Payment,
|
|
||||||
purchaseRecord.proposalId,
|
|
||||||
),
|
|
||||||
transactionId: makeTransactionId(
|
|
||||||
TransactionType.Refund,
|
|
||||||
purchaseRecord.proposalId,
|
|
||||||
`${refundInfo.executionTime.t_s}`,
|
|
||||||
),
|
|
||||||
timestamp: refundInfo.firstTimestamp,
|
|
||||||
amountEffective: Amounts.stringify(refundInfo.amountAppliedEffective),
|
|
||||||
amountRaw: Amounts.stringify(refundInfo.amountAppliedRaw),
|
|
||||||
refundPending:
|
|
||||||
purchaseRecord.refundAmountAwaiting === undefined
|
|
||||||
? undefined
|
|
||||||
: Amounts.stringify(purchaseRecord.refundAmountAwaiting),
|
|
||||||
extendedStatus: ExtendedStatus.Done,
|
|
||||||
pending: false,
|
|
||||||
frozen: false,
|
|
||||||
...(ort?.lastError ? { error: ort.lastError } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildTransactionForPurchase(
|
async function buildTransactionForPurchase(
|
||||||
purchaseRecord: PurchaseRecord,
|
purchaseRecord: PurchaseRecord,
|
||||||
contractData: WalletContractData,
|
contractData: WalletContractData,
|
||||||
refundsInfo: MergedRefundInfo[],
|
refundsInfo: RefundGroupRecord[],
|
||||||
ort?: OperationRetryRecord,
|
ort?: OperationRetryRecord,
|
||||||
): Promise<Transaction> {
|
): Promise<Transaction> {
|
||||||
const zero = Amounts.zeroOfAmount(contractData.amount);
|
const zero = Amounts.zeroOfAmount(contractData.amount);
|
||||||
@ -974,30 +811,7 @@ async function buildTransactionForPurchase(
|
|||||||
info.fulfillmentUrl = contractData.fulfillmentUrl;
|
info.fulfillmentUrl = contractData.fulfillmentUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalRefund = refundsInfo.reduce(
|
const refunds: RefundInfoShort[] = [];
|
||||||
(prev, cur) => {
|
|
||||||
return {
|
|
||||||
raw: Amounts.add(prev.raw, cur.amountAppliedRaw).amount,
|
|
||||||
effective: Amounts.add(prev.effective, cur.amountAppliedEffective)
|
|
||||||
.amount,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
raw: zero,
|
|
||||||
effective: zero,
|
|
||||||
} as { raw: AmountJson; effective: AmountJson },
|
|
||||||
);
|
|
||||||
|
|
||||||
const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
|
|
||||||
amountEffective: Amounts.stringify(r.amountAppliedEffective),
|
|
||||||
amountRaw: Amounts.stringify(r.amountAppliedRaw),
|
|
||||||
timestamp: r.executionTime,
|
|
||||||
transactionId: makeTransactionId(
|
|
||||||
TransactionType.Refund,
|
|
||||||
purchaseRecord.proposalId,
|
|
||||||
`${r.executionTime.t_s}`,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const timestamp = purchaseRecord.timestampAccept;
|
const timestamp = purchaseRecord.timestampAccept;
|
||||||
checkDbInvariant(!!timestamp);
|
checkDbInvariant(!!timestamp);
|
||||||
@ -1008,7 +822,7 @@ async function buildTransactionForPurchase(
|
|||||||
case PurchaseStatus.AbortingWithRefund:
|
case PurchaseStatus.AbortingWithRefund:
|
||||||
status = ExtendedStatus.Aborting;
|
status = ExtendedStatus.Aborting;
|
||||||
break;
|
break;
|
||||||
case PurchaseStatus.Paid:
|
case PurchaseStatus.Done:
|
||||||
case PurchaseStatus.RepurchaseDetected:
|
case PurchaseStatus.RepurchaseDetected:
|
||||||
status = ExtendedStatus.Done;
|
status = ExtendedStatus.Done;
|
||||||
break;
|
break;
|
||||||
@ -1018,10 +832,10 @@ async function buildTransactionForPurchase(
|
|||||||
case PurchaseStatus.Paying:
|
case PurchaseStatus.Paying:
|
||||||
status = ExtendedStatus.Pending;
|
status = ExtendedStatus.Pending;
|
||||||
break;
|
break;
|
||||||
case PurchaseStatus.ProposalDownloadFailed:
|
case PurchaseStatus.FailedClaim:
|
||||||
status = ExtendedStatus.Failed;
|
status = ExtendedStatus.Failed;
|
||||||
break;
|
break;
|
||||||
case PurchaseStatus.PaymentAbortFinished:
|
case PurchaseStatus.AbortedIncompletePayment:
|
||||||
status = ExtendedStatus.Aborted;
|
status = ExtendedStatus.Aborted;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -1034,8 +848,8 @@ async function buildTransactionForPurchase(
|
|||||||
txState: computePayMerchantTransactionState(purchaseRecord),
|
txState: computePayMerchantTransactionState(purchaseRecord),
|
||||||
amountRaw: Amounts.stringify(contractData.amount),
|
amountRaw: Amounts.stringify(contractData.amount),
|
||||||
amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
|
amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
|
||||||
totalRefundRaw: Amounts.stringify(totalRefund.raw),
|
totalRefundRaw: Amounts.stringify(zero), // FIXME!
|
||||||
totalRefundEffective: Amounts.stringify(totalRefund.effective),
|
totalRefundEffective: Amounts.stringify(zero), // FIXME!
|
||||||
refundPending:
|
refundPending:
|
||||||
purchaseRecord.refundAmountAwaiting === undefined
|
purchaseRecord.refundAmountAwaiting === undefined
|
||||||
? undefined
|
? undefined
|
||||||
@ -1057,7 +871,7 @@ async function buildTransactionForPurchase(
|
|||||||
refundQueryActive:
|
refundQueryActive:
|
||||||
purchaseRecord.purchaseStatus === PurchaseStatus.QueryingRefund,
|
purchaseRecord.purchaseStatus === PurchaseStatus.QueryingRefund,
|
||||||
frozen:
|
frozen:
|
||||||
purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ??
|
purchaseRecord.purchaseStatus === PurchaseStatus.AbortedIncompletePayment ??
|
||||||
false,
|
false,
|
||||||
...(ort?.lastError ? { error: ort.lastError } : {}),
|
...(ort?.lastError ? { error: ort.lastError } : {}),
|
||||||
};
|
};
|
||||||
@ -1092,6 +906,7 @@ export async function getTransactions(
|
|||||||
x.tombstones,
|
x.tombstones,
|
||||||
x.withdrawalGroups,
|
x.withdrawalGroups,
|
||||||
x.refreshGroups,
|
x.refreshGroups,
|
||||||
|
x.refundGroups,
|
||||||
])
|
])
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
|
tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
|
||||||
@ -1202,6 +1017,14 @@ export async function getTransactions(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tx.refundGroups.iter().forEachAsync(async (refundGroup) => {
|
||||||
|
const currency = Amounts.currencyOf(refundGroup.amountRaw);
|
||||||
|
if (shouldSkipCurrency(transactionsRequest, currency)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transactions.push(buildTransactionForRefund(refundGroup))
|
||||||
|
});
|
||||||
|
|
||||||
tx.refreshGroups.iter().forEachAsync(async (rg) => {
|
tx.refreshGroups.iter().forEachAsync(async (rg) => {
|
||||||
if (shouldSkipCurrency(transactionsRequest, rg.currency)) {
|
if (shouldSkipCurrency(transactionsRequest, rg.currency)) {
|
||||||
return;
|
return;
|
||||||
@ -1318,47 +1141,13 @@ export async function getTransactions(
|
|||||||
download.contractTermsMerchantSig,
|
download.contractTermsMerchantSig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredRefunds = await Promise.all(
|
|
||||||
Object.values(purchase.refunds).map(async (r) => {
|
|
||||||
const t = await tx.tombstones.get(
|
|
||||||
makeTombstoneId(
|
|
||||||
TombstoneTag.DeleteRefund,
|
|
||||||
purchase.proposalId,
|
|
||||||
`${r.executionTime.t_s}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!t) return r;
|
|
||||||
return undefined;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const cleanRefunds = filteredRefunds.filter(
|
|
||||||
(x): x is WalletRefundItem => !!x,
|
|
||||||
);
|
|
||||||
|
|
||||||
const refunds = mergeRefundByExecutionTime(
|
|
||||||
cleanRefunds,
|
|
||||||
Amounts.zeroOfCurrency(download.currency),
|
|
||||||
);
|
|
||||||
|
|
||||||
refunds.forEach(async (refundInfo) => {
|
|
||||||
transactions.push(
|
|
||||||
await buildTransactionForRefund(
|
|
||||||
purchase,
|
|
||||||
contractData,
|
|
||||||
refundInfo,
|
|
||||||
undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const payOpId = TaskIdentifiers.forPay(purchase);
|
const payOpId = TaskIdentifiers.forPay(purchase);
|
||||||
const payRetryRecord = await tx.operationRetries.get(payOpId);
|
const payRetryRecord = await tx.operationRetries.get(payOpId);
|
||||||
transactions.push(
|
transactions.push(
|
||||||
await buildTransactionForPurchase(
|
await buildTransactionForPurchase(
|
||||||
purchase,
|
purchase,
|
||||||
contractData,
|
contractData,
|
||||||
refunds,
|
[], // FIXME!
|
||||||
payRetryRecord,
|
payRetryRecord,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1425,7 +1214,7 @@ export type ParsedTransactionIdentifier =
|
|||||||
| { tag: TransactionType.PeerPushCredit; peerPushPaymentIncomingId: string }
|
| { tag: TransactionType.PeerPushCredit; peerPushPaymentIncomingId: string }
|
||||||
| { tag: TransactionType.PeerPushDebit; pursePub: string }
|
| { tag: TransactionType.PeerPushDebit; pursePub: string }
|
||||||
| { tag: TransactionType.Refresh; refreshGroupId: string }
|
| { tag: TransactionType.Refresh; refreshGroupId: string }
|
||||||
| { tag: TransactionType.Refund; proposalId: string; executionTime: string }
|
| { tag: TransactionType.Refund; refundGroupId: string }
|
||||||
| { tag: TransactionType.Tip; walletTipId: string }
|
| { tag: TransactionType.Tip; walletTipId: string }
|
||||||
| { tag: TransactionType.Withdrawal; withdrawalGroupId: string };
|
| { tag: TransactionType.Withdrawal; withdrawalGroupId: string };
|
||||||
|
|
||||||
@ -1448,7 +1237,7 @@ export function constructTransactionIdentifier(
|
|||||||
case TransactionType.Refresh:
|
case TransactionType.Refresh:
|
||||||
return `txn:${pTxId.tag}:${pTxId.refreshGroupId}`;
|
return `txn:${pTxId.tag}:${pTxId.refreshGroupId}`;
|
||||||
case TransactionType.Refund:
|
case TransactionType.Refund:
|
||||||
return `txn:${pTxId.tag}:${pTxId.proposalId}:${pTxId.executionTime}`;
|
return `txn:${pTxId.tag}:${pTxId.refundGroupId}`;
|
||||||
case TransactionType.Tip:
|
case TransactionType.Tip:
|
||||||
return `txn:${pTxId.tag}:${pTxId.walletTipId}`;
|
return `txn:${pTxId.tag}:${pTxId.walletTipId}`;
|
||||||
case TransactionType.Withdrawal:
|
case TransactionType.Withdrawal:
|
||||||
@ -1490,8 +1279,7 @@ export function parseTransactionIdentifier(
|
|||||||
case TransactionType.Refund:
|
case TransactionType.Refund:
|
||||||
return {
|
return {
|
||||||
tag: TransactionType.Refund,
|
tag: TransactionType.Refund,
|
||||||
proposalId: rest[0],
|
refundGroupId: rest[0],
|
||||||
executionTime: rest[1],
|
|
||||||
};
|
};
|
||||||
case TransactionType.Tip:
|
case TransactionType.Tip:
|
||||||
return {
|
return {
|
||||||
|
@ -35,7 +35,7 @@ import {
|
|||||||
IDBKeyPath,
|
IDBKeyPath,
|
||||||
IDBKeyRange,
|
IDBKeyRange,
|
||||||
} from "@gnu-taler/idb-bridge";
|
} from "@gnu-taler/idb-bridge";
|
||||||
import { Logger } from "@gnu-taler/taler-util";
|
import { Logger, j2s } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
const logger = new Logger("query.ts");
|
const logger = new Logger("query.ts");
|
||||||
|
|
||||||
|
@ -76,6 +76,11 @@ export namespace OperationAttemptResult {
|
|||||||
result: undefined,
|
result: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function longpoll(): OperationAttemptResult<unknown, unknown> {
|
||||||
|
return {
|
||||||
|
type: OperationAttemptResultType.Longpoll,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OperationAttemptFinishedResult<T> {
|
export interface OperationAttemptFinishedResult<T> {
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
AddKnownBankAccountsRequest,
|
AddKnownBankAccountsRequest,
|
||||||
ApplyDevExperimentRequest,
|
ApplyDevExperimentRequest,
|
||||||
ApplyRefundFromPurchaseIdRequest,
|
ApplyRefundFromPurchaseIdRequest,
|
||||||
ApplyRefundRequest,
|
AcceptRefundRequest,
|
||||||
ApplyRefundResponse,
|
ApplyRefundResponse,
|
||||||
BackupRecovery,
|
BackupRecovery,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
@ -90,6 +90,7 @@ import {
|
|||||||
RetryTransactionRequest,
|
RetryTransactionRequest,
|
||||||
SetCoinSuspendedRequest,
|
SetCoinSuspendedRequest,
|
||||||
SetWalletDeviceIdRequest,
|
SetWalletDeviceIdRequest,
|
||||||
|
StartRefundQueryRequest,
|
||||||
TestPayArgs,
|
TestPayArgs,
|
||||||
TestPayResult,
|
TestPayResult,
|
||||||
Transaction,
|
Transaction,
|
||||||
@ -149,9 +150,8 @@ export enum WalletApiOperation {
|
|||||||
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
||||||
GetPendingOperations = "getPendingOperations",
|
GetPendingOperations = "getPendingOperations",
|
||||||
SetExchangeTosAccepted = "setExchangeTosAccepted",
|
SetExchangeTosAccepted = "setExchangeTosAccepted",
|
||||||
ApplyRefund = "applyRefund",
|
StartRefundQueryForUri = "startRefundQueryForUri",
|
||||||
ApplyRefundFromPurchaseId = "applyRefundFromPurchaseId",
|
StartRefundQuery = "startRefundQuery",
|
||||||
PrepareRefund = "prepareRefund",
|
|
||||||
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
|
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
|
||||||
GetExchangeTos = "getExchangeTos",
|
GetExchangeTos = "getExchangeTos",
|
||||||
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
|
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
|
||||||
@ -435,22 +435,16 @@ export type ConfirmPayOp = {
|
|||||||
/**
|
/**
|
||||||
* Check for a refund based on a taler://refund URI.
|
* Check for a refund based on a taler://refund URI.
|
||||||
*/
|
*/
|
||||||
export type ApplyRefundOp = {
|
export type StartRefundQueryForUriOp = {
|
||||||
op: WalletApiOperation.ApplyRefund;
|
op: WalletApiOperation.StartRefundQueryForUri;
|
||||||
request: ApplyRefundRequest;
|
|
||||||
response: ApplyRefundResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ApplyRefundFromPurchaseIdOp = {
|
|
||||||
op: WalletApiOperation.ApplyRefundFromPurchaseId;
|
|
||||||
request: ApplyRefundFromPurchaseIdRequest;
|
|
||||||
response: ApplyRefundResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PrepareRefundOp = {
|
|
||||||
op: WalletApiOperation.PrepareRefund;
|
|
||||||
request: PrepareRefundRequest;
|
request: PrepareRefundRequest;
|
||||||
response: PrepareRefundResult;
|
response: EmptyObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StartRefundQueryOp = {
|
||||||
|
op: WalletApiOperation.StartRefundQuery;
|
||||||
|
request: StartRefundQueryRequest;
|
||||||
|
response: EmptyObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
// group: Tipping
|
// group: Tipping
|
||||||
@ -954,9 +948,8 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.RetryTransaction]: RetryTransactionOp;
|
[WalletApiOperation.RetryTransaction]: RetryTransactionOp;
|
||||||
[WalletApiOperation.PrepareTip]: PrepareTipOp;
|
[WalletApiOperation.PrepareTip]: PrepareTipOp;
|
||||||
[WalletApiOperation.AcceptTip]: AcceptTipOp;
|
[WalletApiOperation.AcceptTip]: AcceptTipOp;
|
||||||
[WalletApiOperation.ApplyRefund]: ApplyRefundOp;
|
[WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp;
|
||||||
[WalletApiOperation.ApplyRefundFromPurchaseId]: ApplyRefundFromPurchaseIdOp;
|
[WalletApiOperation.StartRefundQuery]: StartRefundQueryOp;
|
||||||
[WalletApiOperation.PrepareRefund]: PrepareRefundOp;
|
|
||||||
[WalletApiOperation.ListCurrencies]: ListCurrenciesOp;
|
[WalletApiOperation.ListCurrencies]: ListCurrenciesOp;
|
||||||
[WalletApiOperation.GetWithdrawalDetailsForAmount]: GetWithdrawalDetailsForAmountOp;
|
[WalletApiOperation.GetWithdrawalDetailsForAmount]: GetWithdrawalDetailsForAmountOp;
|
||||||
[WalletApiOperation.GetWithdrawalDetailsForUri]: GetWithdrawalDetailsForUriOp;
|
[WalletApiOperation.GetWithdrawalDetailsForUri]: GetWithdrawalDetailsForUriOp;
|
||||||
|
@ -48,6 +48,7 @@ import {
|
|||||||
RefreshReason,
|
RefreshReason,
|
||||||
TalerError,
|
TalerError,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
ValidateIbanResponse,
|
ValidateIbanResponse,
|
||||||
WalletCoreVersion,
|
WalletCoreVersion,
|
||||||
@ -95,6 +96,7 @@ import {
|
|||||||
codecForRetryTransactionRequest,
|
codecForRetryTransactionRequest,
|
||||||
codecForSetCoinSuspendedRequest,
|
codecForSetCoinSuspendedRequest,
|
||||||
codecForSetWalletDeviceIdRequest,
|
codecForSetWalletDeviceIdRequest,
|
||||||
|
codecForStartRefundQueryRequest,
|
||||||
codecForSuspendTransaction,
|
codecForSuspendTransaction,
|
||||||
codecForTestPayArgs,
|
codecForTestPayArgs,
|
||||||
codecForTransactionByIdRequest,
|
codecForTransactionByIdRequest,
|
||||||
@ -188,13 +190,11 @@ import {
|
|||||||
} from "./operations/exchanges.js";
|
} from "./operations/exchanges.js";
|
||||||
import { getMerchantInfo } from "./operations/merchants.js";
|
import { getMerchantInfo } from "./operations/merchants.js";
|
||||||
import {
|
import {
|
||||||
applyRefund,
|
|
||||||
applyRefundFromPurchaseId,
|
|
||||||
confirmPay,
|
confirmPay,
|
||||||
getContractTermsDetails,
|
getContractTermsDetails,
|
||||||
preparePayForUri,
|
preparePayForUri,
|
||||||
prepareRefund,
|
|
||||||
processPurchase,
|
processPurchase,
|
||||||
|
startRefundQueryForUri,
|
||||||
} from "./operations/pay-merchant.js";
|
} from "./operations/pay-merchant.js";
|
||||||
import {
|
import {
|
||||||
checkPeerPullPaymentInitiation,
|
checkPeerPullPaymentInitiation,
|
||||||
@ -233,6 +233,7 @@ import {
|
|||||||
deleteTransaction,
|
deleteTransaction,
|
||||||
getTransactionById,
|
getTransactionById,
|
||||||
getTransactions,
|
getTransactions,
|
||||||
|
parseTransactionIdentifier,
|
||||||
resumeTransaction,
|
resumeTransaction,
|
||||||
retryTransaction,
|
retryTransaction,
|
||||||
suspendTransaction,
|
suspendTransaction,
|
||||||
@ -276,6 +277,7 @@ import {
|
|||||||
WalletCoreApiClient,
|
WalletCoreApiClient,
|
||||||
WalletCoreResponseType,
|
WalletCoreResponseType,
|
||||||
} from "./wallet-api-types.js";
|
} from "./wallet-api-types.js";
|
||||||
|
import { startQueryRefund } from "./operations/pay-merchant.js";
|
||||||
|
|
||||||
const logger = new Logger("wallet.ts");
|
const logger = new Logger("wallet.ts");
|
||||||
|
|
||||||
@ -1141,14 +1143,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
|
await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
case WalletApiOperation.ApplyRefund: {
|
|
||||||
const req = codecForApplyRefundRequest().decode(payload);
|
|
||||||
return await applyRefund(ws, req.talerRefundUri);
|
|
||||||
}
|
|
||||||
case WalletApiOperation.ApplyRefundFromPurchaseId: {
|
|
||||||
const req = codecForApplyRefundFromPurchaseIdRequest().decode(payload);
|
|
||||||
return await applyRefundFromPurchaseId(ws, req.purchaseId);
|
|
||||||
}
|
|
||||||
case WalletApiOperation.AcceptBankIntegratedWithdrawal: {
|
case WalletApiOperation.AcceptBankIntegratedWithdrawal: {
|
||||||
const req =
|
const req =
|
||||||
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
|
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
|
||||||
@ -1292,9 +1286,22 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
const req = codecForPrepareTipRequest().decode(payload);
|
const req = codecForPrepareTipRequest().decode(payload);
|
||||||
return await prepareTip(ws, req.talerTipUri);
|
return await prepareTip(ws, req.talerTipUri);
|
||||||
}
|
}
|
||||||
case WalletApiOperation.PrepareRefund: {
|
case WalletApiOperation.StartRefundQueryForUri: {
|
||||||
const req = codecForPrepareRefundRequest().decode(payload);
|
const req = codecForPrepareRefundRequest().decode(payload);
|
||||||
return await prepareRefund(ws, req.talerRefundUri);
|
await startRefundQueryForUri(ws, req.talerRefundUri);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case WalletApiOperation.StartRefundQuery: {
|
||||||
|
const req = codecForStartRefundQueryRequest().decode(payload);
|
||||||
|
const txIdParsed = parseTransactionIdentifier(req.transactionId);
|
||||||
|
if (!txIdParsed) {
|
||||||
|
throw Error("invalid transaction ID");
|
||||||
|
}
|
||||||
|
if (txIdParsed.tag !== TransactionType.Payment) {
|
||||||
|
throw Error("expected payment transaction ID");
|
||||||
|
}
|
||||||
|
await startQueryRefund(ws, txIdParsed.proposalId);
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
case WalletApiOperation.AcceptTip: {
|
case WalletApiOperation.AcceptTip: {
|
||||||
const req = codecForAcceptTipRequest().decode(payload);
|
const req = codecForAcceptTipRequest().decode(payload);
|
||||||
|
@ -35,7 +35,7 @@ export function useComponentState({
|
|||||||
|
|
||||||
const info = useAsyncAsHook(async () => {
|
const info = useAsyncAsHook(async () => {
|
||||||
if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
|
if (!talerRefundUri) throw Error("ERROR_NO-URI-FOR-REFUND");
|
||||||
const refund = await api.wallet.call(WalletApiOperation.PrepareRefund, {
|
const refund = await api.wallet.call(WalletApiOperation.StartRefundQueryForUri, {
|
||||||
talerRefundUri,
|
talerRefundUri,
|
||||||
});
|
});
|
||||||
return { refund, uri: talerRefundUri };
|
return { refund, uri: talerRefundUri };
|
||||||
@ -70,8 +70,8 @@ export function useComponentState({
|
|||||||
const { refund, uri } = info.response;
|
const { refund, uri } = info.response;
|
||||||
|
|
||||||
const doAccept = async (): Promise<void> => {
|
const doAccept = async (): Promise<void> => {
|
||||||
const res = await api.wallet.call(WalletApiOperation.ApplyRefund, {
|
const res = await api.wallet.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||||
talerRefundUri: uri,
|
transactionId: uri,
|
||||||
});
|
});
|
||||||
|
|
||||||
onSuccess(res.transactionId);
|
onSuccess(res.transactionId);
|
||||||
|
@ -72,7 +72,7 @@ describe("Refund CTA states", () => {
|
|||||||
onSuccess: nullFunction,
|
onSuccess: nullFunction,
|
||||||
};
|
};
|
||||||
|
|
||||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||||
awaiting: "EUR:2",
|
awaiting: "EUR:2",
|
||||||
effectivePaid: "EUR:2",
|
effectivePaid: "EUR:2",
|
||||||
gone: "EUR:0",
|
gone: "EUR:0",
|
||||||
@ -126,7 +126,7 @@ describe("Refund CTA states", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||||
awaiting: "EUR:2",
|
awaiting: "EUR:2",
|
||||||
effectivePaid: "EUR:2",
|
effectivePaid: "EUR:2",
|
||||||
gone: "EUR:0",
|
gone: "EUR:0",
|
||||||
@ -187,7 +187,7 @@ describe("Refund CTA states", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||||
awaiting: "EUR:2",
|
awaiting: "EUR:2",
|
||||||
effectivePaid: "EUR:2",
|
effectivePaid: "EUR:2",
|
||||||
gone: "EUR:0",
|
gone: "EUR:0",
|
||||||
@ -203,7 +203,7 @@ describe("Refund CTA states", () => {
|
|||||||
summary: "the summary",
|
summary: "the summary",
|
||||||
} as OrderShortInfo,
|
} as OrderShortInfo,
|
||||||
});
|
});
|
||||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||||
awaiting: "EUR:1",
|
awaiting: "EUR:1",
|
||||||
effectivePaid: "EUR:2",
|
effectivePaid: "EUR:2",
|
||||||
gone: "EUR:0",
|
gone: "EUR:0",
|
||||||
@ -219,7 +219,7 @@ describe("Refund CTA states", () => {
|
|||||||
summary: "the summary",
|
summary: "the summary",
|
||||||
} as OrderShortInfo,
|
} as OrderShortInfo,
|
||||||
});
|
});
|
||||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||||
awaiting: "EUR:0",
|
awaiting: "EUR:0",
|
||||||
effectivePaid: "EUR:2",
|
effectivePaid: "EUR:2",
|
||||||
gone: "EUR:0",
|
gone: "EUR:0",
|
||||||
|
Loading…
Reference in New Issue
Block a user