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);
|
||||
|
||||
let rr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
||||
talerRefundUri: ref.talerRefundUri,
|
||||
let rr = await wallet.client.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||
transactionId: ref.talerRefundUri,
|
||||
});
|
||||
|
||||
console.log("refund response:", rr);
|
||||
|
@ -94,8 +94,8 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
|
||||
console.log("first refund increase response", ref);
|
||||
|
||||
{
|
||||
let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
||||
talerRefundUri: ref.talerRefundUri,
|
||||
let wr = await wallet.client.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||
transactionId: ref.talerRefundUri,
|
||||
});
|
||||
console.log(wr);
|
||||
const txs = await wallet.client.call(
|
||||
@ -135,8 +135,8 @@ export async function runRefundIncrementalTest(t: GlobalTestState) {
|
||||
console.log("third refund increase response", ref);
|
||||
|
||||
{
|
||||
let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
||||
talerRefundUri: ref.talerRefundUri,
|
||||
let wr = await wallet.client.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||
transactionId: ref.talerRefundUri,
|
||||
});
|
||||
console.log(wr);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
Duration,
|
||||
durationFromSpec,
|
||||
NotificationType,
|
||||
TransactionMajorState,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
|
||||
@ -100,11 +101,14 @@ export async function runRefundTest(t: GlobalTestState) {
|
||||
console.log(ref);
|
||||
|
||||
{
|
||||
// FIXME!
|
||||
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, {
|
||||
talerRefundUri: ref.talerRefundUri,
|
||||
const r = await wallet.client.call(WalletApiOperation.StartRefundQuery, {
|
||||
transactionId: r1.transactionId,
|
||||
});
|
||||
console.log(r);
|
||||
|
||||
@ -120,19 +124,20 @@ export async function runRefundTest(t: GlobalTestState) {
|
||||
console.log(JSON.stringify(r2, undefined, 2));
|
||||
}
|
||||
|
||||
{
|
||||
const refundQueriedCond = wallet.waitForNotificationCond(
|
||||
(x) => x.type === NotificationType.RefundQueried,
|
||||
);
|
||||
const r3 = await wallet.client.call(
|
||||
WalletApiOperation.ApplyRefundFromPurchaseId,
|
||||
{
|
||||
purchaseId: r1.proposalId,
|
||||
},
|
||||
);
|
||||
console.log(r3);
|
||||
await refundQueriedCond;
|
||||
}
|
||||
// FIXME: Test is incomplete without this!
|
||||
// {
|
||||
// const refundQueriedCond = wallet.waitForNotificationCond(
|
||||
// (x) => x.type === NotificationType.RefundQueried,
|
||||
// );
|
||||
// const r3 = await wallet.client.call(
|
||||
// WalletApiOperation.ApplyRefundFromPurchaseId,
|
||||
// {
|
||||
// purchaseId: r1.proposalId,
|
||||
// },
|
||||
// );
|
||||
// console.log(r3);
|
||||
// await refundQueriedCond;
|
||||
// }
|
||||
}
|
||||
|
||||
runRefundTest.suites = ["wallet"];
|
||||
|
@ -44,7 +44,6 @@ export enum NotificationType {
|
||||
WaitingForRetry = "waiting-for-retry",
|
||||
RefundStarted = "refund-started",
|
||||
RefundQueried = "refund-queried",
|
||||
RefundFinished = "refund-finished",
|
||||
ExchangeOperationError = "exchange-operation-error",
|
||||
ExchangeAdded = "exchange-added",
|
||||
RefreshOperationError = "refresh-operation-error",
|
||||
@ -192,14 +191,6 @@ export interface WaitingForRetryNotification {
|
||||
numDue: number;
|
||||
}
|
||||
|
||||
export interface RefundFinishedNotification {
|
||||
type: NotificationType.RefundFinished;
|
||||
|
||||
/**
|
||||
* Transaction ID of the purchase (NOT the refund transaction).
|
||||
*/
|
||||
transactionId: string;
|
||||
}
|
||||
|
||||
export interface ExchangeAddedNotification {
|
||||
type: NotificationType.ExchangeAdded;
|
||||
@ -321,7 +312,6 @@ export type WalletNotification =
|
||||
| WithdrawalGroupFinishedNotification
|
||||
| WaitingForRetryNotification
|
||||
| RefundStartedNotification
|
||||
| RefundFinishedNotification
|
||||
| RefundQueriedNotification
|
||||
| WithdrawalGroupCreatedNotification
|
||||
| CoinWithdrawnNotification
|
||||
|
@ -130,6 +130,8 @@ export enum TransactionMinorState {
|
||||
Withdraw = "withdraw",
|
||||
MerchantOrderProposed = "merchant-order-proposed",
|
||||
Proposed = "proposed",
|
||||
RefundAvailable = "refund-available",
|
||||
AcceptRefund = "accept-refund",
|
||||
}
|
||||
|
||||
export interface TransactionsResponse {
|
||||
@ -549,14 +551,6 @@ export interface TransactionRefund extends TransactionCommon {
|
||||
// ID for the transaction that is refunded
|
||||
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
|
||||
amountRaw: AmountString;
|
||||
|
||||
|
@ -419,6 +419,7 @@ export const codecForPreparePayResultPaymentPossible =
|
||||
.property("amountEffective", codecForAmountString())
|
||||
.property("amountRaw", codecForAmountString())
|
||||
.property("contractTerms", codecForMerchantContractTerms())
|
||||
.property("transactionId", codecForString())
|
||||
.property("proposalId", codecForString())
|
||||
.property("contractTermsHash", codecForString())
|
||||
.property("talerUri", codecForString())
|
||||
@ -494,6 +495,7 @@ export const codecForPreparePayResultInsufficientBalance =
|
||||
.property("contractTerms", codecForAny())
|
||||
.property("talerUri", codecForString())
|
||||
.property("proposalId", codecForString())
|
||||
.property("transactionId", codecForString())
|
||||
.property("noncePriv", codecForString())
|
||||
.property(
|
||||
"status",
|
||||
@ -518,6 +520,7 @@ export const codecForPreparePayResultAlreadyConfirmed =
|
||||
.property("talerUri", codecOptional(codecForString()))
|
||||
.property("contractTerms", codecForAny())
|
||||
.property("contractTermsHash", codecForString())
|
||||
.property("transactionId", codecForString())
|
||||
.property("proposalId", codecForString())
|
||||
.build("PreparePayResultAlreadyConfirmed");
|
||||
|
||||
@ -551,6 +554,10 @@ export type PreparePayResult =
|
||||
*/
|
||||
export interface PreparePayResultPaymentPossible {
|
||||
status: PreparePayResultType.PaymentPossible;
|
||||
transactionId: string;
|
||||
/**
|
||||
* @deprecated use transactionId instead
|
||||
*/
|
||||
proposalId: string;
|
||||
contractTerms: MerchantContractTerms;
|
||||
contractTermsHash: string;
|
||||
@ -562,6 +569,7 @@ export interface PreparePayResultPaymentPossible {
|
||||
|
||||
export interface PreparePayResultInsufficientBalance {
|
||||
status: PreparePayResultType.InsufficientBalance;
|
||||
transactionId: string;
|
||||
proposalId: string;
|
||||
contractTerms: MerchantContractTerms;
|
||||
amountRaw: string;
|
||||
@ -572,6 +580,7 @@ export interface PreparePayResultInsufficientBalance {
|
||||
|
||||
export interface PreparePayResultAlreadyConfirmed {
|
||||
status: PreparePayResultType.AlreadyConfirmed;
|
||||
transactionId: string;
|
||||
contractTerms: MerchantContractTerms;
|
||||
paid: boolean;
|
||||
amountRaw: string;
|
||||
@ -1352,14 +1361,14 @@ export const codecForAcceptExchangeTosRequest =
|
||||
.property("etag", codecOptional(codecForString()))
|
||||
.build("AcceptExchangeTosRequest");
|
||||
|
||||
export interface ApplyRefundRequest {
|
||||
talerRefundUri: string;
|
||||
export interface AcceptRefundRequest {
|
||||
transactionId: string;
|
||||
}
|
||||
|
||||
export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
|
||||
buildCodecForObject<ApplyRefundRequest>()
|
||||
.property("talerRefundUri", codecForString())
|
||||
.build("ApplyRefundRequest");
|
||||
export const codecForApplyRefundRequest = (): Codec<AcceptRefundRequest> =>
|
||||
buildCodecForObject<AcceptRefundRequest>()
|
||||
.property("transactionId", codecForString())
|
||||
.build("AcceptRefundRequest");
|
||||
|
||||
export interface ApplyRefundFromPurchaseIdRequest {
|
||||
purchaseId: string;
|
||||
@ -1641,6 +1650,16 @@ export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
|
||||
.property("talerRefundUri", codecForString())
|
||||
.build("PrepareRefundRequest");
|
||||
|
||||
export interface StartRefundQueryRequest {
|
||||
transactionId: string;
|
||||
}
|
||||
|
||||
export const codecForStartRefundQueryRequest = (): Codec<StartRefundQueryRequest> =>
|
||||
buildCodecForObject<StartRefundQueryRequest>()
|
||||
.property("transactionId", codecForString())
|
||||
.build("StartRefundQueryRequest");
|
||||
|
||||
|
||||
export interface PrepareTipRequest {
|
||||
talerTipUri: string;
|
||||
}
|
||||
|
@ -661,7 +661,7 @@ walletCli
|
||||
}
|
||||
break;
|
||||
case TalerUriType.TalerRefund:
|
||||
await wallet.client.call(WalletApiOperation.ApplyRefund, {
|
||||
await wallet.client.call(WalletApiOperation.StartRefundQueryForUri, {
|
||||
talerRefundUri: uri,
|
||||
});
|
||||
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
|
||||
.subcommand("payConfirm", "pay-confirm", {
|
||||
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
|
||||
* are added.
|
||||
*/
|
||||
export const WALLET_DB_MINOR_VERSION = 6;
|
||||
export const WALLET_DB_MINOR_VERSION = 7;
|
||||
|
||||
/**
|
||||
* Ranges for operation status fields.
|
||||
@ -208,7 +208,7 @@ export enum WithdrawalGroupStatus {
|
||||
* talk to the exchange. Money might have been
|
||||
* wired or not.
|
||||
*/
|
||||
AbortedExchange = 60
|
||||
AbortedExchange = 60,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1012,63 +1012,6 @@ export interface RefreshSessionRecord {
|
||||
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 {
|
||||
/**
|
||||
* Normal refund given by the merchant.
|
||||
@ -1161,6 +1104,8 @@ export enum PurchaseStatus {
|
||||
*/
|
||||
QueryingAutoRefund = 15,
|
||||
|
||||
PendingAcceptRefund = 16,
|
||||
|
||||
/**
|
||||
* Proposal downloaded, but the user needs to accept/reject it.
|
||||
*/
|
||||
@ -1169,12 +1114,12 @@ export enum PurchaseStatus {
|
||||
/**
|
||||
* The user has rejected the proposal.
|
||||
*/
|
||||
ProposalRefused = 50,
|
||||
AbortedProposalRefused = 50,
|
||||
|
||||
/**
|
||||
* Downloading or processing the proposal has failed permanently.
|
||||
*/
|
||||
ProposalDownloadFailed = 51,
|
||||
FailedClaim = 51,
|
||||
|
||||
/**
|
||||
* Downloaded proposal was detected as a re-purchase.
|
||||
@ -1184,12 +1129,12 @@ export enum PurchaseStatus {
|
||||
/**
|
||||
* The payment has been aborted.
|
||||
*/
|
||||
PaymentAbortFinished = 53,
|
||||
AbortedIncompletePayment = 53,
|
||||
|
||||
/**
|
||||
* Payment was successful.
|
||||
*/
|
||||
Paid = 54,
|
||||
Done = 54,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1303,7 +1248,7 @@ export interface PurchaseRecord {
|
||||
*
|
||||
* FIXME: Put this into a separate object store?
|
||||
*/
|
||||
refunds: { [refundKey: string]: WalletRefundItem };
|
||||
// refunds: { [refundKey: string]: WalletRefundItem };
|
||||
|
||||
/**
|
||||
* 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.
|
||||
}
|
||||
|
||||
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
|
||||
* 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",
|
||||
describeContents<FixupRecord>({
|
||||
|
@ -69,7 +69,6 @@ import {
|
||||
DenominationRecord,
|
||||
PurchaseStatus,
|
||||
RefreshCoinStatus,
|
||||
RefundState,
|
||||
WithdrawalGroupStatus,
|
||||
WithdrawalRecordType,
|
||||
} from "../../db.js";
|
||||
@ -384,34 +383,34 @@ export async function exportBackup(
|
||||
await tx.purchases.iter().forEachAsync(async (purch) => {
|
||||
const refunds: BackupRefundItem[] = [];
|
||||
purchaseProposalIdSet.add(purch.proposalId);
|
||||
for (const refundKey of Object.keys(purch.refunds)) {
|
||||
const ri = purch.refunds[refundKey];
|
||||
const common = {
|
||||
coin_pub: ri.coinPub,
|
||||
execution_time: ri.executionTime,
|
||||
obtained_time: ri.obtainedTime,
|
||||
refund_amount: Amounts.stringify(ri.refundAmount),
|
||||
rtransaction_id: ri.rtransactionId,
|
||||
total_refresh_cost_bound: Amounts.stringify(
|
||||
ri.totalRefreshCostBound,
|
||||
),
|
||||
};
|
||||
switch (ri.type) {
|
||||
case RefundState.Applied:
|
||||
refunds.push({ type: BackupRefundState.Applied, ...common });
|
||||
break;
|
||||
case RefundState.Failed:
|
||||
refunds.push({ type: BackupRefundState.Failed, ...common });
|
||||
break;
|
||||
case RefundState.Pending:
|
||||
refunds.push({ type: BackupRefundState.Pending, ...common });
|
||||
break;
|
||||
}
|
||||
}
|
||||
// for (const refundKey of Object.keys(purch.refunds)) {
|
||||
// const ri = purch.refunds[refundKey];
|
||||
// const common = {
|
||||
// coin_pub: ri.coinPub,
|
||||
// execution_time: ri.executionTime,
|
||||
// obtained_time: ri.obtainedTime,
|
||||
// refund_amount: Amounts.stringify(ri.refundAmount),
|
||||
// rtransaction_id: ri.rtransactionId,
|
||||
// total_refresh_cost_bound: Amounts.stringify(
|
||||
// ri.totalRefreshCostBound,
|
||||
// ),
|
||||
// };
|
||||
// switch (ri.type) {
|
||||
// case RefundState.Applied:
|
||||
// refunds.push({ type: BackupRefundState.Applied, ...common });
|
||||
// break;
|
||||
// case RefundState.Failed:
|
||||
// refunds.push({ type: BackupRefundState.Failed, ...common });
|
||||
// break;
|
||||
// case RefundState.Pending:
|
||||
// refunds.push({ type: BackupRefundState.Pending, ...common });
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
let propStatus: BackupProposalStatus;
|
||||
switch (purch.purchaseStatus) {
|
||||
case PurchaseStatus.Paid:
|
||||
case PurchaseStatus.Done:
|
||||
case PurchaseStatus.QueryingAutoRefund:
|
||||
case PurchaseStatus.QueryingRefund:
|
||||
propStatus = BackupProposalStatus.Paid;
|
||||
@ -422,19 +421,19 @@ export async function exportBackup(
|
||||
case PurchaseStatus.Paying:
|
||||
propStatus = BackupProposalStatus.Proposed;
|
||||
break;
|
||||
case PurchaseStatus.ProposalDownloadFailed:
|
||||
case PurchaseStatus.PaymentAbortFinished:
|
||||
case PurchaseStatus.FailedClaim:
|
||||
case PurchaseStatus.AbortedIncompletePayment:
|
||||
propStatus = BackupProposalStatus.PermanentlyFailed;
|
||||
break;
|
||||
case PurchaseStatus.AbortingWithRefund:
|
||||
case PurchaseStatus.ProposalRefused:
|
||||
case PurchaseStatus.AbortedProposalRefused:
|
||||
propStatus = BackupProposalStatus.Refused;
|
||||
break;
|
||||
case PurchaseStatus.RepurchaseDetected:
|
||||
propStatus = BackupProposalStatus.Repurchase;
|
||||
break;
|
||||
default: {
|
||||
const error: never = purch.purchaseStatus;
|
||||
const error = purch.purchaseStatus;
|
||||
throw Error(`purchase status ${error} is not handled`);
|
||||
}
|
||||
}
|
||||
|
@ -49,9 +49,7 @@ import {
|
||||
PurchasePayInfo,
|
||||
RefreshCoinStatus,
|
||||
RefreshSessionRecord,
|
||||
RefundState,
|
||||
WalletContractData,
|
||||
WalletRefundItem,
|
||||
WalletStoresV1,
|
||||
WgInfo,
|
||||
WithdrawalGroupStatus,
|
||||
@ -65,7 +63,6 @@ import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||
import {
|
||||
makeCoinAvailable,
|
||||
makeTombstoneId,
|
||||
makeTransactionId,
|
||||
TombstoneTag,
|
||||
} from "../common.js";
|
||||
import { getExchangeDetails } from "../exchanges.js";
|
||||
@ -576,16 +573,16 @@ export async function importBackup(
|
||||
let proposalStatus: PurchaseStatus;
|
||||
switch (backupPurchase.proposal_status) {
|
||||
case BackupProposalStatus.Paid:
|
||||
proposalStatus = PurchaseStatus.Paid;
|
||||
proposalStatus = PurchaseStatus.Done;
|
||||
break;
|
||||
case BackupProposalStatus.Proposed:
|
||||
proposalStatus = PurchaseStatus.Proposed;
|
||||
break;
|
||||
case BackupProposalStatus.PermanentlyFailed:
|
||||
proposalStatus = PurchaseStatus.PaymentAbortFinished;
|
||||
proposalStatus = PurchaseStatus.AbortedIncompletePayment;
|
||||
break;
|
||||
case BackupProposalStatus.Refused:
|
||||
proposalStatus = PurchaseStatus.ProposalRefused;
|
||||
proposalStatus = PurchaseStatus.AbortedProposalRefused;
|
||||
break;
|
||||
case BackupProposalStatus.Repurchase:
|
||||
proposalStatus = PurchaseStatus.RepurchaseDetected;
|
||||
@ -596,48 +593,48 @@ export async function importBackup(
|
||||
}
|
||||
}
|
||||
if (!existingPurchase) {
|
||||
const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
||||
for (const backupRefund of backupPurchase.refunds) {
|
||||
const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
||||
const coin = await tx.coins.get(backupRefund.coin_pub);
|
||||
checkBackupInvariant(!!coin);
|
||||
const denom = await tx.denominations.get([
|
||||
coin.exchangeBaseUrl,
|
||||
coin.denomPubHash,
|
||||
]);
|
||||
checkBackupInvariant(!!denom);
|
||||
const common = {
|
||||
coinPub: backupRefund.coin_pub,
|
||||
executionTime: backupRefund.execution_time,
|
||||
obtainedTime: backupRefund.obtained_time,
|
||||
refundAmount: Amounts.stringify(backupRefund.refund_amount),
|
||||
refundFee: Amounts.stringify(denom.fees.feeRefund),
|
||||
rtransactionId: backupRefund.rtransaction_id,
|
||||
totalRefreshCostBound: Amounts.stringify(
|
||||
backupRefund.total_refresh_cost_bound,
|
||||
),
|
||||
};
|
||||
switch (backupRefund.type) {
|
||||
case BackupRefundState.Applied:
|
||||
refunds[key] = {
|
||||
type: RefundState.Applied,
|
||||
...common,
|
||||
};
|
||||
break;
|
||||
case BackupRefundState.Failed:
|
||||
refunds[key] = {
|
||||
type: RefundState.Failed,
|
||||
...common,
|
||||
};
|
||||
break;
|
||||
case BackupRefundState.Pending:
|
||||
refunds[key] = {
|
||||
type: RefundState.Pending,
|
||||
...common,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
//const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
||||
// for (const backupRefund of backupPurchase.refunds) {
|
||||
// const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
||||
// const coin = await tx.coins.get(backupRefund.coin_pub);
|
||||
// checkBackupInvariant(!!coin);
|
||||
// const denom = await tx.denominations.get([
|
||||
// coin.exchangeBaseUrl,
|
||||
// coin.denomPubHash,
|
||||
// ]);
|
||||
// checkBackupInvariant(!!denom);
|
||||
// const common = {
|
||||
// coinPub: backupRefund.coin_pub,
|
||||
// executionTime: backupRefund.execution_time,
|
||||
// obtainedTime: backupRefund.obtained_time,
|
||||
// refundAmount: Amounts.stringify(backupRefund.refund_amount),
|
||||
// refundFee: Amounts.stringify(denom.fees.feeRefund),
|
||||
// rtransactionId: backupRefund.rtransaction_id,
|
||||
// totalRefreshCostBound: Amounts.stringify(
|
||||
// backupRefund.total_refresh_cost_bound,
|
||||
// ),
|
||||
// };
|
||||
// switch (backupRefund.type) {
|
||||
// case BackupRefundState.Applied:
|
||||
// refunds[key] = {
|
||||
// type: RefundState.Applied,
|
||||
// ...common,
|
||||
// };
|
||||
// break;
|
||||
// case BackupRefundState.Failed:
|
||||
// refunds[key] = {
|
||||
// type: RefundState.Failed,
|
||||
// ...common,
|
||||
// };
|
||||
// break;
|
||||
// case BackupRefundState.Pending:
|
||||
// refunds[key] = {
|
||||
// type: RefundState.Pending,
|
||||
// ...common,
|
||||
// };
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
const parsedContractTerms = codecForMerchantContractTerms().decode(
|
||||
backupPurchase.contract_terms_raw,
|
||||
);
|
||||
@ -694,7 +691,7 @@ export async function importBackup(
|
||||
posConfirmation: backupPurchase.pos_confirmation,
|
||||
lastSessionId: undefined,
|
||||
download,
|
||||
refunds,
|
||||
//refunds,
|
||||
claimToken: backupPurchase.claim_token,
|
||||
downloadSessionId: backupPurchase.download_session_id,
|
||||
merchantBaseUrl: backupPurchase.merchant_base_url,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -81,7 +81,7 @@ import {
|
||||
readUnexpectedResponseDetails,
|
||||
} from "@gnu-taler/taler-util/http";
|
||||
import { checkDbInvariant } from "../util/invariants.js";
|
||||
import { GetReadWriteAccess } from "../util/query.js";
|
||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
|
||||
import {
|
||||
constructTaskIdentifier,
|
||||
OperationAttemptResult,
|
||||
@ -874,18 +874,13 @@ async function processRefreshSession(
|
||||
await refreshReveal(ws, refreshGroupId, coinIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
export interface RefreshOutputInfo {
|
||||
outputPerCoin: AmountJson[];
|
||||
}
|
||||
|
||||
export async function calculateRefreshOutput(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadWriteAccess<{
|
||||
tx: GetReadOnlyAccess<{
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
@ -893,12 +888,7 @@ export async function createRefreshGroup(
|
||||
}>,
|
||||
currency: string,
|
||||
oldCoinPubs: CoinRefreshRequest[],
|
||||
reason: RefreshReason,
|
||||
reasonDetails?: RefreshReasonDetails,
|
||||
): Promise<RefreshGroupId> {
|
||||
const refreshGroupId = encodeCrock(getRandomBytes(32));
|
||||
|
||||
const inputPerCoin: AmountJson[] = [];
|
||||
): Promise<RefreshOutputInfo> {
|
||||
const estimatedOutputPerCoin: AmountJson[] = [];
|
||||
|
||||
const denomsPerExchange: Record<string, DenominationRecord[]> = {};
|
||||
@ -918,6 +908,47 @@ export async function createRefreshGroup(
|
||||
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) {
|
||||
const coin = await tx.coins.get(ocp.coinPub);
|
||||
checkDbInvariant(!!coin, "coin must be in database");
|
||||
@ -962,19 +993,39 @@ export async function createRefreshGroup(
|
||||
id: `txn:refresh:${refreshGroupId}`,
|
||||
};
|
||||
}
|
||||
const refreshAmount = ocp.amount;
|
||||
inputPerCoin.push(Amounts.parseOrThrow(refreshAmount));
|
||||
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 = {
|
||||
operationStatus: RefreshOperationStatus.Pending,
|
||||
@ -987,7 +1038,7 @@ export async function createRefreshGroup(
|
||||
reason,
|
||||
refreshGroupId,
|
||||
refreshSessionPerCoin: oldCoinPubs.map(() => undefined),
|
||||
inputPerCoin: inputPerCoin.map((x) => Amounts.stringify(x)),
|
||||
inputPerCoin: oldCoinPubs.map((x) => x.amount),
|
||||
estimatedOutputPerCoin: estimatedOutputPerCoin.map((x) =>
|
||||
Amounts.stringify(x),
|
||||
),
|
||||
|
@ -45,7 +45,7 @@ import {
|
||||
PreparePayResultType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
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 { checkLogicInvariant } from "../util/invariants.js";
|
||||
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
||||
@ -416,7 +416,7 @@ export async function runIntegrationTest(
|
||||
|
||||
logger.trace("refund URI", refundUri);
|
||||
|
||||
await applyRefund(ws, refundUri);
|
||||
await startRefundQueryForUri(ws, refundUri);
|
||||
|
||||
logger.trace("integration test: applied refund");
|
||||
|
||||
@ -512,7 +512,7 @@ export async function runIntegrationTest2(
|
||||
|
||||
logger.trace("refund URI", refundUri);
|
||||
|
||||
await applyRefund(ws, refundUri);
|
||||
await startRefundQueryForUri(ws, refundUri);
|
||||
|
||||
logger.trace("integration test: applied refund");
|
||||
|
||||
|
@ -19,7 +19,6 @@
|
||||
*/
|
||||
import {
|
||||
AbsoluteTime,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
constructPayPullUri,
|
||||
constructPayPushUri,
|
||||
@ -51,9 +50,7 @@ import {
|
||||
PeerPushPaymentInitiationRecord,
|
||||
PurchaseStatus,
|
||||
PurchaseRecord,
|
||||
RefundState,
|
||||
TipRecord,
|
||||
WalletRefundItem,
|
||||
WithdrawalGroupRecord,
|
||||
WithdrawalRecordType,
|
||||
WalletContractData,
|
||||
@ -66,6 +63,7 @@ import {
|
||||
PeerPushPaymentIncomingRecord,
|
||||
PeerPushPaymentIncomingStatus,
|
||||
PeerPullPaymentInitiationRecord,
|
||||
RefundGroupRecord,
|
||||
} from "../db.js";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { PendingTaskType } from "../pending-types.js";
|
||||
@ -89,6 +87,7 @@ import { getExchangeDetails } from "./exchanges.js";
|
||||
import {
|
||||
abortPayMerchant,
|
||||
computePayMerchantTransactionState,
|
||||
computeRefundTransactionState,
|
||||
expectProposalDownload,
|
||||
extractContractData,
|
||||
processPurchasePay,
|
||||
@ -205,40 +204,15 @@ export async function getTransactionById(
|
||||
.runReadWrite(async (tx) => {
|
||||
const purchase = await tx.purchases.get(proposalId);
|
||||
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 cleanRefunds = filteredRefunds.filter(
|
||||
(x): x is WalletRefundItem => !!x,
|
||||
);
|
||||
|
||||
const contractData = download.contractData;
|
||||
const refunds = mergeRefundByExecutionTime(
|
||||
cleanRefunds,
|
||||
Amounts.zeroOfAmount(contractData.amount),
|
||||
);
|
||||
|
||||
const payOpId = TaskIdentifiers.forPay(purchase);
|
||||
const payRetryRecord = await tx.operationRetries.get(payOpId);
|
||||
|
||||
return buildTransactionForPurchase(
|
||||
purchase,
|
||||
contractData,
|
||||
refunds,
|
||||
[], // FIXME: Add refunds from refund group records here.
|
||||
payRetryRecord,
|
||||
);
|
||||
});
|
||||
@ -272,66 +246,8 @@ export async function getTransactionById(
|
||||
return buildTransactionForDeposit(depositRecord, retries);
|
||||
});
|
||||
} else if (type === TransactionType.Refund) {
|
||||
const proposalId = rest[0];
|
||||
const executionTimeStr = rest[1];
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
// FIXME!
|
||||
throw Error("not implemented");
|
||||
} else if (type === TransactionType.PeerPullDebit) {
|
||||
const peerPullPaymentIncomingId = rest[0];
|
||||
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(
|
||||
refreshGroupRecord: RefreshGroupRecord,
|
||||
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(
|
||||
purchaseRecord: PurchaseRecord,
|
||||
contractData: WalletContractData,
|
||||
refundsInfo: MergedRefundInfo[],
|
||||
refundsInfo: RefundGroupRecord[],
|
||||
ort?: OperationRetryRecord,
|
||||
): Promise<Transaction> {
|
||||
const zero = Amounts.zeroOfAmount(contractData.amount);
|
||||
@ -974,30 +811,7 @@ async function buildTransactionForPurchase(
|
||||
info.fulfillmentUrl = contractData.fulfillmentUrl;
|
||||
}
|
||||
|
||||
const totalRefund = refundsInfo.reduce(
|
||||
(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 refunds: RefundInfoShort[] = [];
|
||||
|
||||
const timestamp = purchaseRecord.timestampAccept;
|
||||
checkDbInvariant(!!timestamp);
|
||||
@ -1008,7 +822,7 @@ async function buildTransactionForPurchase(
|
||||
case PurchaseStatus.AbortingWithRefund:
|
||||
status = ExtendedStatus.Aborting;
|
||||
break;
|
||||
case PurchaseStatus.Paid:
|
||||
case PurchaseStatus.Done:
|
||||
case PurchaseStatus.RepurchaseDetected:
|
||||
status = ExtendedStatus.Done;
|
||||
break;
|
||||
@ -1018,10 +832,10 @@ async function buildTransactionForPurchase(
|
||||
case PurchaseStatus.Paying:
|
||||
status = ExtendedStatus.Pending;
|
||||
break;
|
||||
case PurchaseStatus.ProposalDownloadFailed:
|
||||
case PurchaseStatus.FailedClaim:
|
||||
status = ExtendedStatus.Failed;
|
||||
break;
|
||||
case PurchaseStatus.PaymentAbortFinished:
|
||||
case PurchaseStatus.AbortedIncompletePayment:
|
||||
status = ExtendedStatus.Aborted;
|
||||
break;
|
||||
default:
|
||||
@ -1034,8 +848,8 @@ async function buildTransactionForPurchase(
|
||||
txState: computePayMerchantTransactionState(purchaseRecord),
|
||||
amountRaw: Amounts.stringify(contractData.amount),
|
||||
amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
|
||||
totalRefundRaw: Amounts.stringify(totalRefund.raw),
|
||||
totalRefundEffective: Amounts.stringify(totalRefund.effective),
|
||||
totalRefundRaw: Amounts.stringify(zero), // FIXME!
|
||||
totalRefundEffective: Amounts.stringify(zero), // FIXME!
|
||||
refundPending:
|
||||
purchaseRecord.refundAmountAwaiting === undefined
|
||||
? undefined
|
||||
@ -1057,7 +871,7 @@ async function buildTransactionForPurchase(
|
||||
refundQueryActive:
|
||||
purchaseRecord.purchaseStatus === PurchaseStatus.QueryingRefund,
|
||||
frozen:
|
||||
purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ??
|
||||
purchaseRecord.purchaseStatus === PurchaseStatus.AbortedIncompletePayment ??
|
||||
false,
|
||||
...(ort?.lastError ? { error: ort.lastError } : {}),
|
||||
};
|
||||
@ -1092,6 +906,7 @@ export async function getTransactions(
|
||||
x.tombstones,
|
||||
x.withdrawalGroups,
|
||||
x.refreshGroups,
|
||||
x.refundGroups,
|
||||
])
|
||||
.runReadOnly(async (tx) => {
|
||||
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) => {
|
||||
if (shouldSkipCurrency(transactionsRequest, rg.currency)) {
|
||||
return;
|
||||
@ -1318,47 +1141,13 @@ export async function getTransactions(
|
||||
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 payRetryRecord = await tx.operationRetries.get(payOpId);
|
||||
transactions.push(
|
||||
await buildTransactionForPurchase(
|
||||
purchase,
|
||||
contractData,
|
||||
refunds,
|
||||
[], // FIXME!
|
||||
payRetryRecord,
|
||||
),
|
||||
);
|
||||
@ -1425,7 +1214,7 @@ export type ParsedTransactionIdentifier =
|
||||
| { tag: TransactionType.PeerPushCredit; peerPushPaymentIncomingId: string }
|
||||
| { tag: TransactionType.PeerPushDebit; pursePub: 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.Withdrawal; withdrawalGroupId: string };
|
||||
|
||||
@ -1448,7 +1237,7 @@ export function constructTransactionIdentifier(
|
||||
case TransactionType.Refresh:
|
||||
return `txn:${pTxId.tag}:${pTxId.refreshGroupId}`;
|
||||
case TransactionType.Refund:
|
||||
return `txn:${pTxId.tag}:${pTxId.proposalId}:${pTxId.executionTime}`;
|
||||
return `txn:${pTxId.tag}:${pTxId.refundGroupId}`;
|
||||
case TransactionType.Tip:
|
||||
return `txn:${pTxId.tag}:${pTxId.walletTipId}`;
|
||||
case TransactionType.Withdrawal:
|
||||
@ -1490,8 +1279,7 @@ export function parseTransactionIdentifier(
|
||||
case TransactionType.Refund:
|
||||
return {
|
||||
tag: TransactionType.Refund,
|
||||
proposalId: rest[0],
|
||||
executionTime: rest[1],
|
||||
refundGroupId: rest[0],
|
||||
};
|
||||
case TransactionType.Tip:
|
||||
return {
|
||||
|
@ -35,7 +35,7 @@ import {
|
||||
IDBKeyPath,
|
||||
IDBKeyRange,
|
||||
} 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");
|
||||
|
||||
|
@ -76,6 +76,11 @@ export namespace OperationAttemptResult {
|
||||
result: undefined,
|
||||
};
|
||||
}
|
||||
export function longpoll(): OperationAttemptResult<unknown, unknown> {
|
||||
return {
|
||||
type: OperationAttemptResultType.Longpoll,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface OperationAttemptFinishedResult<T> {
|
||||
|
@ -36,7 +36,7 @@ import {
|
||||
AddKnownBankAccountsRequest,
|
||||
ApplyDevExperimentRequest,
|
||||
ApplyRefundFromPurchaseIdRequest,
|
||||
ApplyRefundRequest,
|
||||
AcceptRefundRequest,
|
||||
ApplyRefundResponse,
|
||||
BackupRecovery,
|
||||
BalancesResponse,
|
||||
@ -90,6 +90,7 @@ import {
|
||||
RetryTransactionRequest,
|
||||
SetCoinSuspendedRequest,
|
||||
SetWalletDeviceIdRequest,
|
||||
StartRefundQueryRequest,
|
||||
TestPayArgs,
|
||||
TestPayResult,
|
||||
Transaction,
|
||||
@ -149,9 +150,8 @@ export enum WalletApiOperation {
|
||||
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
||||
GetPendingOperations = "getPendingOperations",
|
||||
SetExchangeTosAccepted = "setExchangeTosAccepted",
|
||||
ApplyRefund = "applyRefund",
|
||||
ApplyRefundFromPurchaseId = "applyRefundFromPurchaseId",
|
||||
PrepareRefund = "prepareRefund",
|
||||
StartRefundQueryForUri = "startRefundQueryForUri",
|
||||
StartRefundQuery = "startRefundQuery",
|
||||
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
|
||||
GetExchangeTos = "getExchangeTos",
|
||||
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
|
||||
@ -435,22 +435,16 @@ export type ConfirmPayOp = {
|
||||
/**
|
||||
* Check for a refund based on a taler://refund URI.
|
||||
*/
|
||||
export type ApplyRefundOp = {
|
||||
op: WalletApiOperation.ApplyRefund;
|
||||
request: ApplyRefundRequest;
|
||||
response: ApplyRefundResponse;
|
||||
};
|
||||
|
||||
export type ApplyRefundFromPurchaseIdOp = {
|
||||
op: WalletApiOperation.ApplyRefundFromPurchaseId;
|
||||
request: ApplyRefundFromPurchaseIdRequest;
|
||||
response: ApplyRefundResponse;
|
||||
};
|
||||
|
||||
export type PrepareRefundOp = {
|
||||
op: WalletApiOperation.PrepareRefund;
|
||||
export type StartRefundQueryForUriOp = {
|
||||
op: WalletApiOperation.StartRefundQueryForUri;
|
||||
request: PrepareRefundRequest;
|
||||
response: PrepareRefundResult;
|
||||
response: EmptyObject;
|
||||
};
|
||||
|
||||
export type StartRefundQueryOp = {
|
||||
op: WalletApiOperation.StartRefundQuery;
|
||||
request: StartRefundQueryRequest;
|
||||
response: EmptyObject;
|
||||
};
|
||||
|
||||
// group: Tipping
|
||||
@ -954,9 +948,8 @@ export type WalletOperations = {
|
||||
[WalletApiOperation.RetryTransaction]: RetryTransactionOp;
|
||||
[WalletApiOperation.PrepareTip]: PrepareTipOp;
|
||||
[WalletApiOperation.AcceptTip]: AcceptTipOp;
|
||||
[WalletApiOperation.ApplyRefund]: ApplyRefundOp;
|
||||
[WalletApiOperation.ApplyRefundFromPurchaseId]: ApplyRefundFromPurchaseIdOp;
|
||||
[WalletApiOperation.PrepareRefund]: PrepareRefundOp;
|
||||
[WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp;
|
||||
[WalletApiOperation.StartRefundQuery]: StartRefundQueryOp;
|
||||
[WalletApiOperation.ListCurrencies]: ListCurrenciesOp;
|
||||
[WalletApiOperation.GetWithdrawalDetailsForAmount]: GetWithdrawalDetailsForAmountOp;
|
||||
[WalletApiOperation.GetWithdrawalDetailsForUri]: GetWithdrawalDetailsForUriOp;
|
||||
|
@ -48,6 +48,7 @@ import {
|
||||
RefreshReason,
|
||||
TalerError,
|
||||
TalerErrorCode,
|
||||
TransactionType,
|
||||
URL,
|
||||
ValidateIbanResponse,
|
||||
WalletCoreVersion,
|
||||
@ -95,6 +96,7 @@ import {
|
||||
codecForRetryTransactionRequest,
|
||||
codecForSetCoinSuspendedRequest,
|
||||
codecForSetWalletDeviceIdRequest,
|
||||
codecForStartRefundQueryRequest,
|
||||
codecForSuspendTransaction,
|
||||
codecForTestPayArgs,
|
||||
codecForTransactionByIdRequest,
|
||||
@ -188,13 +190,11 @@ import {
|
||||
} from "./operations/exchanges.js";
|
||||
import { getMerchantInfo } from "./operations/merchants.js";
|
||||
import {
|
||||
applyRefund,
|
||||
applyRefundFromPurchaseId,
|
||||
confirmPay,
|
||||
getContractTermsDetails,
|
||||
preparePayForUri,
|
||||
prepareRefund,
|
||||
processPurchase,
|
||||
startRefundQueryForUri,
|
||||
} from "./operations/pay-merchant.js";
|
||||
import {
|
||||
checkPeerPullPaymentInitiation,
|
||||
@ -233,6 +233,7 @@ import {
|
||||
deleteTransaction,
|
||||
getTransactionById,
|
||||
getTransactions,
|
||||
parseTransactionIdentifier,
|
||||
resumeTransaction,
|
||||
retryTransaction,
|
||||
suspendTransaction,
|
||||
@ -276,6 +277,7 @@ import {
|
||||
WalletCoreApiClient,
|
||||
WalletCoreResponseType,
|
||||
} from "./wallet-api-types.js";
|
||||
import { startQueryRefund } from "./operations/pay-merchant.js";
|
||||
|
||||
const logger = new Logger("wallet.ts");
|
||||
|
||||
@ -1141,14 +1143,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag);
|
||||
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: {
|
||||
const req =
|
||||
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
|
||||
@ -1292,9 +1286,22 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
const req = codecForPrepareTipRequest().decode(payload);
|
||||
return await prepareTip(ws, req.talerTipUri);
|
||||
}
|
||||
case WalletApiOperation.PrepareRefund: {
|
||||
case WalletApiOperation.StartRefundQueryForUri: {
|
||||
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: {
|
||||
const req = codecForAcceptTipRequest().decode(payload);
|
||||
|
@ -35,7 +35,7 @@ export function useComponentState({
|
||||
|
||||
const info = useAsyncAsHook(async () => {
|
||||
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,
|
||||
});
|
||||
return { refund, uri: talerRefundUri };
|
||||
@ -70,8 +70,8 @@ export function useComponentState({
|
||||
const { refund, uri } = info.response;
|
||||
|
||||
const doAccept = async (): Promise<void> => {
|
||||
const res = await api.wallet.call(WalletApiOperation.ApplyRefund, {
|
||||
talerRefundUri: uri,
|
||||
const res = await api.wallet.call(WalletApiOperation.AcceptPurchaseRefund, {
|
||||
transactionId: uri,
|
||||
});
|
||||
|
||||
onSuccess(res.transactionId);
|
||||
|
@ -72,7 +72,7 @@ describe("Refund CTA states", () => {
|
||||
onSuccess: nullFunction,
|
||||
};
|
||||
|
||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
||||
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||
awaiting: "EUR:2",
|
||||
effectivePaid: "EUR:2",
|
||||
gone: "EUR:0",
|
||||
@ -126,7 +126,7 @@ describe("Refund CTA states", () => {
|
||||
},
|
||||
};
|
||||
|
||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
||||
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||
awaiting: "EUR:2",
|
||||
effectivePaid: "EUR:2",
|
||||
gone: "EUR:0",
|
||||
@ -187,7 +187,7 @@ describe("Refund CTA states", () => {
|
||||
},
|
||||
};
|
||||
|
||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
||||
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||
awaiting: "EUR:2",
|
||||
effectivePaid: "EUR:2",
|
||||
gone: "EUR:0",
|
||||
@ -203,7 +203,7 @@ describe("Refund CTA states", () => {
|
||||
summary: "the summary",
|
||||
} as OrderShortInfo,
|
||||
});
|
||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
||||
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||
awaiting: "EUR:1",
|
||||
effectivePaid: "EUR:2",
|
||||
gone: "EUR:0",
|
||||
@ -219,7 +219,7 @@ describe("Refund CTA states", () => {
|
||||
summary: "the summary",
|
||||
} as OrderShortInfo,
|
||||
});
|
||||
handler.addWalletCallResponse(WalletApiOperation.PrepareRefund, undefined, {
|
||||
handler.addWalletCallResponse(WalletApiOperation.StartRefundQueryForUri, undefined, {
|
||||
awaiting: "EUR:0",
|
||||
effectivePaid: "EUR:2",
|
||||
gone: "EUR:0",
|
||||
|
Loading…
Reference in New Issue
Block a user