wallet-core: refund DD37 refactoring

This commit is contained in:
Florian Dold 2023-05-05 19:03:44 +02:00
parent a0bf83fbb5
commit 7f0edb6a78
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
20 changed files with 1112 additions and 1370 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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