commit 9d77dc861f88b0e47caaddd21fd1023f7d499912
parent 85a355cfdd7fcab7cac2d16a142633575d584173
Author: Florian Dold <florian@dold.me>
Date: Wed, 3 Jun 2026 19:13:49 +0200
harness: new playground command for testing short expiration payments
Diffstat:
3 files changed, 122 insertions(+), 2 deletions(-)
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
@@ -18,6 +18,7 @@
* Imports.
*/
import {
+ AbsoluteTime,
AccessToken,
AmountJson,
AmountString,
@@ -32,6 +33,9 @@ import {
Logger,
LoginTokenScope,
MerchantAuthMethod,
+ OrderInputType,
+ OrderOutputType,
+ OrderVersion,
PaytoString,
TalerBankConversionHttpClient,
TalerCoreBankHttpClient,
@@ -1767,6 +1771,105 @@ talerHarnessCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
console.log("check passed!");
});
+export const playgroundCli = talerHarnessCli.subcommand(
+ "playground",
+ "playground",
+ {
+ help: "Helpers for testing various scenarios.",
+ },
+);
+
+playgroundCli
+ .subcommand("exp", "blog", {})
+ .requiredOption("merchantUrl", ["--merchant-url"], clk.STRING)
+ .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,
+ ),
+ );
+
+ if (st.order_status === "unpaid") {
+ console.log(st.taler_pay_uri);
+ }
+ });
+
export const helpersCli = talerHarnessCli.subcommand(
"helperProgram",
"helper-program",
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -216,6 +216,22 @@ export class TalerMerchantInstanceHttpClient {
}
/**
+ * Derive the instance name from the base URL.
+ */
+ guessInstanceName(): string {
+ const inst = "/instances/";
+ const instIndex = this.baseUrl.lastIndexOf(inst);
+ if (instIndex < 0) {
+ return "admin";
+ } else {
+ return this.baseUrl.substring(
+ instIndex + inst.length,
+ this.baseUrl.length - 1,
+ );
+ }
+ }
+
+ /**
* https://docs.taler.net/core/api-merchant.html#get--config
*/
async getConfig() {
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -3088,8 +3088,7 @@ export interface CheckPaymentUnpaidResponse {
// to show the order QR code / trigger the wallet.
order_status_url: string;
- // We do we NOT return the contract terms here because they may not
- // exist in case the wallet did not yet claim them.
+ proto_contract_terms?: any;
}
export interface RefundDetails {
// Reason given for the refund.
@@ -4841,6 +4840,7 @@ export const codecForLoginTokenSuccessResponse =
.property("access_token", codecForAccessToken())
.property("expiration", codecForTimestamp)
.property("refreshable", codecForBoolean())
+ .deprecatedProperty("token")
.build("TalerMerchantApi.LoginTokenSuccessResponse");
export const codecForExchangeKycTimeout = (): Codec<ExchangeKycTimeout> =>
@@ -5172,6 +5172,7 @@ export const codecForCheckPaymentUnpaidResponse =
.property("already_paid_order_id", codecOptional(codecForString()))
.property("already_paid_fulfillment_url", codecOptional(codecForString()))
.property("order_status_url", codecForString())
+ .property("proto_contract_terms", codecOptional(codecForAny()))
.build("TalerMerchantApi.CheckPaymentUnpaidResponse");
export const codecForCheckPaymentClaimedResponse =