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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,6 +76,11 @@ export namespace OperationAttemptResult {
result: undefined,
};
}
export function longpoll(): OperationAttemptResult<unknown, unknown> {
return {
type: OperationAttemptResultType.Longpoll,
}
}
}
export interface OperationAttemptFinishedResult<T> {

View File

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

View File

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

View File

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

View File

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