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:
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"