diff --git a/packages/taler-wallet-core/src/balance.test.ts b/packages/taler-wallet-core/src/balance.test.ts index f3576906a..66261e1ee 100644 --- a/packages/taler-wallet-core/src/balance.test.ts +++ b/packages/taler-wallet-core/src/balance.test.ts @@ -17,6 +17,7 @@ import { test } from "node:test"; import { Amounts, + BalanceFlag, RefreshReason, ScopeType, stringifyScopeInfo, @@ -29,6 +30,8 @@ import { ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, DepositOperationStatus, + PeerPushCreditStatus, + PeerPullPaymentCreditStatus, PurchaseStatus, RefreshOperationStatus, WalletCoinAvailability, @@ -36,6 +39,8 @@ import { WalletExchangeEntry, WalletRefreshGroup, WalletDepositGroup, + WalletPeerPushCredit, + WalletPeerPullCredit, } from "./db/records.js"; import { WalletDbTransaction } from "./db/transaction.js"; import { WalletExecutionContext } from "./wallet.js"; @@ -628,3 +633,81 @@ test("active purchases share one deduplicated coin lookup", async () => { assert.strictEqual(byUrl.get(exchangeA)?.pendingOutgoing, "TESTKUDOS:4"); assert.strictEqual(byUrl.get(exchangeB)?.pendingOutgoing, "TESTKUDOS:2"); }); + +test("a peer credit blocked on KYC raises the incoming-kyc flag", async () => { + // Without the flag a wallet cannot tell, at the balance level, a transfer + // that is waiting for the exchange from one that is waiting for the user. + // Both show up only as pendingIncoming, which is what left the iOS wallet + // showing a bare "Pending incoming" row for a receipt the user had to act + // on (bugs.taler.net/n/11914). Withdrawals and deposits have always + // raised their flag; peer credits did not. + const exchange = "https://peer-kyc.example/"; + for (const status of [ + PeerPushCreditStatus.PendingMergeKycRequired, + PeerPushCreditStatus.PendingBalanceKycRequired, + PeerPushCreditStatus.PendingBalanceKycInit, + PeerPushCreditStatus.SuspendedMergeKycRequired, + ]) { + const { wex, tx } = makeBalanceContext([makeExchange(exchange)]); + tx.getActivePeerPushCredits = async () => [ + { + exchangeBaseUrl: exchange, + estimatedAmountEffective: "TESTKUDOS:2.5", + status, + } as unknown as WalletPeerPushCredit, + ]; + const balances = await getBalancesInsideTransaction(wex, tx); + const b = balances.balances.find( + (x) => x.scopeInfo.type === ScopeType.Exchange, + ); + assert.strictEqual(b?.pendingIncoming, "TESTKUDOS:2.5", `push ${status}`); + assert.ok( + b?.flags.includes(BalanceFlag.IncomingKyc), + `push ${status} should raise incoming-kyc, got ${JSON.stringify(b?.flags)}`, + ); + } + + for (const status of [ + PeerPullPaymentCreditStatus.PendingMergeKycRequired, + PeerPullPaymentCreditStatus.PendingBalanceKycRequired, + PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired, + ]) { + const { wex, tx } = makeBalanceContext([makeExchange(exchange)]); + tx.getActivePeerPullCredits = async () => [ + { + exchangeBaseUrl: exchange, + amount: "TESTKUDOS:2.5", + estimatedAmountEffective: "TESTKUDOS:2.5", + status, + } as unknown as WalletPeerPullCredit, + ]; + const balances = await getBalancesInsideTransaction(wex, tx); + const b = balances.balances.find( + (x) => x.scopeInfo.type === ScopeType.Exchange, + ); + assert.ok( + b?.flags.includes(BalanceFlag.IncomingKyc), + `pull ${status} should raise incoming-kyc, got ${JSON.stringify(b?.flags)}`, + ); + } +}); + +test("a peer credit that is merely pending raises no flag", async () => { + // The other half of the claim: the flag has to mean "waiting for you", so + // an ordinary pending merge must not set it. + const exchange = "https://peer-plain.example/"; + const { wex, tx } = makeBalanceContext([makeExchange(exchange)]); + tx.getActivePeerPushCredits = async () => [ + { + exchangeBaseUrl: exchange, + estimatedAmountEffective: "TESTKUDOS:2.5", + status: PeerPushCreditStatus.PendingMerge, + } as unknown as WalletPeerPushCredit, + ]; + const balances = await getBalancesInsideTransaction(wex, tx); + const b = balances.balances.find( + (x) => x.scopeInfo.type === ScopeType.Exchange, + ); + assert.strictEqual(b?.pendingIncoming, "TESTKUDOS:2.5"); + assert.deepStrictEqual(b?.flags, []); +}); diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts index b13a9b3d1..0a6631054 100644 --- a/packages/taler-wallet-core/src/balance.ts +++ b/packages/taler-wallet-core/src/balance.ts @@ -817,6 +817,27 @@ export async function getBalancesInsideTransaction( const peerPushCredits = await tx.getActivePeerPushCredits(); for (const rec of peerPushCredits) { + // A separate switch for the flag, the way the deposit-group loop below + // does it: the pending-amount cases and the needs-the-user cases are + // different questions and overlap only partly. + // + // Without this, a wallet cannot tell the two apart at the balance + // level. The amount shows up as pendingIncoming either way, so a + // transfer that is waiting for the exchange and one that is waiting + // for the user to pass a legitimization check render identically, and + // the only way to find out which is to open the transaction. + switch (rec.status) { + case PeerPushCreditStatus.PendingBalanceKycInit: + case PeerPushCreditStatus.PendingBalanceKycRequired: + case PeerPushCreditStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.SuspendedBalanceKycInit: + case PeerPushCreditStatus.SuspendedBalanceKycRequired: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + await balanceStore.setFlagIncomingKyc( + Amounts.currencyOf(rec.estimatedAmountEffective), + rec.exchangeBaseUrl, + ); + } switch (rec.status) { case PeerPushCreditStatus.PendingMerge: case PeerPushCreditStatus.PendingBalanceKycInit: @@ -845,6 +866,18 @@ export async function getBalancesInsideTransaction( const peerPullCredits = await tx.getActivePeerPullCredits(); for (const rec of peerPullCredits) { + switch (rec.status) { + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + await balanceStore.setFlagIncomingKyc( + Amounts.currencyOf(rec.amount), + rec.exchangeBaseUrl, + ); + } switch (rec.status) { case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: case PeerPullPaymentCreditStatus.PendingBalanceKycInit: