taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 51d2d831b0a6831cc7f722b0f0358ee073e52635
parent a314508d92884f20890987f3161d5a245708e931
Author: Florian Dold <florian@dold.me>
Date:   Wed, 16 Apr 2025 15:27:55 +0200

wallet-core: hide repurchase transactions by default, delete them when parent tx gets deleted

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-repurchase.ts | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-util/src/types-taler-wallet-transactions.ts | 7+++++++
Mpackages/taler-wallet-core/src/db.ts | 2+-
Mpackages/taler-wallet-core/src/pay-merchant.ts | 33+++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/transactions.ts | 15++++++++++++++-
5 files changed, 116 insertions(+), 13 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-repurchase.ts b/packages/taler-harness/src/integrationtests/test-repurchase.ts @@ -19,11 +19,12 @@ */ import { ConfirmPayResultType, + j2s, PreparePayResultType, succeedOrThrow, TalerCorebankApiClient, - TalerMerchantApi, TalerMerchantInstanceHttpClient, + TransactionType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { @@ -51,12 +52,6 @@ export async function runRepurchaseTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - } satisfies TalerMerchantApi.Order; - const merchantClient = new TalerMerchantInstanceHttpClient( merchant.makeInstanceBaseUrl(), ); @@ -90,11 +85,14 @@ export async function runRepurchaseTest(t: GlobalTestState) { preparePayOneResult.status === PreparePayResultType.PaymentPossible, ); - const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { - transactionId: preparePayOneResult.transactionId, - }); + const confirmPayResp = await walletClient.call( + WalletApiOperation.ConfirmPay, + { + transactionId: preparePayOneResult.transactionId, + }, + ); - t.assertTrue(r2.type === ConfirmPayResultType.Done); + t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Done); const orderTwoResp = succeedOrThrow( await merchantClient.createOrder(undefined, { @@ -174,6 +172,58 @@ export async function runRepurchaseTest(t: GlobalTestState) { t.assertTrue( preparePayThreeResult.status === PreparePayResultType.AlreadyConfirmed, ); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + { + const txnsNormal = await walletClient.call( + WalletApiOperation.GetTransactionsV2, + {}, + ); + + console.log(j2s(txnsNormal)); + + t.assertDeepEqual(txnsNormal.transactions.length, 2); + } + + // The list of all transactions should also include repurchases. + { + const txnsAll = await walletClient.call( + WalletApiOperation.GetTransactionsV2, + { + includeAll: true, + }, + ); + + console.log(j2s(txnsAll)); + + const numPayments = txnsAll.transactions.filter( + (x) => x.type === TransactionType.Payment, + ).length; + + t.assertDeepEqual(numPayments, 3); + } + + // Test that deleting the original transaction also deletes the repurchase transaction. + + await walletClient.call(WalletApiOperation.DeleteTransaction, { + transactionId: preparePayOneResult.transactionId, + }); + + { + const txnsAll = await walletClient.call( + WalletApiOperation.GetTransactionsV2, + { + includeAll: true, + }, + ); + + const numPayments = txnsAll.transactions.filter( + (x) => x.type === TransactionType.Payment, + ).length; + + t.assertDeepEqual(numPayments, 0); + } } runRepurchaseTest.suites = ["wallet"]; diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts @@ -121,6 +121,12 @@ export interface GetTransactionsV2Request { includeRefreshes?: boolean; /** + * If true, include transactions that would usually be filtered out. + * Implies includeRefreshes. + */ + includeAll?: boolean; + + /** * Only return transactions before/after this offset. */ offsetTransactionId?: TransactionIdStr; @@ -889,6 +895,7 @@ export const codecForGetTransactionsV2Request = ), ) .property("includeRefreshes", codecOptional(codecForBoolean())) + .property("includeAll", codecOptional(codecForBoolean())) .build("GetTransactionsV2Request"); export const codecForTransactionsRequest = (): Codec<TransactionsRequest> => diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1536,7 +1536,7 @@ export interface WithdrawalGroupRecord { /** * Delay to wait until the next withdrawal attempt. - * + * * @deprecated by https://bugs.gnunet.org/view.php?id=9694 */ kycWithdrawalDelay?: TalerProtocolDuration; diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -417,6 +417,39 @@ export class PayMerchantTransactionContext implements TransactionContext { if (!rec) { return { notifs }; } + let relatedTransactions: PurchaseRecord[] = []; + // Automatically delete transactions that are a repurchase of this transaction, + // as they're typically hidden. + if (rec.download?.fulfillmentUrl) { + const otherTransactions = + await tx.purchases.indexes.byFulfillmentUrl.getAll( + rec.download.fulfillmentUrl, + ); + for (const ot of otherTransactions) { + if ( + ot.purchaseStatus === PurchaseStatus.DoneRepurchaseDetected && + ot.repurchaseProposalId === rec.proposalId + ) { + relatedTransactions.push(ot); + } + } + } + for (const rt of relatedTransactions) { + const otherCtx = new PayMerchantTransactionContext( + this.wex, + rt.proposalId, + ); + await tx.purchases.delete(rt.proposalId); + await otherCtx.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState: computePayMerchantTransactionState(rt), + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + } const oldTxState = computePayMerchantTransactionState(rec); await tx.purchases.delete(rec.proposalId); await this.updateTransactionMeta(tx); diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -54,6 +54,7 @@ import { OPERATION_STATUS_DONE_LAST, OPERATION_STATUS_NONFINAL_FIRST, OPERATION_STATUS_NONFINAL_LAST, + PurchaseStatus, timestampPreciseToDb, TransactionMetaRecord, WalletDbAllStoresReadOnlyTransaction, @@ -189,10 +190,22 @@ function checkFilterIncludes( } const parsedTx = parseTransactionIdentifier(mtx.transactionId); - if (parsedTx?.tag === TransactionType.Refresh && !req?.includeRefreshes) { + if ( + parsedTx?.tag === TransactionType.Refresh && + !(req?.includeRefreshes || req?.includeAll) + ) { return false; } + if (!req?.includeAll) { + if ( + parsedTx?.tag === TransactionType.Payment && + mtx.status === PurchaseStatus.DoneRepurchaseDetected + ) { + return false; + } + } + let included: boolean; const filter = req?.filterByState;