merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 0f7de2887b1acb3d6cce8332c95f5fbf2ed39074
parent 813f441dc777206c613d0c03ec2c835649e554e4
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Wed, 28 Jan 2026 23:01:36 +0900

add test for # 10612

Diffstat:
Msrc/backend/taler-merchant-httpd_private-get-orders-ID.c | 2+-
Msrc/testing/test_merchant_order_creation.sh | 8+++++---
Asrc/testing/test_merchant_tokenfamilies.sh | 420+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 426 insertions(+), 4 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -859,7 +859,7 @@ phase_check_paid (struct GetOrderRequestContext *gorc) /** * Check if the @a reply satisfies the long-poll not_etag - * constraint. If so, return it as a reponse for @a gorc, + * constraint. If so, return it as a response for @a gorc, * otherwise suspend and wait for a change. * * @param[in,out] gorc request to handle diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh @@ -254,9 +254,10 @@ VALID_BEFORE="{\"t_s\": $(date +%s -d "+30 days")}" # 30 days from now DURATION="{\"d_us\": $(expr 30 \* 24 \* 60 \* 60 \* 1000000)}" # 30 days STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ -d "{\"kind\": \"discount\", \"slug\":\"test-discount\", \"name\": \"Test discount\", \"description\": \"Less money $$\", \"description_i18n\": {\"en\": \"Less money $$\", \"es\": \"Menos dinero $$\"}, \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $DURATION}" \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected '204 OK' response. Got instead $STATUS" fi echo "Ok" @@ -270,10 +271,11 @@ VALID_BEFORE="{\"t_s\": $(date +%s -d "+30 days")}" # 30 days from now DURATION="{\"d_us\": $(expr 30 \* 24 \* 60 \* 60 \* 1000000)}" # 30 days STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ -d "{\"kind\": \"subscription\", \"slug\":\"test-subscription\", \"name\": \"Test subscription\", \"description\": \"Money per month\", \"description_i18n\": {\"en\": \"Money $$$ per month\", \"es\": \"Dinero $$$ al mes\"}, \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $DURATION}" \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then - exit_fail "Expected '204 OK' response. Got instead $STATUS" + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected '204 OK' response. Got instead $STATUS" fi echo "Ok" diff --git a/src/testing/test_merchant_tokenfamilies.sh b/src/testing/test_merchant_tokenfamilies.sh @@ -0,0 +1,420 @@ +#!/bin/bash +# This file is part of TALER +# Copyright (C) 2026 Taler Systems SA +# +# 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. +# +# 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 TALER; see the file COPYING. If not, see +# <http://www.gnu.org/licenses/> +# + +# Cleanup to run whenever we exit +function my_cleanup() +{ + for n in $(jobs -p) + do + kill "$n" 2> /dev/null || true + done + wait + if [ -n "${LAST_RESPONSE+x}" ] + then + rm -f "${LAST_RESPONSE}" + fi +} + + +# Replace with 0 for nexus... +USE_FAKEBANK=1 +if [ 1 = "$USE_FAKEBANK" ] +then + ACCOUNT="exchange-account-2" + BANK_FLAGS="-f -d x-taler-bank -u $ACCOUNT" + BANK_URL="http://localhost:8082/" +else + ACCOUNT="exchange-account-1" + BANK_FLAGS="-ns -d iban -u $ACCOUNT" + BANK_URL="http://localhost:18082/" + echo -n "Testing for libeufin-bank" + libeufin-bank --help >/dev/null </dev/null || exit_skip " MISSING" + echo " FOUND" +fi + +. setup.sh + +setup -c test_template.conf -m +CONF="test_template.conf.edited" +LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX) +WALLET_DB=$(mktemp -p "${TMPDIR:-/tmp}" test_wallet.json-XXXXXX) +EXCHANGE_URL="http://localhost:8081"/ + +echo -n "Configuring 'admin' instance ..." >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + http://localhost:9966/management/instances \ + -d '{"auth":{"method":"token","password":"new_pw"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ + -w "%{http_code}" \ + -s \ + -o /dev/null) + +if [ "$STATUS" != "204" ] +then + exit_fail "Expected 204, instance created. got: $STATUS" >&2 +fi + +BASIC_AUTH=$(echo -n admin:new_pw | base64) + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H "Authorization: Basic $BASIC_AUTH" \ + http://localhost:9966/private/token \ + -d '{"scope":"spa"}' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +BEARER_TOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") + +echo " OK" >&2 + +echo -n "Setting up bank account..." >&2 + +STATUS=$(curl -H "Content-Type: application/json" \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + http://localhost:9966/private/accounts \ + -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/43?receiver-name=user43"}' \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200 OK. Got: $STATUS" +fi + + +# CREATE A DISCOUNT TOKEN FAMILY +# +echo -n "Creating discount token family..." +VALID_AFTER="{\"t_s\": $(date +%s)}" # now +VALID_BEFORE="{\"t_s\": $(date +%s -d "+300 days")}" # 300 days from now +DURATION="{\"d_us\": $(expr 3 \* 60 \* 1000000)}" # 3 minutes +GRANULARITY="{\"d_us\": $(expr 60 \* 1000000)}" # 1 minute +STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -d "{\"kind\": \"discount\", \"slug\":\"test-discount\", \"name\": \"Test discount\", \"description\": \"Less money $$\", \"description_i18n\": {\"en\": \"Less money $$\", \"es\": \"Menos dinero $$\"}, \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $GRANULARITY}" \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") +if [ "$STATUS" != "204" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected '204 OK' response. Got instead $STATUS" +fi +echo "Ok" + +# +# CREATE A SUBSCRIPTION TOKEN FAMILY +# +echo -n "Creating subscription token family..." +VALID_AFTER="{\"t_s\": $(date +%s)}" # now +VALID_BEFORE="{\"t_s\": $(date +%s -d "+30 days")}" # 300 days from now +DURATION="{\"d_us\": $(expr 3 \* 60 \* 1000000)}" # 3 minutes +GRANULARITY="{\"d_us\": $(expr 60 \* 1000000)}" # 1 minute +STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -d "{\"kind\": \"subscription\", \"slug\":\"test-subscription\", \"name\": \"Test subscription\", \"description\": \"Money per month\", \"description_i18n\": {\"en\": \"Money $$$ per month\", \"es\": \"Dinero $$$ al mes\"}, \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $GRANULARITY}" \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") +if [ "$STATUS" != "204" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected '204 OK' response. Got instead $STATUS" +fi +echo "Ok" + +echo "Time traveling merchant 10 minutes into the future ..." + +# Kill merchant +kill -TERM "$SETUP_PID" +wait +unset SETUP_PID + +setup -c test_template.conf \ + -ef \ + -u "exchange-account-2" \ + -r "merchant-exchange-default" + +taler-merchant-exchangekeyupdate \ + -c "${CONF}" \ + -L DEBUG \ + -t \ + 2> taler-merchant-exchangekeyupdate2.log +# 600000000 = 10 minutes +taler-merchant-httpd \ + -c "${CONF}" \ + -L DEBUG \ + --timetravel=600000000 \ + 2> taler-merchant-httpd2.log & +# Install cleanup handler (except for kill -9) +trap my_cleanup EXIT + +echo -n "Waiting for the merchant..." >&2 +# Wait for merchant to be available (usually the slowest) +for n in $(seq 1 50) +do + echo -n "." >&2 + sleep 0.1 + OK=0 + # merchant + wget --waitretry=0 \ + --timeout=1 \ + http://localhost:9966/ \ + -o /dev/null \ + -O /dev/null \ + >/dev/null || continue + OK=1 + break +done + +if [ "x$OK" != "x1" ] +then + exit_fail "Failed to (re)start merchant backend" +fi + +echo " OK" >&2 + +echo -n "Preparing wallet with coins ..." +rm -f "$WALLET_DB" +taler-wallet-cli \ + --no-throttle \ + --wallet-db="$WALLET_DB" \ + api \ + --expect-success 'withdrawTestBalance' \ + "$(jq -n ' + { + amount: "TESTKUDOS:99", + corebankApiBaseUrl: $BANK_URL, + exchangeBaseUrl: $EXCHANGE_URL + }' \ + --arg BANK_URL "${BANK_URL}" \ + --arg EXCHANGE_URL "$EXCHANGE_URL" + )" 2>wallet-withdraw-1.err >wallet-withdraw-1.out +echo -n "." +# FIXME-MS: add logic to have nexus check immediately here. +# sleep 10 +echo -n "." +# NOTE: once libeufin can do long-polling, we should +# be able to reduce the delay here and run wirewatch +# always in the background via setup +taler-exchange-wirewatch \ + -a "$ACCOUNT" \ + -L "INFO" \ + -c "$CONF" \ + -t &> taler-exchange-wirewatch.out +echo -n "." +taler-wallet-cli \ + --wallet-db="$WALLET_DB" \ + run-until-done \ + 2>wallet-withdraw-finish-1.err \ + >wallet-withdraw-finish-1.out +echo " OK" + +CURRENCY_COUNT=$(taler-wallet-cli --wallet-db="$WALLET_DB" balance | jq '.balances|length') +if [ "$CURRENCY_COUNT" = "0" ] +then + exit_fail "Expected least one currency, withdrawal failed. check log." +fi + + + +RANDOM_IMG='data:image/png;base64,abcdefg' + +echo -n "Creating subscribeable order..." +STATUS=$(curl 'http://localhost:9966/private/orders' \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -d '{"create_token":true,"refund_delay":{"d_us":0},"order":{"version":1,"summary":"Expensive purchase","products":[{"description":"Expensive subscription","quantity":1,"unit":"pieces","price":"TESTKUDOS:10"}],"choices":[{"amount":"TESTKUDOS:10","inputs":[],"outputs":[{"type":"token","token_family_slug":"test-subscription","count":1}]},{"amount":"TESTKUDOS:0","inputs":[{"type":"token","token_family_slug":"test-subscription","count":1}],"outputs":[{"type":"token","token_family_slug":"test-subscription","count":1}]}]}}' \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, order created. got: $STATUS" +fi +echo "OK" + +echo -n "Fetching payment URL " + +ORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") +TOKEN=$(jq -r .token < "$LAST_RESPONSE") + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, getting order info. got: $STATUS" +fi + +PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE") + +echo "OK" + + +NOW=$(date +%s) + +echo -n "Pay for subscription (choice 1) at ${PAY_URL} ..." +echo "1" \ + | taler-wallet-cli \ + --no-throttle \ + --timetravel=600000000 \ + --wallet-db="$WALLET_DB" \ + handle-uri "${PAY_URL}" \ + -y 2> wallet-pay1.err > wallet-pay1.log +taler-wallet-cli \ + --no-throttle \ + --timetravel=600000000 \ + --wallet-db="$WALLET_DB" \ + run-until-done 2> wallet-finish-pay1.err > wallet-finish-pay1.log +NOW2=$(date +%s) +echo " OK (took $(( NOW2 - NOW )) secs )" + +echo -n "Checking order was paid ..." + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, after pay. got: $STATUS" +fi + +ORDER_STATUS=$(jq -r .order_status < "$LAST_RESPONSE") + +if [ "$ORDER_STATUS" != "paid" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Order status should be 'paid'. got: $ORDER_STATUS" +fi +echo " OK" + +# FIXME: test also paying with the subscription token... + + + +echo -n "Creating discountable order..." +STATUS=$(curl 'http://localhost:9966/private/orders' \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -d '{"create_token":true,"refund_delay":{"d_us":0},"order":{"version":1,"summary":"Expensive subscription","products":[{"description":"Simple purchase","quantity":1,"unit":"pieces","price":"TESTKUDOS:10"}],"choices":[{"amount":"TESTKUDOS:10","inputs":[],"outputs":[{"type":"token","token_family_slug":"test-discount","count":1}]},{"amount":"TESTKUDOS:9","inputs":[{"type":"token","token_family_slug":"test-discount","count":1}],"outputs":[]}]}}' \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, order created. got: $STATUS" +fi +echo "OK" + +ORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") +TOKEN=$(jq -r .token < "$LAST_RESPONSE") + +echo -n "Fetching payment URL " + +ORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") +TOKEN=$(jq -r .token < "$LAST_RESPONSE") + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, getting order info. got: $STATUS" +fi + +PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE") + +echo "OK" + + +NOW=$(date +%s) + +echo -n "Pay without discount (choice 0) at ${PAY_URL} ..." +echo "0" \ + | taler-wallet-cli \ + --no-throttle \ + --timetravel=600000000 \ + --wallet-db="$WALLET_DB" \ + handle-uri "${PAY_URL}" \ + -y 2> wallet-pay1.err > wallet-pay1.log +taler-wallet-cli \ + --no-throttle \ + --timetravel=600000000 \ + --wallet-db="$WALLET_DB" \ + run-until-done 2> wallet-finish-pay1.err > wallet-finish-pay1.log +NOW2=$(date +%s) +echo " OK (took $(( NOW2 - NOW )) secs )" + +echo -n "Checking order was paid ..." + +STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \ + -X POST \ + -H "Authorization: Bearer $BEARER_TOKEN" \ + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, after pay. got: $STATUS" +fi + +ORDER_STATUS=$(jq -r .order_status < "$LAST_RESPONSE") + +if [ "$ORDER_STATUS" != "paid" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Order status should be 'paid'. got: $ORDER_STATUS" +fi +echo " OK" + +# FIXME: test also paying with the discount token... + +echo "Test PASSED" + +exit 0