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:
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,
};
}