taler-typescript-core

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

commit db6f5c92214c6947e39318881691e5e9fe7b8cdb
parent 6c33d36abb293fa553d4a24359d6fb41c4521dee
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 31 Jul 2025 19:24:17 +0200

wallet-core: add automaticExecutable field to choice details

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-wallet-tokens.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-wallet.ts | 14+++++++++++---
Mpackages/taler-wallet-core/src/pay-merchant.ts | 44+++++++++++++++++++++++++++++---------------
3 files changed, 100 insertions(+), 18 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-wallet-tokens.ts b/packages/taler-harness/src/integrationtests/test-wallet-tokens.ts @@ -247,6 +247,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { t.assertTrue(choicesRes.defaultChoiceIndex === 1); t.assertTrue(choicesRes.automaticExecution === false); + t.assertTrue(choicesRes.automaticExecutableIndex === undefined); t.assertTrue(choicesRes.choices[choiceIndex].status === ChoiceSelectionDetailType.PaymentPossible, @@ -311,6 +312,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { t.assertTrue(choicesRes.defaultChoiceIndex === 0); t.assertTrue(choicesRes.automaticExecution === false); + t.assertTrue(choicesRes.automaticExecutableIndex === undefined); t.assertTrue(choicesRes.choices[choiceIndex].status === ChoiceSelectionDetailType.InsufficientBalance, @@ -345,6 +347,63 @@ export async function runWalletTokensTest(t: GlobalTestState) { } { + logger.info("Payment with subscription token input and output, insufficient balance..."); + const choiceIndex = 1; + + const orderResp = succeedOrThrow( + await merchantApi.createOrder(undefined, { + order: orderJsonSubscription, + }), + ); + + let orderStatus = succeedOrThrow( + await merchantApi.getOrderDetails(undefined, orderResp.order_id), + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUri = orderStatus.taler_pay_uri; + const orderId = orderResp.order_id; + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri, + } + ); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.ChoiceSelection, + ); + + { + const choicesRes = await walletClient.call(WalletApiOperation.GetChoicesForPayment, { + transactionId: preparePayResult.transactionId, + }); + + t.assertTrue(choicesRes.defaultChoiceIndex === 0); + t.assertTrue(choicesRes.automaticExecution === false); + t.assertTrue(choicesRes.automaticExecutableIndex === choiceIndex); + + t.assertTrue(choicesRes.choices[choiceIndex].status === + ChoiceSelectionDetailType.InsufficientBalance, + ); + + const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails; + logger.info("tokenDetails:", tokenDetails); + t.assertTrue(tokenDetails?.tokensAvailable === 0); + t.assertTrue(tokenDetails?.tokensRequested === 1); + for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) { + t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 0); + t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1); + t.assertTrue(tokenDetails?.perTokenFamily[tf].causeHint === + TokenAvailabilityHint.WalletTokensAvailableInsufficient); + break; + } + } + } + + { logger.info("Payment with subscription token output..."); const choiceIndex = 0; @@ -423,6 +482,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { t.assertTrue(choicesRes.defaultChoiceIndex === choiceIndex); t.assertTrue(choicesRes.automaticExecution === true); + t.assertTrue(choicesRes.automaticExecutableIndex === choiceIndex); t.assertTrue(choicesRes.choices[choiceIndex].status === ChoiceSelectionDetailType.PaymentPossible, diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -2407,9 +2407,9 @@ export type GetChoicesForPaymentResult = { defaultChoiceIndex?: number; /** - * Whether the default choice or the only - * choice should be executed automatically without - * user confirmation. + * Whether the choice referenced by @e automaticExecutableIndex + * should be confirmed automatically without + * user interaction. * * If true, the wallet should call `confirmPay' * immediately afterwards, if false, the user @@ -2420,6 +2420,14 @@ export type GetChoicesForPaymentResult = { automaticExecution?: boolean; /** + * Index of the choice that would be set to automatically + * execute if the choice was payable. When @e automaticExecution + * is set to true, the payment should be confirmed with this + * choice index without user interaction. + */ + automaticExecutableIndex?: number; + + /** * Data extracted from the contract terms that * is relevant for payment processing in the wallet. */ diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -2630,9 +2630,11 @@ async function calculateDefaultChoice( ): Promise<{ defaultChoiceIndex?: number; automaticExecution?: boolean; + automaticExecutableIndex?: number; }> { - var defaultChoiceIndex: number | undefined = undefined; - var automaticExecution: boolean | undefined = undefined; + let defaultChoiceIndex: number | undefined; + let automaticExecution: boolean | undefined; + let automaticExecutableIndex: number | undefined; switch (contractTerms.version) { case undefined: case MerchantContractVersion.V0: @@ -2668,25 +2670,36 @@ async function calculateDefaultChoice( } } + defaultChoiceIndex = cheapestPayableIndex; + // If the cheapest choice has one subscription input and one // subscription output of the same type, and it's free, then it can be // automatically confirmed without user confirmation. // // TODO: in the future, a setting should allow the user to specify // merchants where discounts should be automatically spent. - defaultChoiceIndex = cheapestPayableIndex; - automaticExecution = - Amounts.isZero(cheapestPayableChoice.amount) && - cheapestPayableChoice.inputs.length === 1 && - cheapestPayableChoice.outputs.length === 1 && - cheapestPayableChoice.inputs[0].type === - MerchantContractInputType.Token && - cheapestPayableChoice.outputs[0].type === - MerchantContractOutputType.Token && - (cheapestPayableChoice.inputs[0].count ?? 1) === 1 && - (cheapestPayableChoice.outputs[0].count ?? 1) === 1 && - cheapestPayableChoice.inputs[0].token_family_slug === - cheapestPayableChoice.inputs[0].token_family_slug; + automaticExecution = false; + for (let i = 0; i < contractTerms.choices.length; i++) { + const choice = contractTerms.choices[i]; + const details = choiceDetails[i]; + if ( + Amounts.isZero(choice.amount) + && choice.inputs.length === 1 + && choice.outputs.length === 1 + && !details.tokenDetails?.tokensUnexpected + && choice.inputs[0].type === MerchantContractInputType.Token + && choice.outputs[0].type === MerchantContractOutputType.Token + && (choice.inputs[0].count ?? 1) === 1 + && (choice.outputs[0].count ?? 1) === 1 + && choice.inputs[0].token_family_slug === + choice.inputs[0].token_family_slug + ) { + automaticExecution = details.status === + ChoiceSelectionDetailType.PaymentPossible; + automaticExecutableIndex = i; + break; + } + } break; default: assertUnreachable(contractTerms); @@ -2695,6 +2708,7 @@ async function calculateDefaultChoice( return { defaultChoiceIndex, automaticExecution, + automaticExecutableIndex, }; }