taler-typescript-core

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

commit bc382d4674b17e9c91e10287002d40603b9d1ab7
parent 9713967b3f0d4e1d0a17f86978cc024528738877
Author: Florian Dold <florian@dold.me>
Date:   Wed, 24 Jun 2026 01:28:15 +0200

harness: advanced token playground test

Diffstat:
Mpackages/taler-harness/src/index.ts | 105+++++++++++++++----------------------------------------------------------------
Apackages/taler-harness/src/playground.ts | 267+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 2--
3 files changed, 286 insertions(+), 88 deletions(-)

diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -18,7 +18,6 @@ * Imports. */ import { - AbsoluteTime, AccessToken, AmountJson, AmountString, @@ -33,9 +32,6 @@ import { Logger, LoginTokenScope, MerchantAuthMethod, - OrderInputType, - OrderOutputType, - OrderVersion, PaytoString, TalerBankConversionHttpClient, TalerCoreBankHttpClient, @@ -110,6 +106,10 @@ import { AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT } from "./integrationtests/test- import { AML_PROGRAM_NEXT_MEASURE_FORM } from "./integrationtests/test-kyc-two-forms.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { lintExchangeDeployment, lintExchangeUrl } from "./lint.js"; +import { + runPlaygroundAdvancedTokens1, + runPlaygroundBlog, +} from "./playground.js"; const logger = new Logger("taler-harness:index.ts"); @@ -1787,89 +1787,22 @@ playgroundCli .requiredOption("merchantPw", ["--merchant-pw"], clk.STRING) .maybeOption("payDelay", ["--pay-delay"], clk.STRING) .action(async (args) => { - const merchantClient = new TalerMerchantInstanceHttpClient( - args.exp.merchantUrl, - ); - const config = succeedOrThrow(await merchantClient.getConfig()); - const tok = succeedOrThrow( - await merchantClient.createAccessToken( - merchantClient.guessInstanceName(), - args.exp.merchantPw, - { - scope: LoginTokenScope.All, - duration: Duration.toTalerProtocolDuration( - Duration.fromSpec({ hours: 1 }), - ), - }, - ), - ); - - const subscriptionAmount = `${config.currency}:5` as AmountString; - const articleAmount = `${config.currency}:1` as AmountString; - - const payDeadline = - args.exp.payDelay == null - ? undefined - : AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - Duration.fromPrettyString(args.exp.payDelay), - ), - ); - - const createResp = succeedOrThrow( - await merchantClient.createOrder(tok.access_token, { - order: { - pay_deadline: payDeadline, - version: OrderVersion.V1, - summary: "Test Blog Payment", - choices: [ - { - amount: articleAmount, - description: "Buy an individual article", - }, - { - amount: subscriptionAmount as AmountString, - description: "Buy one month of unlimited access", - outputs: [ - { - type: OrderOutputType.Token, - token_family_slug: "blog_abo_en", - }, - ], - }, - { - amount: Amounts.stringify( - Amounts.zeroOfCurrency(config.currency), - ), - inputs: [ - { - type: OrderInputType.Token, - token_family_slug: "blog_abo_en", - }, - ], - outputs: [ - { - type: OrderOutputType.Token, - token_family_slug: "blog_abo_en", - }, - ], - }, - ], - }, - }), - ); - - const st = succeedOrThrow( - await merchantClient.getOrderDetails( - tok.access_token, - createResp.order_id, - ), - ); + await runPlaygroundBlog({ + merchantPw: args.exp.merchantPw, + merchantUrl: args.exp.merchantUrl, + payDelay: args.exp.payDelay, + }); + }); - if (st.order_status === "unpaid") { - console.log(st.taler_pay_uri); - } +playgroundCli + .subcommand("exp", "advanced-tokens-1", {}) + .requiredOption("merchantUrl", ["--merchant-url"], clk.STRING) + .requiredOption("merchantPw", ["--merchant-pw"], clk.STRING) + .action(async (args) => { + await runPlaygroundAdvancedTokens1({ + merchantPw: args.exp.merchantPw, + merchantUrl: args.exp.merchantUrl, + }); }); export const helpersCli = talerHarnessCli.subcommand( diff --git a/packages/taler-harness/src/playground.ts b/packages/taler-harness/src/playground.ts @@ -0,0 +1,267 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + AbsoluteTime, + AmountString, + Amounts, + Duration, + LoginTokenScope, + OrderInputType, + OrderOutputType, + OrderVersion, + TalerMerchantInstanceHttpClient, + TokenFamilyKind, + encodeCrock, + getRandomBytes, + succeedOrThrow, +} from "@gnu-taler/taler-util"; + +export interface PlaygroundBlogArgs { + merchantUrl: string; + merchantPw: string; + payDelay?: string; +} + +export async function runPlaygroundBlog( + args: PlaygroundBlogArgs, +): Promise<void> { + const merchantClient = new TalerMerchantInstanceHttpClient(args.merchantUrl); + const config = succeedOrThrow(await merchantClient.getConfig()); + const tok = succeedOrThrow( + await merchantClient.createAccessToken( + merchantClient.guessInstanceName(), + args.merchantPw, + { + scope: LoginTokenScope.All, + duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ hours: 1 }), + ), + }, + ), + ); + + const subscriptionAmount = `${config.currency}:5` as AmountString; + const articleAmount = `${config.currency}:1` as AmountString; + + const payDeadline = + args.payDelay == null + ? undefined + : AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromPrettyString(args.payDelay), + ), + ); + + const createResp = succeedOrThrow( + await merchantClient.createOrder(tok.access_token, { + order: { + pay_deadline: payDeadline, + version: OrderVersion.V1, + summary: "Test Blog Payment", + choices: [ + { + amount: articleAmount, + description: "Buy an individual article", + }, + { + amount: subscriptionAmount as AmountString, + description: "Buy one month of unlimited access", + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: "blog_abo_en", + }, + ], + }, + { + amount: Amounts.stringify(Amounts.zeroOfCurrency(config.currency)), + inputs: [ + { + type: OrderInputType.Token, + token_family_slug: "blog_abo_en", + }, + ], + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: "blog_abo_en", + }, + ], + }, + ], + }, + }), + ); + + const st = succeedOrThrow( + await merchantClient.getOrderDetails(tok.access_token, createResp.order_id), + ); + + if (st.order_status === "unpaid") { + console.log(st.taler_pay_uri); + } +} + +export interface PlaygroundAdvancedTokensArgs { + merchantUrl: string; + merchantPw: string; + payDelay?: string; +} + +export async function runPlaygroundAdvancedTokens1( + args: PlaygroundAdvancedTokensArgs, +): Promise<void> { + const merchantClient = new TalerMerchantInstanceHttpClient(args.merchantUrl); + const config = succeedOrThrow(await merchantClient.getConfig()); + const tok = succeedOrThrow( + await merchantClient.createAccessToken( + merchantClient.guessInstanceName(), + args.merchantPw, + { + scope: LoginTokenScope.All, + duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ hours: 1 }), + ), + }, + ), + ); + + const rnd = encodeCrock(getRandomBytes(16)); + + const slugDiscount1 = `slugdiscount1-${rnd}`; + const slugSubscription1 = `slugsubscription1-${rnd}`; + + await merchantClient.createTokenFamily(tok.access_token, { + name: "discount1", + slug: slugDiscount1, + description: "My Discount 1 (valid two minutes)", + duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 2 }), + ), + valid_before: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ), + ), + kind: TokenFamilyKind.Discount, + validity_granularity: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchantClient.createTokenFamily(tok.access_token, { + name: "subscription1", + slug: slugSubscription1, + description: "My Subscription 1 (valid one minute)", + duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 2 }), + ), + valid_before: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ), + ), + kind: TokenFamilyKind.Subscription, + validity_granularity: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + const payDeadline = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 10 }), + ), + ); + + const subscriptionAmount = `${config.currency}:5` as AmountString; + const articleAmount = `${config.currency}:1` as AmountString; + const articleDiscountedAmount = `${config.currency}:0.5` as AmountString; + + const createResp = succeedOrThrow( + await merchantClient.createOrder(tok.access_token, { + order: { + pay_deadline: payDeadline, + version: OrderVersion.V1, + summary: "Test Payment", + choices: [ + { + amount: articleAmount, + description: "Buy an individual article", + }, + { + amount: subscriptionAmount as AmountString, + description: "Article and discount", + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: slugDiscount1, + }, + ], + }, + { + amount: subscriptionAmount as AmountString, + description: "Article and subscription", + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: slugSubscription1, + }, + ], + }, + { + amount: articleDiscountedAmount, + inputs: [ + { + type: OrderInputType.Token, + token_family_slug: slugDiscount1, + }, + ], + outputs: [], + }, + { + amount: articleAmount, + description: "Article via subscription", + inputs: [ + { + type: OrderInputType.Token, + token_family_slug: slugSubscription1, + }, + ], + outputs: [ + { + type: OrderOutputType.Token, + token_family_slug: slugSubscription1, + }, + ], + }, + ], + }, + }), + ); + + const st = succeedOrThrow( + await merchantClient.getOrderDetails(tok.access_token, createResp.order_id), + ); + + if (st.order_status === "unpaid") { + console.log(st.taler_pay_uri); + } +} diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -961,8 +961,6 @@ async function processDownloadProposal( return TaskRunResult.finished(); } - const transactionId = ctx.transactionId; - const orderClaimUrl = new URL( `orders/${proposal.orderId}/claim`, proposal.merchantBaseUrl,