merchant

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

commit 6cf98569a969d37d19c8c483fa25b0a88811d37a
parent 9fa415e5c533a1002a44ebb782b1741234cb7f6d
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sun, 11 Oct 2026 15:55:43 +0000

adding public get for units + version bump

Diffstat:
MREADME | 6++++++
Msrc/backend/meson.build | 1+
Msrc/backend/taler-merchant-httpd_dispatcher.c | 7+++++++
Msrc/backend/taler-merchant-httpd_get-config.c | 2+-
Msrc/backend/taler-merchant-httpd_get-private-units.c | 62++++----------------------------------------------------------
Msrc/backend/taler-merchant-httpd_get-private-units.h | 8++++++++
Asrc/backend/taler-merchant-httpd_get-units.c | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-units.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/merchant_api_get-config.c | 4++--
Msrc/testing/meson.build | 1+
Asrc/testing/test_merchant_units.sh | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 387 insertions(+), 61 deletions(-)

diff --git a/README b/README @@ -12,6 +12,12 @@ A general-purpose merchant back-office single-page (HTML/JavaScript) application is also included (alas, the source code for it is in another Git repository). +Wallets can retrieve all active unit definitions without authentication using +GET /units relative to the merchant instance base URL (for example, +/instances/shop/units). The response contains a "units" array with labels, +translations, and precision, including built-in units. The existing private +unit endpoints remain authenticated and include inactive units. + This merchant backend must be combined with a merchant frontend, such as an e-commerce application. You can find a WooCommerce plugin, a snack machine integration (taler-mdb) and various demonstrator front-ends at diff --git a/src/backend/meson.build b/src/backend/meson.build @@ -76,6 +76,7 @@ taler_merchant_httpd_SOURCES = [ 'taler-merchant-httpd_get-config.c', 'taler-merchant-httpd_get-exchanges.c', 'taler-merchant-httpd_get-templates-TEMPLATE_ID.c', + 'taler-merchant-httpd_get-units.c', 'taler-merchant-httpd_helper.c', 'taler-merchant-httpd_token-keys.c', 'taler-merchant-httpd_fountains.c', diff --git a/src/backend/taler-merchant-httpd_dispatcher.c b/src/backend/taler-merchant-httpd_dispatcher.c @@ -26,6 +26,7 @@ #include "taler-merchant-httpd_get-sessions-SESSION_ID.h" #include "taler-merchant-httpd_get-products-IMAGE_HASH-image.h" #include "taler-merchant-httpd_get-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_get-units.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_delete-private-accounts-H_WIRE.h" #include "taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h" @@ -1150,6 +1151,12 @@ determine_handler_group (const char **urlp, } }; static struct TMH_RequestHandler public_handlers[] = { + /* GET /units: active unit definitions for wallets. */ + { + .url_prefix = "/units", + .method = MHD_HTTP_METHOD_GET, + .handler = &TMH_get_units + }, { /* for "admin" instance, it does not even have to exist before we give the WebUI */ diff --git a/src/backend/taler-merchant-httpd_get-config.c b/src/backend/taler-merchant-httpd_get-config.c @@ -44,7 +44,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_get_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "43:0:31" +#define MERCHANT_PROTOCOL_VERSION "44:0:32" /** diff --git a/src/backend/taler-merchant-httpd_get-private-units.c b/src/backend/taler-merchant-httpd_get-private-units.c @@ -20,42 +20,7 @@ */ #include "platform.h" #include "taler-merchant-httpd_get-private-units.h" -#include "merchant-database/iterate_units.h" - - -static void -add_unit (void *cls, - uint64_t unit_serial, - const struct TALER_MERCHANTDB_UnitDetails *ud) -{ - json_t *ua = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - ua, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("unit_serial", - unit_serial), - GNUNET_JSON_pack_string ("unit", - ud->unit), - GNUNET_JSON_pack_string ("unit_name_long", - ud->unit_name_long), - GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", - (json_t *) ud->unit_name_long_i18n), - GNUNET_JSON_pack_string ("unit_name_short", - ud->unit_name_short), - GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", - (json_t *) ud->unit_name_short_i18n), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - ud->unit_allow_fraction), - GNUNET_JSON_pack_uint64 ("unit_precision_level", - ud->unit_precision_level), - GNUNET_JSON_pack_bool ("unit_active", - ud->unit_active), - GNUNET_JSON_pack_bool ("unit_builtin", - ud->unit_builtin)))); -} +#include "taler-merchant-httpd_get-units.h" enum MHD_Result @@ -63,29 +28,10 @@ TMH_private_get_units (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { - json_t *ua; - enum GNUNET_DB_QueryStatus qs; - (void) rh; - ua = json_array (); - GNUNET_assert (NULL != ua); - qs = TALER_MERCHANTDB_iterate_units (TMH_db, - hc->instance->settings.id, - &add_unit, - ua); - if (0 > qs) - { - GNUNET_break (0); - json_decref (ua); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("units", - ua)); + return TMH_reply_units (connection, + hc, + false); } diff --git a/src/backend/taler-merchant-httpd_get-private-units.h b/src/backend/taler-merchant-httpd_get-private-units.h @@ -24,6 +24,14 @@ #include "taler-merchant-httpd.h" +/** + * Handle a GET "/private/units" request, including inactive units. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ enum MHD_Result TMH_private_get_units (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, diff --git a/src/backend/taler-merchant-httpd_get-units.c b/src/backend/taler-merchant-httpd_get-units.c @@ -0,0 +1,134 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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/> +*/ +/** + * @file src/backend/taler-merchant-httpd_get-units.c + * @brief implement GET /units and shared unit list response + * @author Bohdan Potuzhnyi + */ +#include "platform.h" +#include "taler-merchant-httpd_get-units.h" +#include "merchant-database/iterate_units.h" + + +/** + * Context for building a unit list response. + */ +struct UnitListContext +{ + /** + * Unit definitions to return. + */ + json_t *units; + + /** + * Omit inactive units from the public catalogue. + */ + bool active_only; +}; + + +/** + * Add unit details to the response array. + * + * @param cls a `struct UnitListContext *` + * @param unit_serial serial number of the unit + * @param ud unit details + */ +static void +add_unit (void *cls, + uint64_t unit_serial, + const struct TALER_MERCHANTDB_UnitDetails *ud) +{ + const struct UnitListContext *ulc = cls; + json_t *ua = ulc->units; + + if (ulc->active_only && + (! ud->unit_active)) + return; + + GNUNET_assert ( + 0 == + json_array_append_new ( + ua, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("unit_serial", + unit_serial), + GNUNET_JSON_pack_string ("unit", + ud->unit), + GNUNET_JSON_pack_string ("unit_name_long", + ud->unit_name_long), + GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", + (json_t *) ud->unit_name_long_i18n), + GNUNET_JSON_pack_string ("unit_name_short", + ud->unit_name_short), + GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", + (json_t *) ud->unit_name_short_i18n), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + ud->unit_allow_fraction), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + ud->unit_precision_level), + GNUNET_JSON_pack_bool ("unit_active", + ud->unit_active), + GNUNET_JSON_pack_bool ("unit_builtin", + ud->unit_builtin)))); +} + + +enum MHD_Result +TMH_reply_units (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + bool active_only) +{ + struct UnitListContext ulc = { + .units = json_array (), + .active_only = active_only + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != ulc.units); + qs = TALER_MERCHANTDB_iterate_units (TMH_db, + hc->instance->settings.id, + &add_unit, + &ulc); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ulc.units); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("units", + ulc.units)); +} + + +enum MHD_Result +TMH_get_units (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + (void) rh; + return TMH_reply_units (connection, + hc, + true); +} + + +/* end of taler-merchant-httpd_get-units.c */ diff --git a/src/backend/taler-merchant-httpd_get-units.h b/src/backend/taler-merchant-httpd_get-units.h @@ -0,0 +1,54 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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/> +*/ +/** + * @file src/backend/taler-merchant-httpd_get-units.h + * @brief implement GET /units and shared unit list response + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_GET_UNITS_H +#define TALER_MERCHANT_HTTPD_GET_UNITS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/units" request, returning active unit definitions. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +enum MHD_Result +TMH_get_units (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Build the unit list response shared by the public and private endpoints. + * + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @param active_only true to omit inactive units + * @return MHD result code + */ +enum MHD_Result +TMH_reply_units (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + bool active_only); + +#endif diff --git a/src/lib/merchant_api_get-config.c b/src/lib/merchant_api_get-config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 43 +#define MERCHANT_PROTOCOL_CURRENT 44 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 19 +#define MERCHANT_PROTOCOL_AGE 20 /** * How many exchanges do we allow at most per merchant? diff --git a/src/testing/meson.build b/src/testing/meson.build @@ -18,6 +18,7 @@ check_SCRIPTS = [ 'test_merchant_statistics', 'test_merchant_templates', 'test_merchant_transfer_tracking', + 'test_merchant_units', 'test-merchant-walletharness', 'test_merchant_wirewatch', ] diff --git a/src/testing/test_merchant_units.sh b/src/testing/test_merchant_units.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env 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/> +# + +set -eu + +. setup.sh + +function units_cleanup() +{ + exit_cleanup + if [ -n "${LAST_RESPONSE:-}" ] + then + rm -f "$LAST_RESPONSE" + fi +} + +trap units_cleanup EXIT + +# Wallets get one public list of active units; private endpoints require a token. +function check_unit_access() +{ + local BASE_URL="$1" + local ACCESS_TOKEN="$2" + local UNIT_BODY + local STATUS + local EXPECTED_UNITS + local ENDPOINT + local METHOD + + UNIT_BODY=$(jq -n --arg unit_name "$BASE_URL" '{ + unit: "public-test-unit", unit_name_long: $unit_name, + unit_name_short: "ptu", unit_name_long_i18n: {de: "Testeinheit"}, + unit_name_short_i18n: {de: "te"}, unit_allow_fraction: true, + unit_precision_level: 2, unit_active: true + }') + STATUS=$(curl -s -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$BASE_URL/private/units" -d "$UNIT_BODY" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "204" ] + then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected authenticated unit creation to return 204. Got: $STATUS" + fi + # Keep an inactive definition in the private catalogue only. + STATUS=$(curl -s -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$BASE_URL/private/units" \ + -d "$(printf '%s' "$UNIT_BODY" | jq '.unit = "inactive-test-unit" | .unit_active = false')" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "204" ] + then + exit_fail "Expected inactive unit creation to return 204. Got: $STATUS" + fi + STATUS=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$BASE_URL/private/units" -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "200" ] + then + exit_fail "Expected private catalogue to return 200. Got: $STATUS" + fi + jq -e 'any(.units[]; .unit == "inactive-test-unit" and .unit_active == false)' \ + < "$LAST_RESPONSE" > /dev/null || exit_fail "Private catalogue omits inactive unit" + EXPECTED_UNITS=$(jq -c '[.units[] | select(.unit_active)]' < "$LAST_RESPONSE") + + STATUS=$(curl -s "$BASE_URL/units" -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "200" ] + then + exit_fail "Expected public catalogue to return 200. Got: $STATUS" + fi + jq -e --argjson expected "$EXPECTED_UNITS" --argjson unit "$UNIT_BODY" ' + .units == $expected and + any(.units[]; .unit == $unit.unit and + .unit_name_long == $unit.unit_name_long and + .unit_name_short == $unit.unit_name_short and + .unit_name_long_i18n == $unit.unit_name_long_i18n and + .unit_name_short_i18n == $unit.unit_name_short_i18n and + .unit_allow_fraction == $unit.unit_allow_fraction and + .unit_precision_level == $unit.unit_precision_level) and + any(.units[]; .unit_builtin == true) + ' < "$LAST_RESPONSE" > /dev/null || exit_fail "Public catalogue has incorrect unit metadata" + for ENDPOINT in units units/public-test-unit + do + STATUS=$(curl -s "$BASE_URL/private/$ENDPOINT" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "401" ] + then + exit_fail "Expected unauthenticated private unit read to return 401. Got: $STATUS" + fi + done + # There is no public endpoint for individual units. + STATUS=$(curl -s "$BASE_URL/units/public-test-unit" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "404" ] + then + exit_fail "Expected public per-unit lookup to return 404. Got: $STATUS" + fi + for METHOD in POST PATCH DELETE + do + STATUS=$(curl -s -X "$METHOD" -H "Content-Type: application/json" \ + "$BASE_URL/units" -d "$UNIT_BODY" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "405" ] + then + exit_fail "Expected $METHOD on public catalogue to return 405. Got: $STATUS" + fi + ENDPOINT=units/public-test-unit + if [ "$METHOD" = POST ] + then + ENDPOINT=units + fi + STATUS=$(curl -s -X "$METHOD" -H "Content-Type: application/json" \ + "$BASE_URL/private/$ENDPOINT" -d "$UNIT_BODY" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "401" ] + then + exit_fail "Expected unauthenticated $METHOD unit request to return 401. Got: $STATUS" + fi + done +} + +# The catalogue tests only need the merchant and its database. +setup -c test_template.conf -m +LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_units_response-XXXXXX) +AUTH_HEADERS=() + +for INSTANCE in admin second +do + echo -n "Checking unit catalogue for $INSTANCE ..." + INSTANCE_BODY=$(jq -n --arg id "$INSTANCE" '{ + auth: {method: "token", password: "unit_test_password"}, + id: $id, name: $id, address: {}, jurisdiction: {}, use_stefan: true, + default_wire_transfer_delay: {d_us: 3600000000}, + default_pay_delay: {d_us: 3600000000} + }') + STATUS=$(curl -s -H "Content-Type: application/json" "${AUTH_HEADERS[@]}" \ + http://localhost:9966/management/instances -d "$INSTANCE_BODY" \ + -w "%{http_code}" -o "$LAST_RESPONSE") + if [ "$STATUS" != "204" ] + then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected instance creation to return 204. Got: $STATUS" + fi + TOKEN=$(merchant_login "$INSTANCE" unit_test_password) + if [ "$INSTANCE" = admin ] + then + AUTH_HEADERS=(-H "Authorization: Bearer $TOKEN") + fi + INSTANCE_URL=$(merchant_instance_url "$INSTANCE") + check_unit_access "${INSTANCE_URL%/}" "$TOKEN" + echo " OK" +done + +echo "TEST PASSED"