commit 8f19589b66258f79f54a4a65d4bb221f9a56a6ea
parent 1892db4afacb02dd6935367adb4c20f0dee0ca94
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Sat, 18 Oct 2025 23:09:43 +0200
[inventory] Small fix #0010506 (dd72) ¯\_(ツ)_/¯
Diffstat:
79 files changed, 8378 insertions(+), 279 deletions(-)
diff --git a/contrib/ci/jobs/0-codespell/dictionary.txt b/contrib/ci/jobs/0-codespell/dictionary.txt
@@ -44,3 +44,5 @@ updateing
wan
wih
decose
+ue
+bu
+\ No newline at end of file
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am
@@ -90,6 +90,8 @@ taler_merchant_httpd_SOURCES = \
taler-merchant-httpd_private-delete-account-ID.h \
taler-merchant-httpd_private-delete-categories-ID.c \
taler-merchant-httpd_private-delete-categories-ID.h \
+ taler-merchant-httpd_private-delete-units-ID.c \
+ taler-merchant-httpd_private-delete-units-ID.h \
taler-merchant-httpd_private-delete-instances-ID.c \
taler-merchant-httpd_private-delete-instances-ID.h \
taler-merchant-httpd_private-delete-instances-ID-token.c \
@@ -114,8 +116,12 @@ taler_merchant_httpd_SOURCES = \
taler-merchant-httpd_private-get-accounts-ID.h \
taler-merchant-httpd_private-get-categories.c \
taler-merchant-httpd_private-get-categories.h \
+ taler-merchant-httpd_private-get-units.c \
+ taler-merchant-httpd_private-get-units.h \
taler-merchant-httpd_private-get-categories-ID.c \
taler-merchant-httpd_private-get-categories-ID.h \
+ taler-merchant-httpd_private-get-units-ID.c \
+ taler-merchant-httpd_private-get-units-ID.h \
taler-merchant-httpd_private-get-instances.c \
taler-merchant-httpd_private-get-instances.h \
taler-merchant-httpd_private-get-instances-ID.c \
@@ -158,6 +164,8 @@ taler_merchant_httpd_SOURCES = \
taler-merchant-httpd_private-patch-accounts-ID.h \
taler-merchant-httpd_private-patch-categories-ID.c \
taler-merchant-httpd_private-patch-categories-ID.h \
+ taler-merchant-httpd_private-patch-units-ID.c \
+ taler-merchant-httpd_private-patch-units-ID.h \
taler-merchant-httpd_private-patch-instances-ID.c \
taler-merchant-httpd_private-patch-instances-ID.h \
taler-merchant-httpd_private-patch-orders-ID-forget.c \
@@ -176,6 +184,8 @@ taler_merchant_httpd_SOURCES = \
taler-merchant-httpd_private-post-account.h \
taler-merchant-httpd_private-post-categories.c \
taler-merchant-httpd_private-post-categories.h \
+ taler-merchant-httpd_private-post-units.c \
+ taler-merchant-httpd_private-post-units.h \
taler-merchant-httpd_private-post-instances.c \
taler-merchant-httpd_private-post-instances.h \
taler-merchant-httpd_private-post-instances-ID-auth.c \
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c
@@ -39,6 +39,7 @@
#include "taler-merchant-httpd_mfa.h"
#include "taler-merchant-httpd_private-delete-account-ID.h"
#include "taler-merchant-httpd_private-delete-categories-ID.h"
+#include "taler-merchant-httpd_private-delete-units-ID.h"
#include "taler-merchant-httpd_private-delete-instances-ID.h"
#include "taler-merchant-httpd_private-delete-instances-ID-token.h"
#include "taler-merchant-httpd_private-delete-products-ID.h"
@@ -52,6 +53,8 @@
#include "taler-merchant-httpd_private-get-accounts-ID.h"
#include "taler-merchant-httpd_private-get-categories.h"
#include "taler-merchant-httpd_private-get-categories-ID.h"
+#include "taler-merchant-httpd_private-get-units.h"
+#include "taler-merchant-httpd_private-get-units-ID.h"
#include "taler-merchant-httpd_private-get-incoming.h"
#include "taler-merchant-httpd_private-get-instances.h"
#include "taler-merchant-httpd_private-get-instances-ID.h"
@@ -75,6 +78,7 @@
#include "taler-merchant-httpd_private-get-webhooks-ID.h"
#include "taler-merchant-httpd_private-patch-accounts-ID.h"
#include "taler-merchant-httpd_private-patch-categories-ID.h"
+#include "taler-merchant-httpd_private-patch-units-ID.h"
#include "taler-merchant-httpd_private-patch-instances-ID.h"
#include "taler-merchant-httpd_private-patch-orders-ID-forget.h"
#include "taler-merchant-httpd_private-patch-otp-devices-ID.h"
@@ -84,6 +88,7 @@
#include "taler-merchant-httpd_private-patch-webhooks-ID.h"
#include "taler-merchant-httpd_private-post-account.h"
#include "taler-merchant-httpd_private-post-categories.h"
+#include "taler-merchant-httpd_private-post-units.h"
#include "taler-merchant-httpd_private-post-instances.h"
#include "taler-merchant-httpd_private-post-instances-ID-auth.h"
#include "taler-merchant-httpd_private-post-instances-ID-token.h"
@@ -1455,6 +1460,47 @@ url_handler (void *cls,
/* allow category data of up to 8 kb, that should be plenty */
.max_upload = 1024 * 8
},
+ /* GET /units: */
+ {
+ .url_prefix = "/units",
+ .method = MHD_HTTP_METHOD_GET,
+ .handler = &TMH_private_get_units
+ },
+ /* POST /units: */
+ {
+ .url_prefix = "/units",
+ .method = MHD_HTTP_METHOD_POST,
+ .permission = "units-write",
+ .handler = &TMH_private_post_units,
+ .max_upload = 1024 * 8
+ },
+ /* GET /units/$UNIT: */
+ {
+ .url_prefix = "/units/",
+ .method = MHD_HTTP_METHOD_GET,
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_get_units_ID
+ },
+ /* DELETE /units/$UNIT: */
+ {
+ .url_prefix = "/units/",
+ .method = MHD_HTTP_METHOD_DELETE,
+ .permission = "units-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_delete_units_ID
+ },
+ /* PATCH /units/$UNIT: */
+ {
+ .url_prefix = "/units/",
+ .method = MHD_HTTP_METHOD_PATCH,
+ .permission = "units-write",
+ .have_id_segment = true,
+ .allow_deleted_instance = true,
+ .handler = &TMH_private_patch_units_ID,
+ .max_upload = 1024 * 8
+ },
/* GET /products: */
{
.url_prefix = "/products",
diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c
@@ -21,7 +21,6 @@
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_db_lib.h>
-#include <taler/taler_util.h>
#include <taler/taler_json_lib.h>
#include "taler-merchant-httpd_helper.h"
#include <taler/taler_templating_lib.h>
@@ -29,6 +28,366 @@
enum GNUNET_GenericReturnValue
+TMH_parse_fractional_string (const char *value,
+ int64_t *integer_part,
+ uint32_t *fractional_part)
+{
+ const char *ptr;
+ uint64_t integer = 0;
+ uint32_t frac = 0;
+ unsigned int digits = 0;
+
+ GNUNET_assert (NULL != integer_part);
+ GNUNET_assert (NULL != fractional_part);
+
+ if (NULL == value)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ ptr = value;
+ if ('\0' == *ptr)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if ('-' == *ptr)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if (! isdigit ((unsigned char) *ptr))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ while (isdigit ((unsigned char) *ptr))
+ {
+ unsigned int digit = (unsigned int) (*ptr - '0');
+
+ if (integer > (UINT64_MAX - digit) / 10)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ integer = integer * 10 + digit;
+ ptr++;
+ }
+ if ('.' == *ptr)
+ {
+ ptr++;
+ if ('\0' == *ptr)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ while (isdigit ((unsigned char) *ptr))
+ {
+ unsigned int digit = (unsigned int) (*ptr - '0');
+
+ if (digits >= MERCHANT_UNIT_FRAC_MAX_DIGITS)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ frac = (uint32_t) (frac * 10 + digit);
+ digits++;
+ ptr++;
+ }
+ while (digits < MERCHANT_UNIT_FRAC_MAX_DIGITS)
+ {
+ frac *= 10;
+ digits++;
+ }
+ }
+ if ('\0' != *ptr)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if (integer > (uint64_t) INT64_MAX)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ *integer_part = integer;
+ *fractional_part = frac;
+ return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
+TMH_process_quantity_inputs (enum TMH_ValueKind kind,
+ bool allow_fractional,
+ bool int_missing,
+ int64_t int_raw,
+ bool str_missing,
+ const char *str_raw,
+ uint64_t *int_out,
+ uint32_t *frac_out,
+ const char **error_param)
+{
+ static char errbuf[128];
+ int64_t parsed_int = 0;
+ uint32_t parsed_frac = 0;
+ const char *int_field = (TMH_VK_STOCK == kind)
+ ? "total_stock"
+ : "quantity";
+ const char *str_field = (TMH_VK_STOCK == kind)
+ ? "unit_total_stock"
+ : "unit_quantity";
+
+ *error_param = NULL;
+
+ if (int_missing && str_missing)
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "missing %s and %s",
+ int_field,
+ str_field);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+
+ if (! str_missing)
+ {
+ if ( (TMH_VK_STOCK == kind) &&
+ (0 == strcmp ("-1",
+ str_raw)) )
+ {
+ parsed_int = -1;
+ parsed_frac = 0;
+ }
+ else
+ {
+ if (GNUNET_OK !=
+ TMH_parse_fractional_string (str_raw,
+ &parsed_int,
+ &parsed_frac))
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "malformed %s",
+ str_field);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+
+ if ( (! int_missing) && (! str_missing) )
+ {
+ if ( (parsed_int != int_raw) || (0 != parsed_frac) )
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "%s/%s mismatch",
+ int_field,
+ str_field);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ }
+ else if (int_missing)
+ {
+ int_raw = parsed_int;
+ }
+
+ if ( (TMH_VK_STOCK == kind) && (-1 == int_raw) )
+ {
+ if ( (! str_missing) && (0 != parsed_frac) )
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "fractional part forbidden with %s='-1'",
+ str_field);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ *int_out = INT64_MAX;
+ *frac_out = INT32_MAX;
+ return GNUNET_OK;
+ }
+
+ if (int_raw < 0)
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "%s must be non-negative",
+ int_field);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+
+ if (! allow_fractional)
+ {
+ if ( (! str_missing) && (0 != parsed_frac) )
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "fractional part not allowed for %s",
+ str_field);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ parsed_frac = 0;
+ }
+ else if (! str_missing)
+ {
+ if (parsed_frac >= MERCHANT_UNIT_FRAC_BASE)
+ {
+ GNUNET_snprintf (errbuf,
+ sizeof (errbuf),
+ "%s fractional part exceeds base %u",
+ str_field,
+ MERCHANT_UNIT_FRAC_BASE);
+ *error_param = errbuf;
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ }
+
+ *int_out = (uint64_t) int_raw;
+ *frac_out = parsed_frac;
+ return GNUNET_OK;
+}
+
+
+void
+TMH_format_fractional_string (enum TMH_ValueKind kind,
+ uint64_t integer,
+ uint32_t fractional,
+ size_t buffer_length,
+ char buffer[static buffer_length])
+{
+ GNUNET_assert (0 < buffer_length);
+
+ if ( (TMH_VK_STOCK == kind) &&
+ (INT64_MAX == (int64_t) integer) &&
+ (INT32_MAX == (int32_t) fractional) )
+ {
+ GNUNET_snprintf (buffer,
+ buffer_length,
+ "-1");
+ return;
+ }
+
+ GNUNET_assert ( (TMH_VK_QUANTITY != kind) ||
+ ((INT64_MAX != (int64_t) integer) &&
+ (INT32_MAX != (int32_t) fractional)) );
+ GNUNET_assert (fractional < MERCHANT_UNIT_FRAC_BASE);
+
+ if (0 == fractional)
+ {
+ GNUNET_snprintf (buffer,
+ buffer_length,
+ "%lu",
+ integer);
+ return;
+ }
+ {
+ char frac_buf[MERCHANT_UNIT_FRAC_MAX_DIGITS + 1];
+ size_t idx;
+
+ GNUNET_snprintf (frac_buf,
+ sizeof (frac_buf),
+ "%0*u",
+ MERCHANT_UNIT_FRAC_MAX_DIGITS,
+ (unsigned int) fractional);
+ for (idx = strlen (frac_buf); idx > 0; idx--)
+ {
+ if ('0' != frac_buf[idx - 1])
+ break;
+ frac_buf[idx - 1] = '\0';
+ }
+ GNUNET_snprintf (buffer,
+ buffer_length,
+ "%lu.%s",
+ integer,
+ frac_buf);
+ }
+}
+
+
+void
+TMH_quantity_defaults_from_unit (const struct TMH_MerchantInstance *mi,
+ const char *unit,
+ bool *allow_fractional,
+ uint32_t *precision_level)
+{
+ GNUNET_assert (NULL != allow_fractional);
+ GNUNET_assert (NULL != precision_level);
+ if (GNUNET_OK !=
+ TMH_unit_defaults_for_instance (mi,
+ unit,
+ allow_fractional,
+ precision_level))
+ {
+ *allow_fractional = false;
+ *precision_level = 0;
+ }
+}
+
+
+enum GNUNET_GenericReturnValue
+TMH_unit_defaults_for_instance (const struct TMH_MerchantInstance *mi,
+ const char *unit,
+ bool *allow_fractional,
+ uint32_t *precision_level)
+{
+ struct TALER_MERCHANTDB_UnitDetails ud = { 0 };
+ enum GNUNET_DB_QueryStatus qs;
+ bool allow = false;
+ uint32_t precision = 0;
+
+ GNUNET_assert (NULL != allow_fractional);
+ GNUNET_assert (NULL != precision_level);
+
+ qs = TMH_db->select_unit (TMH_db->cls,
+ mi->settings.id,
+ unit,
+ &ud);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ TALER_MERCHANTDB_unit_details_free (&ud);
+ return GNUNET_SYSERR;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ allow = ud.unit_allow_fraction;
+ precision = ud.unit_precision_level;
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ break;
+ default:
+ GNUNET_break (0);
+ TALER_MERCHANTDB_unit_details_free (&ud);
+ return GNUNET_SYSERR;
+ }
+ TALER_MERCHANTDB_unit_details_free (&ud);
+
+ /* This is definitely not supposed to happen
+ combination of allow -> false, and precision > 0
+ yet let's fix it */
+ if (! allow)
+ {
+ GNUNET_break (0);
+ precision = 0;
+ }
+ *allow_fractional = allow;
+ *precision_level = precision;
+ return GNUNET_OK;
+}
+
+
+enum GNUNET_GenericReturnValue
TMH_cmp_wire_account (
const json_t *account,
const struct TMH_WireMethod *wm)
@@ -307,6 +666,9 @@ TMH_products_array_valid (const json_t *products)
const char *product_id = NULL;
const char *description;
uint64_t quantity = 0;
+ bool quantity_missing = true;
+ const char *unit_quantity = NULL;
+ bool unit_quantity_missing = true;
const char *unit = NULL;
struct TALER_Amount price = { .value = 0 };
const char *image_data_url = NULL;
@@ -322,7 +684,11 @@ TMH_products_array_valid (const json_t *products)
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_uint64 ("quantity",
&quantity),
- NULL),
+ &quantity_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_quantity",
+ &unit_quantity),
+ &unit_quantity_missing),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("unit",
&unit),
@@ -360,6 +726,14 @@ TMH_products_array_valid (const json_t *products)
ename);
return false;
}
+ if (quantity_missing)
+ {
+ if (unit_quantity_missing)
+ {
+ GNUNET_break_op (0);
+ valid = false;
+ }
+ }
if ( (NULL != image_data_url) &&
(! TMH_image_data_url_valid (image_data_url)) )
{
diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h
@@ -23,6 +23,8 @@
#ifndef TALER_EXCHANGE_HTTPD_HELPER_H
#define TALER_EXCHANGE_HTTPD_HELPER_H
+#define TMH_MAX_FRACTIONAL_PRECISION_LEVEL 6
+
#include "taler-merchant-httpd.h"
@@ -73,6 +75,115 @@ TMH_products_array_valid (const json_t *products);
/**
+ * Parse decimal quantity expressed as string for request handling.
+ *
+ * @param value string to parse
+ * @param[out] integer_part result integer component
+ * @param[out] fractional_part result fractional component (0..MERCHANT_UNIT_FRAC_BASE-1)
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure
+ */
+enum GNUNET_GenericReturnValue
+TMH_parse_fractional_string (const char *value,
+ int64_t *integer_part,
+ uint32_t *fractional_part);
+
+/**
+ * Kind of fixed-decimal value the helpers operate on.
+ * Primarily distinguishes how special sentinel values (such as "-1"
+ * meaning infinity for stock) must be encoded.
+ */
+enum TMH_ValueKind
+{
+ TMH_VK_QUANTITY, /* -1 is illegal */
+ TMH_VK_STOCK /* -1 means "infinity" */
+};
+
+/**
+ * Extract a fixed-decimal number that may be supplied either
+ * - as pure integer (e.g. "total_stock"), or
+ * - as decimal text (e.g. "unit_total_stock").
+ *
+ * Rules:
+ * - If both forms are missing -> error.
+ * - If both are present -> they must match and the decimal must have no fraction.
+ * - For kind == TMH_VK_STOCK the integer value -1 represents infinity.
+ *
+ * @param kind See #TMH_ValueKind
+ * @param allow_fractional False: any fractional part is rejected
+ * @param int_missing True if client omitted the integer field
+ * @param int_raw Raw integer (undefined if @a int_missing is true)
+ * @param str_missing True if client omitted the string field
+ * @param str_raw Raw UTF-8 string (undefined if @a str_missing is true)
+ * @param[out] int_out Canonicalised integer part
+ * @param[out] frac_out Canonicalised fractional part
+ * @param[out] error_param Set to offending field name on failure
+ * @param int_field Integer field name (for error reporting)
+ * @param str_field String field name (for error reporting)
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise
+ */
+enum GNUNET_GenericReturnValue
+TMH_process_quantity_inputs (enum TMH_ValueKind kind,
+ bool allow_fractional,
+ bool int_missing,
+ int64_t int_raw,
+ bool str_missing,
+ const char *str_raw,
+ uint64_t *int_out,
+ uint32_t *frac_out,
+ const char **error_param);
+
+/**
+ * Lookup the defaults for @a unit within @a mi and fall back to sane
+ * values (disallow fractional quantities, zero precision) if no data
+ * is available.
+ *
+ * @param mi merchant instance whose defaults should be consulted (must not be NULL)
+ * @param unit textual unit name (must not be NULL or empty)
+ * @param allow_fractional updated with whether fractional quantities are allowed (must not be NULL)
+ * @param precision_level updated with the supported precision (must not be NULL)
+ */
+void
+TMH_quantity_defaults_from_unit (const struct TMH_MerchantInstance *mi,
+ const char *unit,
+ bool *allow_fractional,
+ uint32_t *precision_level);
+
+/**
+ * Query the database for precision defaults tied to @a unit within
+ * @a mi. Returns #GNUNET_OK even if no unit information exists, in
+ * which case the out-parameters remain at their implicit defaults.
+ *
+ * @param mi merchant instance whose unit table is inspected (must not be NULL)
+ * @param unit textual unit name (must not be NULL or empty)
+ * @param allow_fractional updated with whether fractional quantities are allowed (must not be NULL)
+ * @param precision_level updated with the supported precision (must not be NULL)
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on database failure
+ */
+enum GNUNET_GenericReturnValue
+TMH_unit_defaults_for_instance (const struct TMH_MerchantInstance *mi,
+ const char *unit,
+ bool *allow_fractional,
+ uint32_t *precision_level);
+
+/**
+ * Format a fixed-decimal pair into canonical string representation.
+ * Recognises INT64_MAX / INT32_MAX as the "-1" sentinel used for
+ * infinite stock if @a kind equals #TMH_VK_STOCK.
+ *
+ * @param kind specifies whether sentinel values are permitted
+ * @param integer integer portion
+ * @param fractional fractional portion (0..MERCHANT_UNIT_FRAC_BASE-1 or sentinel)
+ * @param buffer output buffer
+ * @param buffer_length length of @a buffer
+ */
+void
+TMH_format_fractional_string (enum TMH_ValueKind kind,
+ uint64_t integer,
+ uint32_t fractional,
+ size_t buffer_length,
+ char buffer[static buffer_length]);
+
+/**
* Check if @a image_data_url is a valid image
* data URL. Does not validate the actual payload,
* only the syntax and that it properly claims to
@@ -238,13 +349,13 @@ TMH_make_order_status_url (struct MHD_Connection *con,
* @param hr a `TALER_EXCHANGE_HttpResponse`
*/
#define TMH_pack_exchange_reply(hr) \
- GNUNET_JSON_pack_uint64 ("exchange_code", (hr)->ec), \
- GNUNET_JSON_pack_uint64 ("exchange_http_status", (hr)->http_status), \
- GNUNET_JSON_pack_uint64 ("exchange_ec", (hr)->ec), /* LEGACY */ \
- GNUNET_JSON_pack_uint64 ("exchange_hc", (hr)->http_status), /* LEGACY */ \
- GNUNET_JSON_pack_allow_null ( \
- GNUNET_JSON_pack_object_incref ("exchange_reply", (json_t *) (hr)-> \
- reply))
+ GNUNET_JSON_pack_uint64 ("exchange_code", (hr)->ec), \
+ GNUNET_JSON_pack_uint64 ("exchange_http_status", (hr)->http_status), \
+ GNUNET_JSON_pack_uint64 ("exchange_ec", (hr)->ec), /* LEGACY */ \
+ GNUNET_JSON_pack_uint64 ("exchange_hc", (hr)->http_status), /* LEGACY */ \
+ GNUNET_JSON_pack_allow_null ( \
+ GNUNET_JSON_pack_object_incref ("exchange_reply", (json_t *) (hr)-> \
+ reply))
/**
diff --git a/src/backend/taler-merchant-httpd_private-delete-units-ID.c b/src/backend/taler-merchant-httpd_private-delete-units-ID.c
@@ -0,0 +1,81 @@
+/*
+ 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 taler-merchant-httpd_private-delete-units-ID.c
+ * @brief implement DELETE /private/units/$UNIT
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_private-delete-units-ID.h"
+
+
+MHD_RESULT
+TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ bool no_instance = false;
+ bool no_unit = false;
+ bool builtin_conflict = false;
+
+ GNUNET_assert (NULL != hc->infix);
+ qs = TMH_db->delete_unit (TMH_db->cls,
+ hc->instance->settings.id,
+ hc->infix,
+ &no_instance,
+ &no_unit,
+ &builtin_conflict);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ "delete_unit");
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ default:
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "delete_unit");
+ }
+ if (no_instance)
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
+ hc->instance->settings.id);
+ if (no_unit)
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN,
+ hc->infix);
+ if (builtin_conflict)
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN,
+ hc->infix);
+ return TALER_MHD_reply_static (connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+}
+
+
+/* end of taler-merchant-httpd_private-delete-units-ID.c */
diff --git a/src/backend/taler-merchant-httpd_private-delete-units-ID.h b/src/backend/taler-merchant-httpd_private-delete-units-ID.h
@@ -0,0 +1,33 @@
+/*
+ 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 taler-merchant-httpd_private-delete-units-ID.h
+ * @brief implement DELETE /private/units/$UNIT
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_UNITS_ID_H
+#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_UNITS_ID_H
+
+#include "taler-merchant-httpd.h"
+
+
+MHD_RESULT
+TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+/* end of taler-merchant-httpd_private-delete-units-ID.h */
+#endif
diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c
@@ -21,6 +21,7 @@
#include "platform.h"
#include "taler-merchant-httpd_private-get-pos.h"
#include <taler/taler_json_lib.h>
+#include "taler-merchant-httpd_helper.h"
/**
* Closure for add_product().
@@ -96,6 +97,8 @@ add_product (void *cls,
struct Context *ctx = cls;
json_t *pa = ctx->pa;
json_t *cata;
+ int64_t total_stock_api;
+ char unit_total_stock_buf[64];
cata = json_array ();
GNUNET_assert (NULL != cata);
@@ -112,6 +115,16 @@ add_product (void *cls,
cata,
json_integer (0)));
}
+ if (INT64_MAX == pd->total_stock)
+ total_stock_api = -1;
+ else
+ total_stock_api = (int64_t) pd->total_stock;
+ TMH_format_fractional_string (TMH_VK_STOCK,
+ pd->total_stock,
+ pd->total_stock_frac,
+ sizeof (unit_total_stock_buf),
+ unit_total_stock_buf);
+
GNUNET_assert (
0 ==
json_array_append_new (
@@ -127,6 +140,9 @@ add_product (void *cls,
pd->unit),
TALER_JSON_pack_amount ("price",
&pd->price),
+ TALER_JSON_pack_amount_array ("unit_price",
+ pd->price_array_length,
+ pd->price_array),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("image",
pd->image)),
@@ -135,12 +151,14 @@ add_product (void *cls,
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_array_incref ("taxes",
(json_t *) pd->taxes)),
- (INT64_MAX == pd->total_stock)
- ? GNUNET_JSON_pack_int64 ("total_stock",
- pd->total_stock)
- : GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_string ("total_stock",
- NULL)),
+ GNUNET_JSON_pack_int64 ("total_stock",
+ total_stock_api),
+ GNUNET_JSON_pack_string ("unit_total_stock",
+ unit_total_stock_buf),
+ GNUNET_JSON_pack_bool ("unit_allow_fraction",
+ pd->allow_fractional_quantity),
+ GNUNET_JSON_pack_uint64 ("unit_precision_level",
+ pd->fractional_precision_level),
GNUNET_JSON_pack_uint64 ("minimum_age",
pd->minimum_age),
GNUNET_JSON_pack_uint64 ("product_serial",
diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c
@@ -21,7 +21,8 @@
#include "platform.h"
#include "taler-merchant-httpd_private-get-products-ID.h"
#include <taler/taler_json_lib.h>
-
+#include "taler-merchant-httpd_helper.h"
+#include <taler/taler_util.h>
/**
* Handle a GET "/products/$ID" request.
@@ -78,6 +79,19 @@ TMH_private_get_products_ID (
{
MHD_RESULT ret;
+ int64_t total_stock_api;
+ char unit_total_stock_buf[64];
+
+ if (INT64_MAX == pd.total_stock)
+ total_stock_api = -1;
+ else
+ total_stock_api = (int64_t) pd.total_stock;
+
+ TMH_format_fractional_string (TMH_VK_STOCK,
+ pd.total_stock,
+ pd.total_stock_frac,
+ sizeof (unit_total_stock_buf),
+ unit_total_stock_buf);
ret = TALER_MHD_REPLY_JSON_PACK (
connection,
@@ -101,9 +115,16 @@ TMH_private_get_products_ID (
GNUNET_JSON_pack_array_incref ("taxes",
pd.taxes)),
GNUNET_JSON_pack_int64 ("total_stock",
- (INT64_MAX == pd.total_stock)
- ? -1LL
- : pd.total_stock),
+ total_stock_api),
+ GNUNET_JSON_pack_string ("unit_total_stock",
+ unit_total_stock_buf),
+ GNUNET_JSON_pack_bool ("unit_allow_fraction",
+ pd.allow_fractional_quantity),
+ GNUNET_JSON_pack_uint64 ("unit_precision_level",
+ pd.fractional_precision_level),
+ TALER_JSON_pack_amount_array ("unit_price",
+ pd.price_array_length,
+ pd.price_array),
GNUNET_JSON_pack_uint64 ("total_sold",
pd.total_sold),
GNUNET_JSON_pack_uint64 ("total_lost",
diff --git a/src/backend/taler-merchant-httpd_private-get-units-ID.c b/src/backend/taler-merchant-httpd_private-get-units-ID.c
@@ -0,0 +1,88 @@
+/*
+ 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 taler-merchant-httpd_private-get-units-ID.c
+ * @brief implement GET /private/units/$UNIT
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_private-get-units-ID.h"
+
+
+MHD_RESULT
+TMH_private_get_units_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ struct TALER_MERCHANTDB_UnitDetails ud = { 0 };
+ enum GNUNET_DB_QueryStatus qs;
+ MHD_RESULT ret;
+
+ (void) rh;
+ GNUNET_assert (NULL != hc->infix);
+ qs = TMH_db->select_unit (TMH_db->cls,
+ hc->instance->settings.id,
+ hc->infix,
+ &ud);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN,
+ hc->infix);
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "unit");
+ }
+
+ ret = TALER_MHD_REPLY_JSON_PACK (connection,
+ MHD_HTTP_OK,
+ GNUNET_JSON_pack_uint64 ("unit_serial",
+ ud.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",
+ 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",
+ 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));
+ TALER_MERCHANTDB_unit_details_free (&ud);
+ return ret;
+}
+
+
+/* end of taler-merchant-httpd_private-get-units-ID.c */
diff --git a/src/backend/taler-merchant-httpd_private-get-units-ID.h b/src/backend/taler-merchant-httpd_private-get-units-ID.h
@@ -0,0 +1,33 @@
+/*
+ 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 taler-merchant-httpd_private-get-units-ID.h
+ * @brief implement GET /private/units/$UNIT
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_ID_H
+#define TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_ID_H
+
+#include "taler-merchant-httpd.h"
+
+
+MHD_RESULT
+TMH_private_get_units_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+/* end of taler-merchant-httpd_private-get-units-ID.h */
+#endif
diff --git a/src/backend/taler-merchant-httpd_private-get-units.c b/src/backend/taler-merchant-httpd_private-get-units.c
@@ -0,0 +1,91 @@
+/*
+ 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 taler-merchant-httpd_private-get-units.c
+ * @brief implement GET /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_private-get-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))));
+}
+
+
+MHD_RESULT
+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 = TMH_db->lookup_units (TMH_db->cls,
+ 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));
+}
+
+
+/* end of taler-merchant-httpd_private-get-units.c */
diff --git a/src/backend/taler-merchant-httpd_private-get-units.h b/src/backend/taler-merchant-httpd_private-get-units.h
@@ -0,0 +1,33 @@
+/*
+ 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 taler-merchant-httpd_private-get-units.h
+ * @brief implement GET /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_H
+#define TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_H
+
+#include "taler-merchant-httpd.h"
+
+
+MHD_RESULT
+TMH_private_get_units (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+/* end of taler-merchant-httpd_private-get-units.h */
+#endif
diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c
@@ -47,6 +47,15 @@ TMH_private_patch_products_ID (
struct TALER_MERCHANTDB_ProductDetails pd = {0};
const json_t *categories = NULL;
int64_t total_stock;
+ const char *unit_total_stock = NULL;
+ bool unit_total_stock_missing;
+ bool total_stock_missing;
+ bool price_missing;
+ bool unit_price_missing;
+ bool unit_allow_fraction;
+ bool unit_allow_fraction_missing;
+ uint32_t unit_precision_level;
+ bool unit_precision_missing;
enum GNUNET_DB_QueryStatus qs;
struct GNUNET_JSON_Specification spec[] = {
/* new in protocol v20, thus optional for backwards-compatibility */
@@ -62,8 +71,10 @@ TMH_private_patch_products_ID (
NULL),
GNUNET_JSON_spec_string ("unit",
(const char **) &pd.unit),
- TALER_JSON_spec_amount_any ("price",
- &pd.price),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_any ("price",
+ &pd.price),
+ &price_missing),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("image",
(const char **) &pd.image),
@@ -76,8 +87,27 @@ TMH_private_patch_products_ID (
GNUNET_JSON_spec_array_const ("categories",
&categories),
NULL),
- GNUNET_JSON_spec_int64 ("total_stock",
- &total_stock),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_total_stock",
+ &unit_total_stock),
+ &unit_total_stock_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_int64 ("total_stock",
+ &total_stock),
+ &total_stock_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("unit_allow_fraction",
+ &unit_allow_fraction),
+ &unit_allow_fraction_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 ("unit_precision_level",
+ &unit_precision_level),
+ &unit_precision_missing),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_any_array ("unit_price",
+ &pd.price_array_length,
+ &pd.price_array),
+ &unit_price_missing),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_uint64 ("total_lost",
&pd.total_lost),
@@ -124,19 +154,100 @@ TMH_private_patch_products_ID (
if (NULL == pd.product_name)
pd.product_name = pd.description;
}
- if (total_stock < -1)
+ if (! unit_price_missing)
{
- GNUNET_break_op (0);
- ret = TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "total_stock");
- goto cleanup;
+ if (! price_missing)
+ {
+ if (0 != TALER_amount_cmp (&pd.price,
+ &pd.price_array[0]))
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "price,unit_price mismatch");
+ goto cleanup;
+ }
+ }
+ else
+ {
+ pd.price = pd.price_array[0];
+ price_missing = false;
+ }
}
- if (-1 == total_stock)
- pd.total_stock = INT64_MAX;
else
- pd.total_stock = (uint64_t) total_stock;
+ {
+ if (price_missing)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "price missing");
+ goto cleanup;
+ }
+ pd.price_array = GNUNET_new_array (1,
+ struct TALER_Amount);
+ pd.price_array[0] = pd.price;
+ pd.price_array_length = 1;
+ }
+ if (! unit_precision_missing)
+ {
+ if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_precision_level");
+ goto cleanup;
+ }
+ }
+ {
+ bool default_allow_fractional;
+ uint32_t default_precision_level;
+
+ if (GNUNET_OK !=
+ TMH_unit_defaults_for_instance (mi,
+ pd.unit,
+ &default_allow_fractional,
+ &default_precision_level))
+ {
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "unit defaults");
+ goto cleanup;
+ }
+ if (unit_allow_fraction_missing)
+ unit_allow_fraction = default_allow_fractional;
+ if (unit_precision_missing)
+ unit_precision_level = default_precision_level;
+
+ if (! unit_allow_fraction)
+ unit_precision_level = 0;
+ pd.fractional_precision_level = unit_precision_level;
+ }
+ {
+ const char *eparam;
+ if (GNUNET_OK !=
+ TMH_process_quantity_inputs (TMH_VK_STOCK,
+ unit_allow_fraction,
+ total_stock_missing,
+ total_stock,
+ unit_total_stock_missing,
+ unit_total_stock,
+ &pd.total_stock,
+ &pd.total_stock_frac,
+ &eparam))
+ {
+ ret = TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ eparam);
+ goto cleanup;
+ }
+ pd.allow_fractional_quantity = unit_allow_fraction;
+ }
if (NULL == pd.address)
pd.address = json_object ();
@@ -325,6 +436,7 @@ TMH_private_patch_products_ID (
0);
cleanup:
GNUNET_free (cats);
+ GNUNET_free (pd.price_array);
GNUNET_JSON_parse_free (spec);
return ret;
}
diff --git a/src/backend/taler-merchant-httpd_private-patch-units-ID.c b/src/backend/taler-merchant-httpd_private-patch-units-ID.c
@@ -0,0 +1,237 @@
+/*
+ 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 taler-merchant-httpd_private-patch-units-ID.c
+ * @brief implement PATCH /private/units/$UNIT
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_private-patch-units-ID.h"
+#include "taler-merchant-httpd_helper.h"
+#include <taler/taler_json_lib.h>
+
+#define TMH_MAX_UNIT_PRECISION_LEVEL 6
+
+
+MHD_RESULT
+TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ struct TMH_MerchantInstance *mi = hc->instance;
+ const char *unit_id = hc->infix;
+ struct TALER_MERCHANTDB_UnitDetails nud = { 0 };
+ bool unit_allow_fraction_missing = true;
+ bool unit_precision_missing = true;
+ bool unit_active_missing = true;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_name_long",
+ (const char **) &nud.unit_name_long),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_json ("unit_name_long_i18n",
+ &nud.unit_name_long_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_name_short",
+ (const char **) &nud.unit_name_short),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_json ("unit_name_short_i18n",
+ &nud.unit_name_short_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("unit_allow_fraction",
+ &nud.unit_allow_fraction),
+ &unit_allow_fraction_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 ("unit_precision_level",
+ &nud.unit_precision_level),
+ &unit_precision_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("unit_active",
+ &nud.unit_active),
+ &unit_active_missing),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue res;
+ const bool *unit_allow_fraction_ptr = NULL;
+ const uint32_t *unit_precision_ptr = NULL;
+ const bool *unit_active_ptr = NULL;
+ enum GNUNET_DB_QueryStatus qs;
+ bool no_instance = false;
+ bool no_unit = false;
+ bool builtin_conflict = false;
+ MHD_RESULT ret = MHD_YES;
+
+ (void) rh;
+ GNUNET_assert (NULL != mi);
+ GNUNET_assert (NULL != unit_id);
+
+ res = TALER_MHD_parse_json_data (connection,
+ hc->request_body,
+ spec);
+ if (GNUNET_OK != res)
+ return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
+
+ if (NULL == nud.unit_name_long &&
+ NULL == nud.unit_name_long_i18n &&
+ NULL == nud.unit_name_short &&
+ NULL == nud.unit_name_short_i18n &&
+ unit_allow_fraction_missing &&
+ unit_precision_missing &&
+ unit_active_missing)
+ {
+ ret = TALER_MHD_reply_static (connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+ goto cleanup;
+ }
+
+ if (! unit_precision_missing)
+ {
+ if (nud.unit_precision_level > TMH_MAX_UNIT_PRECISION_LEVEL)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_precision_level");
+ goto cleanup;
+ }
+ unit_precision_ptr = &nud.unit_precision_level;
+ }
+
+ if (! unit_allow_fraction_missing)
+ {
+ unit_allow_fraction_ptr = &nud.unit_allow_fraction;
+ if (! nud.unit_allow_fraction)
+ {
+ nud.unit_precision_level = 0;
+ unit_precision_missing = false;
+ unit_precision_ptr = &nud.unit_precision_level;
+ }
+ }
+
+ if (! unit_active_missing)
+ unit_active_ptr = &nud.unit_active;
+
+ if (NULL != nud.unit_name_long_i18n)
+ {
+ if (! TALER_JSON_check_i18n (nud.unit_name_long_i18n))
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_name_long_i18n");
+ goto cleanup;
+ }
+ }
+
+ if (NULL != nud.unit_name_short_i18n)
+ {
+ if (! TALER_JSON_check_i18n (nud.unit_name_short_i18n))
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_name_short_i18n");
+ goto cleanup;
+ }
+ }
+
+ qs = TMH_db->update_unit (TMH_db->cls,
+ mi->settings.id,
+ unit_id,
+ nud.unit_name_long,
+ nud.unit_name_long_i18n,
+ nud.unit_name_short,
+ nud.unit_name_short_i18n,
+ unit_allow_fraction_ptr,
+ unit_precision_ptr,
+ unit_active_ptr,
+ &no_instance,
+ &no_unit,
+ &builtin_conflict);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ "update_unit");
+ goto cleanup;
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ default:
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "update_unit");
+ goto cleanup;
+ }
+
+ if (no_instance)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
+ mi->settings.id);
+ goto cleanup;
+ }
+ if (no_unit)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN,
+ unit_id);
+ goto cleanup;
+ }
+ if (builtin_conflict)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN,
+ unit_id);
+ goto cleanup;
+ }
+
+ ret = TALER_MHD_reply_static (connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+
+cleanup:
+ if (NULL != nud.unit_name_long_i18n)
+ {
+ json_decref (nud.unit_name_long_i18n);
+ nud.unit_name_long_i18n = NULL;
+ }
+ if (NULL != nud.unit_name_short_i18n)
+ {
+ json_decref (nud.unit_name_short_i18n);
+ nud.unit_name_short_i18n = NULL;
+ }
+ GNUNET_JSON_parse_free (spec);
+ return ret;
+}
+
+
+/* end of taler-merchant-httpd_private-patch-units-ID.c */
diff --git a/src/backend/taler-merchant-httpd_private-patch-units-ID.h b/src/backend/taler-merchant-httpd_private-patch-units-ID.h
@@ -0,0 +1,33 @@
+/*
+ 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 taler-merchant-httpd_private-patch-units-ID.h
+ * @brief implement PATCH /private/units/$UNIT
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_UNITS_ID_H
+#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_UNITS_ID_H
+
+#include "taler-merchant-httpd.h"
+
+
+MHD_RESULT
+TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+/* end of taler-merchant-httpd_private-patch-units-ID.h */
+#endif
diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c
@@ -78,7 +78,7 @@
* refuses a forced download.
*/
#define MAX_KEYS_WAIT \
- GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500)
+ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500)
/**
* Generate the base URL for the given merchant instance.
@@ -116,9 +116,29 @@ struct InventoryProduct
const char *product_id;
/**
- * Number of units of the product to add to the order.
+ * Number of units of the product to add to the order (integer part).
*/
- uint32_t quantity;
+ uint64_t quantity;
+
+ /**
+ * Fractional part of the quantity in units of 1/100000000 of the base value.
+ */
+ uint32_t quantity_frac;
+
+ /**
+ * True if the integer quantity field was missing in the request.
+ */
+ bool quantity_missing;
+
+ /**
+ * String representation of the quantity, if supplied.
+ */
+ const char *unit_quantity;
+
+ /**
+ * True if the string quantity field was missing in the request.
+ */
+ bool unit_quantity_missing;
};
@@ -1023,6 +1043,64 @@ clean_order (void *cls)
/* ***************** ORDER_PHASE_EXECUTE_ORDER **************** */
/**
+ * Compute remaining stock (integer and fractional parts) for a product.
+ *
+ * @param pd product details with current totals/sold/lost
+ * @param[out] available_value remaining whole units (normalized, non-negative)
+ * @param[out] available_frac remaining fractional units (0..MERCHANT_UNIT_FRAC_BASE-1)
+ */
+static void
+compute_available_quantity (const struct TALER_MERCHANTDB_ProductDetails *pd,
+ uint64_t *available_value,
+ uint32_t *available_frac)
+{
+ int64_t value;
+ int64_t frac;
+
+ GNUNET_assert (NULL != available_value);
+ GNUNET_assert (NULL != available_frac);
+
+ if ( (INT64_MAX == pd->total_stock) &&
+ (INT32_MAX == pd->total_stock_frac) )
+ {
+ *available_value = pd->total_stock;
+ *available_frac = pd->total_stock_frac;
+ return;
+ }
+
+ value = (int64_t) pd->total_stock
+ - (int64_t) pd->total_sold
+ - (int64_t) pd->total_lost;
+ frac = (int64_t) pd->total_stock_frac
+ - (int64_t) pd->total_sold_frac
+ - (int64_t) pd->total_lost_frac;
+
+ if (frac < 0)
+ {
+ int64_t borrow = ((-frac) + MERCHANT_UNIT_FRAC_BASE - 1)
+ / MERCHANT_UNIT_FRAC_BASE;
+ value -= borrow;
+ frac += borrow * (int64_t) MERCHANT_UNIT_FRAC_BASE;
+ }
+ else if (frac >= MERCHANT_UNIT_FRAC_BASE)
+ {
+ int64_t carry = frac / MERCHANT_UNIT_FRAC_BASE;
+ value += carry;
+ frac -= carry * (int64_t) MERCHANT_UNIT_FRAC_BASE;
+ }
+
+ if (value < 0)
+ {
+ value = 0;
+ frac = 0;
+ }
+
+ *available_value = (uint64_t) value;
+ *available_frac = (uint32_t) frac;
+}
+
+
+/**
* Execute the database transaction to setup the order.
*
* @param[in,out] oc order context
@@ -1122,7 +1200,8 @@ execute_transaction (struct OrderContext *oc)
oc->hc->instance->settings.id,
oc->parse_order.order_id,
oc->parse_request.inventory_products[i].product_id,
- oc->parse_request.inventory_products[i].quantity);
+ oc->parse_request.inventory_products[i].quantity,
+ oc->parse_request.inventory_products[i].quantity_frac);
if (qs < 0)
{
TMH_db->rollback (TMH_db->cls);
@@ -1295,6 +1374,10 @@ phase_execute_order (struct OrderContext *oc)
const struct InventoryProduct *ip;
size_t num_categories = 0;
uint64_t *categories = NULL;
+ uint64_t available_quantity;
+ uint32_t available_quantity_frac;
+ char requested_quantity_buf[64];
+ char available_quantity_buf[64];
ip = &oc->parse_request.inventory_products[
oc->execute_order.out_of_stock_index];
@@ -1314,6 +1397,20 @@ phase_execute_order (struct OrderContext *oc)
GNUNET_free (categories);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order creation failed: product out of stock\n");
+
+ compute_available_quantity (&pd,
+ &available_quantity,
+ &available_quantity_frac);
+ TMH_format_fractional_string (TMH_VK_QUANTITY,
+ ip->quantity,
+ ip->quantity_frac,
+ sizeof (requested_quantity_buf),
+ requested_quantity_buf);
+ TMH_format_fractional_string (TMH_VK_QUANTITY,
+ available_quantity,
+ available_quantity_frac,
+ sizeof (available_quantity_buf),
+ available_quantity_buf);
ret = TALER_MHD_REPLY_JSON_PACK (
oc->connection,
MHD_HTTP_GONE,
@@ -1323,9 +1420,15 @@ phase_execute_order (struct OrderContext *oc)
GNUNET_JSON_pack_uint64 (
"requested_quantity",
ip->quantity),
+ GNUNET_JSON_pack_string (
+ "unit_requested_quantity",
+ requested_quantity_buf),
GNUNET_JSON_pack_uint64 (
"available_quantity",
- pd.total_stock - pd.total_sold - pd.total_lost),
+ available_quantity),
+ GNUNET_JSON_pack_string (
+ "unit_available_quantity",
+ available_quantity_buf),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_timestamp (
"restock_expected",
@@ -3186,7 +3289,7 @@ phase_merge_inventory (struct OrderContext *oc)
GNUNET_assert (NULL != oc->merge_inventory.products);
for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++)
{
- const struct InventoryProduct *ip
+ struct InventoryProduct *ip
= &oc->parse_request.inventory_products[i];
struct TALER_MERCHANTDB_ProductDetails pd;
enum GNUNET_DB_QueryStatus qs;
@@ -3238,7 +3341,48 @@ phase_merge_inventory (struct OrderContext *oc)
= GNUNET_MAX (oc->parse_order.minimum_age,
pd.minimum_age);
{
+ const char *eparam;
+
+ if ( (! ip->quantity_missing) &&
+ (ip->quantity > (uint64_t) INT64_MAX) )
+ {
+ GNUNET_break_op (0);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "quantity");
+ TALER_MERCHANTDB_product_details_free (&pd);
+ return;
+ }
+ if (GNUNET_OK !=
+ TMH_process_quantity_inputs (TMH_VK_QUANTITY,
+ pd.allow_fractional_quantity,
+ ip->quantity_missing,
+ (int64_t) ip->quantity,
+ ip->unit_quantity_missing,
+ ip->unit_quantity,
+ &ip->quantity,
+ &ip->quantity_frac,
+ &eparam))
+ {
+ GNUNET_break_op (0);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ eparam);
+ TALER_MERCHANTDB_product_details_free (&pd);
+ return;
+ }
+ }
+ {
json_t *p;
+ char unit_quantity_buf[64];
+
+ TMH_format_fractional_string (TMH_VK_QUANTITY,
+ ip->quantity,
+ ip->quantity_frac,
+ sizeof (unit_quantity_buf),
+ unit_quantity_buf);
p = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("product_name",
@@ -3257,7 +3401,9 @@ phase_merge_inventory (struct OrderContext *oc)
pd.image),
GNUNET_JSON_pack_uint64 (
"quantity",
- ip->quantity));
+ ip->quantity),
+ GNUNET_JSON_pack_string ("unit_quantity",
+ unit_quantity_buf));
GNUNET_assert (NULL != p);
GNUNET_assert (0 ==
json_array_append_new (oc->merge_inventory.products,
@@ -4299,8 +4445,14 @@ phase_parse_request (struct OrderContext *oc)
struct GNUNET_JSON_Specification ispec[] = {
GNUNET_JSON_spec_string ("product_id",
&ipr->product_id),
- GNUNET_JSON_spec_uint32 ("quantity",
- &ipr->quantity),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint64 ("quantity",
+ &ipr->quantity),
+ &ipr->quantity_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_quantity",
+ &ipr->unit_quantity),
+ &ipr->unit_quantity_missing),
GNUNET_JSON_spec_end ()
};
@@ -4323,6 +4475,11 @@ phase_parse_request (struct OrderContext *oc)
"inventory_products");
return;
}
+ if (ipr->quantity_missing && ipr->unit_quantity_missing)
+ {
+ ipr->quantity = 1;
+ ipr->quantity_missing = false;
+ }
}
}
diff --git a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c
@@ -39,15 +39,24 @@ TMH_private_post_products_ID_lock (
enum GNUNET_DB_QueryStatus qs;
const char *uuids;
struct GNUNET_Uuid uuid;
- uint32_t quantity;
+ uint64_t quantity;
+ bool quantity_missing;
+ const char *unit_quantity = NULL;
+ bool unit_quantity_missing = true;
struct GNUNET_TIME_Relative duration;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("lock_uuid",
&uuids),
GNUNET_JSON_spec_relative_time ("duration",
&duration),
- GNUNET_JSON_spec_uint32 ("quantity",
- &quantity),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint64 ("quantity",
+ &quantity),
+ &quantity_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_quantity",
+ &unit_quantity),
+ &unit_quantity_missing),
GNUNET_JSON_spec_end ()
};
@@ -67,15 +76,102 @@ TMH_private_post_products_ID_lock (
TMH_uuid_from_string (uuids,
&uuid);
TMH_db->expire_locks (TMH_db->cls);
- qs = TMH_db->lock_product (TMH_db->cls,
- mi->settings.id,
- product_id,
- &uuid,
- quantity,
- GNUNET_TIME_relative_to_timestamp (duration));
+ {
+ struct TALER_MERCHANTDB_ProductDetails pd = { 0 };
+ size_t num_categories;
+ uint64_t *categories;
+ uint64_t normalized_quantity = 0;
+ uint32_t normalized_quantity_frac = 0;
+
+ if (quantity_missing && unit_quantity_missing)
+ {
+ quantity = 1;
+ quantity_missing = false;
+ }
+ else if ( (! quantity_missing) &&
+ (quantity > (uint64_t) INT64_MAX) )
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "quantity");
+ }
+
+ qs = TMH_db->lookup_product (TMH_db->cls,
+ mi->settings.id,
+ product_id,
+ &pd,
+ &num_categories,
+ &categories);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ TALER_MERCHANTDB_product_details_free (&pd);
+ GNUNET_free (categories);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "lookup_product");
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ TALER_MERCHANTDB_product_details_free (&pd);
+ GNUNET_free (categories);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ "lookup_product");
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break_op (0);
+ TALER_MERCHANTDB_product_details_free (&pd);
+ GNUNET_free (categories);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
+ product_id);
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ GNUNET_free (categories);
+ {
+ const char *eparam;
+ if (GNUNET_OK !=
+ TMH_process_quantity_inputs (TMH_VK_QUANTITY,
+ pd.allow_fractional_quantity,
+ quantity_missing,
+ (int64_t) quantity,
+ unit_quantity_missing,
+ unit_quantity,
+ &normalized_quantity,
+ &normalized_quantity_frac,
+ &eparam))
+ {
+ TALER_MERCHANTDB_product_details_free (&pd);
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ eparam);
+ }
+ }
+ quantity = normalized_quantity;
+ qs = TMH_db->lock_product (TMH_db->cls,
+ mi->settings.id,
+ product_id,
+ &uuid,
+ quantity,
+ normalized_quantity_frac,
+ GNUNET_TIME_relative_to_timestamp (duration));
+ TALER_MERCHANTDB_product_details_free (&pd);
+ }
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
@@ -89,30 +185,7 @@ TMH_private_post_products_ID_lock (
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
"Serialization error for single-statment request");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- {
- size_t num_categories = 0;
- uint64_t *categories = NULL;
-
- qs = TMH_db->lookup_product (TMH_db->cls,
- mi->settings.id,
- product_id,
- NULL,
- &num_categories,
- &categories);
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- return TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_STORE_FAILED,
- "lookup_product");
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- return TALER_MHD_reply_with_error (
- connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
- product_id);
- GNUNET_free (categories);
- }
+ GNUNET_break_op (0);
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_GONE,
diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c
@@ -27,7 +27,6 @@
#include "taler-merchant-httpd_helper.h"
#include <taler/taler_json_lib.h>
-
MHD_RESULT
TMH_private_post_products (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
@@ -38,6 +37,15 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh,
const json_t *categories = NULL;
const char *product_id;
int64_t total_stock;
+ const char *unit_total_stock = NULL;
+ bool unit_total_stock_missing;
+ bool total_stock_missing;
+ bool unit_price_missing;
+ bool unit_allow_fraction;
+ bool unit_allow_fraction_missing;
+ uint32_t unit_precision_level;
+ bool unit_precision_missing;
+ bool price_missing;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("product_id",
&product_id),
@@ -54,8 +62,10 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh,
NULL),
GNUNET_JSON_spec_string ("unit",
(const char **) &pd.unit),
- TALER_JSON_spec_amount_any ("price",
- &pd.price),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_any ("price",
+ &pd.price),
+ &price_missing),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("image",
(const char **) &pd.image),
@@ -68,8 +78,27 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh,
GNUNET_JSON_spec_array_const ("categories",
&categories),
NULL),
- GNUNET_JSON_spec_int64 ("total_stock",
- &total_stock),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("unit_total_stock",
+ &unit_total_stock),
+ &unit_total_stock_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_int64 ("total_stock",
+ &total_stock),
+ &total_stock_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("unit_allow_fraction",
+ &unit_allow_fraction),
+ &unit_allow_fraction_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 ("unit_precision_level",
+ &unit_precision_level),
+ &unit_precision_missing),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_any_array ("unit_price",
+ &pd.price_array_length,
+ &pd.price_array),
+ &unit_price_missing),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_json ("address",
&pd.address),
@@ -110,15 +139,100 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh,
product name; remove once we make product_name mandatory. */
if (NULL == pd.product_name)
pd.product_name = pd.description;
+
+ if (! unit_price_missing)
+ {
+ if (! price_missing)
+ {
+ if (0 != TALER_amount_cmp (&pd.price,
+ &pd.price_array[0]))
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "price,unit_price mismatch");
+ goto cleanup;
+ }
+ }
+ else
+ {
+ pd.price = pd.price_array[0];
+ price_missing = false;
+ }
+ }
+ else
+ {
+ if (price_missing)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "price and unit_price missing");
+ goto cleanup;
+ }
+ pd.price_array = GNUNET_new_array (1,
+ struct TALER_Amount);
+ pd.price_array[0] = pd.price;
+ pd.price_array_length = 1;
+ }
}
- if (total_stock < -1)
+ if (! unit_precision_missing)
{
- GNUNET_break_op (0);
- ret = TALER_MHD_reply_with_error (connection,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "total_stock");
- goto cleanup;
+ if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_precision_level");
+ goto cleanup;
+ }
+ }
+ {
+ bool default_allow_fractional;
+ uint32_t default_precision_level;
+
+ if (GNUNET_OK !=
+ TMH_unit_defaults_for_instance (mi,
+ pd.unit,
+ &default_allow_fractional,
+ &default_precision_level))
+ {
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "unit defaults");
+ goto cleanup;
+ }
+ if (unit_allow_fraction_missing)
+ unit_allow_fraction = default_allow_fractional;
+ if (unit_precision_missing)
+ unit_precision_level = default_precision_level;
+ }
+ if (! unit_allow_fraction)
+ unit_precision_level = 0;
+ pd.fractional_precision_level = unit_precision_level;
+ {
+ const char *eparam;
+ if (GNUNET_OK !=
+ TMH_process_quantity_inputs (TMH_VK_STOCK,
+ unit_allow_fraction,
+ total_stock_missing,
+ total_stock,
+ unit_total_stock_missing,
+ unit_total_stock,
+ &pd.total_stock,
+ &pd.total_stock_frac,
+ &eparam))
+ {
+ ret = TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ eparam);
+ goto cleanup;
+ }
+ pd.allow_fractional_quantity = unit_allow_fraction;
}
num_cats = json_array_size (categories);
cats = GNUNET_new_array (num_cats,
@@ -142,11 +256,6 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh,
}
}
- if (-1 == total_stock)
- pd.total_stock = INT64_MAX;
- else
- pd.total_stock = (uint64_t) total_stock;
-
if (NULL == pd.address)
pd.address = json_object ();
if (NULL == pd.description_i18n)
@@ -270,6 +379,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh,
0);
cleanup:
GNUNET_JSON_parse_free (spec);
+ GNUNET_free (pd.price_array);
GNUNET_free (cats);
return ret;
}
diff --git a/src/backend/taler-merchant-httpd_private-post-units.c b/src/backend/taler-merchant-httpd_private-post-units.c
@@ -0,0 +1,222 @@
+/*
+ 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 taler-merchant-httpd_private-post-units.c
+ * @brief implement POST /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include "taler-merchant-httpd_private-post-units.h"
+#include "taler-merchant-httpd_helper.h"
+#include <taler/taler_json_lib.h>
+
+/**
+ * Maximum fractional precision level accepted from clients.
+ */
+#define TMH_MAX_UNIT_PRECISION_LEVEL 6
+
+MHD_RESULT
+TMH_private_post_units (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ struct TMH_MerchantInstance *mi = hc->instance;
+ struct TALER_MERCHANTDB_UnitDetails nud = { 0 };
+ bool allow_fraction_missing = true;
+ bool unit_precision_missing = true;
+ bool unit_active_missing = true;
+ enum GNUNET_GenericReturnValue res;
+ enum GNUNET_DB_QueryStatus qs;
+ MHD_RESULT ret;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("unit",
+ (const char **) &nud.unit),
+ GNUNET_JSON_spec_string ("unit_name_long",
+ (const char **) &nud.unit_name_long),
+ GNUNET_JSON_spec_string ("unit_name_short",
+ (const char **) &nud.unit_name_short),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_json ("unit_name_long_i18n",
+ &nud.unit_name_long_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_json ("unit_name_short_i18n",
+ &nud.unit_name_short_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("unit_allow_fraction",
+ &nud.unit_allow_fraction),
+ &allow_fraction_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 ("unit_precision_level",
+ &nud.unit_precision_level),
+ &unit_precision_missing),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("unit_active",
+ &nud.unit_active),
+ &unit_active_missing),
+ GNUNET_JSON_spec_end ()
+ };
+
+
+ GNUNET_assert (NULL != mi);
+ res = TALER_MHD_parse_json_data (connection,
+ hc->request_body,
+ spec);
+ (void) rh;
+
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break_op (0);
+ return (GNUNET_NO == res)
+ ? MHD_YES
+ : MHD_NO;
+ }
+
+ if (allow_fraction_missing)
+ {
+ nud.unit_allow_fraction = false;
+ nud.unit_precision_level = 0;
+ }
+ else
+ {
+ if (! nud.unit_allow_fraction)
+ {
+ nud.unit_precision_level = 0;
+ unit_precision_missing = false;
+ }
+ else if (unit_precision_missing)
+ {
+ nud.unit_precision_level = 0;
+ }
+ }
+ if (nud.unit_precision_level > TMH_MAX_UNIT_PRECISION_LEVEL)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_precision_level");
+ goto cleanup;
+ }
+ if (unit_active_missing)
+ nud.unit_active = true;
+
+ if (NULL == nud.unit_name_long_i18n)
+ nud.unit_name_long_i18n = json_object ();
+ if (NULL == nud.unit_name_short_i18n)
+ nud.unit_name_short_i18n = json_object ();
+
+ if (! TALER_JSON_check_i18n (nud.unit_name_long_i18n))
+ {
+ GNUNET_break_op (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_name_long_i18n");
+ goto cleanup;
+ }
+ if (! TALER_JSON_check_i18n (nud.unit_name_short_i18n))
+ {
+ GNUNET_break_op (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "unit_name_short_i18n");
+ goto cleanup;
+ }
+
+ nud.unit_builtin = false;
+
+ {
+ bool no_instance = false;
+ bool conflict = false;
+ uint64_t unit_serial = 0;
+
+ qs = TMH_db->insert_unit (TMH_db->cls,
+ mi->settings.id,
+ &nud,
+ &no_instance,
+ &conflict,
+ &unit_serial);
+
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ NULL);
+ goto cleanup;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ NULL);
+ goto cleanup;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
+ "insert_unit");
+ goto cleanup;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+
+ if (no_instance)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
+ mi->settings.id);
+ goto cleanup;
+ }
+ if (conflict)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN,
+ nud.unit);
+ goto cleanup;
+ }
+
+ ret = TALER_MHD_reply_static (connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+ }
+
+cleanup:
+ if (NULL != nud.unit_name_long_i18n)
+ {
+ json_decref (nud.unit_name_long_i18n);
+ nud.unit_name_long_i18n = NULL;
+ }
+ if (NULL != nud.unit_name_short_i18n)
+ {
+ json_decref (nud.unit_name_short_i18n);
+ nud.unit_name_short_i18n = NULL;
+ }
+ GNUNET_JSON_parse_free (spec);
+ return ret;
+}
+
+
+/* end of taler-merchant-httpd_private-post-units.c */
diff --git a/src/backend/taler-merchant-httpd_private-post-units.h b/src/backend/taler-merchant-httpd_private-post-units.h
@@ -0,0 +1,33 @@
+/*
+ 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 taler-merchant-httpd_private-post-units.h
+ * @brief implement POST /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_UNITS_H
+#define TALER_MERCHANT_HTTPD_PRIVATE_POST_UNITS_H
+
+#include "taler-merchant-httpd.h"
+
+
+MHD_RESULT
+TMH_private_post_units (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+/* end of taler-merchant-httpd_private-post-units.h */
+#endif
diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am
@@ -43,6 +43,7 @@ sql_DATA = \
merchant-0024.sql \
merchant-0025.sql \
merchant-0026.sql \
+ merchant-0027.sql \
drop.sql
BUILT_SOURCES = \
@@ -114,6 +115,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \
pg_increase_refund.h pg_increase_refund.c \
pg_insert_account.h pg_insert_account.c \
pg_insert_category.h pg_insert_category.c \
+ pg_insert_unit.h pg_insert_unit.c \
pg_insert_contract_terms.h pg_insert_contract_terms.c \
pg_insert_deposit.h pg_insert_deposit.c \
pg_insert_deposit_confirmation.h pg_insert_deposit_confirmation.c \
@@ -132,11 +134,15 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \
pg_insert_refund_proof.h pg_insert_refund_proof.c \
pg_insert_spent_token.h pg_insert_spent_token.c \
pg_insert_template.h pg_insert_template.c \
+ pg_update_unit.h pg_update_unit.c \
pg_insert_token_family.h pg_insert_token_family.c \
pg_insert_token_family_key.h pg_insert_token_family_key.c \
pg_insert_transfer.h pg_insert_transfer.c \
pg_insert_transfer_details.h pg_insert_transfer_details.c \
pg_insert_webhook.h pg_insert_webhook.c \
+ pg_delete_unit.h pg_delete_unit.c \
+ pg_lookup_units.h pg_lookup_units.c \
+ pg_select_unit.h pg_select_unit.c \
pg_lookup_mfa_challenge.h pg_lookup_mfa_challenge.c \
pg_solve_mfa_challenge.h pg_solve_mfa_challenge.c \
pg_update_mfa_challenge.h pg_update_mfa_challenge.c \
diff --git a/src/backenddb/merchant-0027.sql b/src/backenddb/merchant-0027.sql
@@ -0,0 +1,464 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2025 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/>
+
+-- @file merchant-0027.sql
+-- @brief Add fractional stock support to merchant_inventory
+-- @author Bohdan Potuzhnyi
+
+BEGIN;
+
+-- Check patch versioning is in place.
+SELECT _v.register_patch('merchant-0027', NULL, NULL);
+
+SET search_path TO merchant;
+
+ALTER TABLE merchant_inventory
+ ADD COLUMN price_array taler_amount_currency[]
+ NOT NULL
+ DEFAULT ARRAY[]::taler_amount_currency[];
+COMMENT ON COLUMN merchant_inventory.price_array
+ IS 'List of unit prices available for the product (multiple tiers supported).';
+
+UPDATE merchant_inventory
+SET price_array = ARRAY[price]::taler_amount_currency[]
+WHERE price IS NOT NULL; -- theoretically all objects, but just to be sure
+
+-- I assume we want to make drop price column at some point of time
+
+ALTER TABLE merchant_inventory
+ ADD COLUMN total_stock_frac INT4 NOT NULL DEFAULT 0;
+COMMENT ON COLUMN merchant_inventory.total_stock_frac
+ IS 'Fractional part of stock in units of 1/100000000 of the base value';
+
+ALTER TABLE merchant_inventory
+ ADD COLUMN total_sold_frac INT4 NOT NULL DEFAULT 0;
+COMMENT ON COLUMN merchant_inventory.total_sold_frac
+ IS 'Fractional part of units sold in units of 1/100000000 of the base value';
+
+ALTER TABLE merchant_inventory
+ ADD COLUMN total_lost_frac INT4 NOT NULL DEFAULT 0;
+COMMENT ON COLUMN merchant_inventory.total_lost_frac
+ IS 'Fractional part of units lost in units of 1/100000000 of the base value';
+
+ALTER TABLE merchant_inventory
+ ADD COLUMN allow_fractional_quantity BOOL NOT NULL DEFAULT FALSE;
+COMMENT ON COLUMN merchant_inventory.allow_fractional_quantity
+ IS 'Whether fractional stock (total_stock_frac) should be honored for this product';
+
+ALTER TABLE merchant_inventory
+ ADD COLUMN fractional_precision_level INT4 NOT NULL DEFAULT 0;
+COMMENT ON COLUMN merchant_inventory.fractional_precision_level
+ IS 'Preset number of decimal places for fractional quantities';
+
+ALTER TABLE merchant_inventory_locks
+ ADD COLUMN total_locked_frac INT4 NOT NULL DEFAULT 0;
+COMMENT ON COLUMN merchant_inventory_locks.total_locked_frac
+ IS 'Fractional part of locked stock in units of 1/100000000 of the base value';
+
+ALTER TABLE merchant_order_locks
+ ADD COLUMN total_locked_frac INT4 NOT NULL DEFAULT 0;
+COMMENT ON COLUMN merchant_order_locks.total_locked_frac
+ IS 'Fractional part of locked stock associated with orders in units of 1/100000000 of the base value';
+
+CREATE TABLE merchant_builtin_units
+(
+ unit_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ unit TEXT NOT NULL UNIQUE,
+ unit_name_long TEXT NOT NULL,
+ unit_name_short TEXT NOT NULL,
+ unit_name_long_i18n BYTEA NOT NULL DEFAULT convert_to('{}','UTF8'),
+ unit_name_short_i18n BYTEA NOT NULL DEFAULT convert_to('{}','UTF8'),
+ unit_allow_fraction BOOLEAN NOT NULL DEFAULT FALSE,
+ unit_precision_level INT4 NOT NULL DEFAULT 0 CHECK (unit_precision_level BETWEEN 0 AND 6),
+ unit_active BOOLEAN NOT NULL DEFAULT TRUE
+);
+COMMENT ON TABLE merchant_builtin_units
+ IS 'Global catalogue of builtin measurement units.';
+COMMENT ON COLUMN merchant_builtin_units.unit_active
+ IS 'Default visibility for the builtin unit; instances may override.';
+
+CREATE TABLE merchant_custom_units
+(
+ unit_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ merchant_serial BIGINT NOT NULL REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE,
+ unit TEXT NOT NULL,
+ unit_name_long TEXT NOT NULL,
+ unit_name_short TEXT NOT NULL,
+ unit_name_long_i18n BYTEA NOT NULL DEFAULT convert_to('{}','UTF8'),
+ unit_name_short_i18n BYTEA NOT NULL DEFAULT convert_to('{}','UTF8'),
+ unit_allow_fraction BOOLEAN NOT NULL DEFAULT FALSE,
+ unit_precision_level INT4 NOT NULL DEFAULT 0 CHECK (unit_precision_level BETWEEN 0 AND 6),
+ unit_active BOOLEAN NOT NULL DEFAULT TRUE,
+ UNIQUE (merchant_serial, unit)
+);
+COMMENT ON TABLE merchant_custom_units
+ IS 'Per-instance custom measurement units.';
+
+CREATE TABLE merchant_builtin_unit_overrides
+(
+ merchant_serial BIGINT NOT NULL REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE,
+ builtin_unit_serial BIGINT NOT NULL REFERENCES merchant_builtin_units (unit_serial) ON DELETE CASCADE,
+ override_allow_fraction BOOLEAN,
+ override_precision_level INT4 CHECK (override_precision_level BETWEEN 0 AND 6),
+ override_active BOOLEAN,
+ PRIMARY KEY (merchant_serial, builtin_unit_serial)
+);
+COMMENT ON TABLE merchant_builtin_unit_overrides
+ IS 'Per-instance overrides for builtin units (fraction policy and visibility).';
+
+INSERT INTO merchant_builtin_units (unit, unit_name_long, unit_name_short, unit_allow_fraction, unit_precision_level, unit_active)
+VALUES
+ ('Piece', 'piece', 'pc', FALSE, 0, TRUE),
+ ('Set', 'set', 'set', FALSE, 0, TRUE),
+ ('SizeUnitCm', 'centimetre', 'cm', TRUE, 1, TRUE),
+ ('SizeUnitDm', 'decimetre', 'dm', TRUE, 3, TRUE),
+ ('SizeUnitFoot', 'foot', 'ft', TRUE, 3, TRUE),
+ ('SizeUnitInch', 'inch', 'in', TRUE, 2, TRUE),
+ ('SizeUnitM', 'metre', 'm', TRUE, 3, TRUE),
+ ('SizeUnitMm', 'millimetre', 'mm', FALSE, 0, TRUE),
+ ('SurfaceUnitCm2', 'square centimetre', 'cm²', TRUE, 2, TRUE),
+ ('SurfaceUnitDm2', 'square decimetre', 'dm²', TRUE, 3, TRUE),
+ ('SurfaceUnitFoot2', 'square foot', 'ft²', TRUE, 3, TRUE),
+ ('SurfaceUnitInch2', 'square inch', 'in²', TRUE, 4, TRUE),
+ ('SurfaceUnitM2', 'square metre', 'm²', TRUE, 4, TRUE),
+ ('SurfaceUnitMm2', 'square millimetre', 'mm²', TRUE, 1, TRUE),
+ ('TimeUnitDay', 'day', 'd', TRUE, 3, TRUE),
+ ('TimeUnitHour', 'hour', 'h', TRUE, 2, TRUE),
+ ('TimeUnitMinute', 'minute', 'min', TRUE, 3, TRUE),
+ ('TimeUnitMonth', 'month', 'mo', TRUE, 2, TRUE),
+ ('TimeUnitSecond', 'second', 's', TRUE, 3, TRUE),
+ ('TimeUnitWeek', 'week', 'wk', TRUE, 3, TRUE),
+ ('TimeUnitYear', 'year', 'yr', TRUE, 4, TRUE),
+ ('VolumeUnitCm3', 'cubic centimetre', 'cm³', TRUE, 3, TRUE),
+ ('VolumeUnitDm3', 'cubic decimetre', 'dm³', TRUE, 5, TRUE),
+ ('VolumeUnitFoot3', 'cubic foot', 'ft³', TRUE, 5, TRUE),
+ ('VolumeUnitGallon', 'gallon', 'gal', TRUE, 3, TRUE),
+ ('VolumeUnitInch3', 'cubic inch', 'in³', TRUE, 2, TRUE),
+ ('VolumeUnitLitre', 'litre', 'L', TRUE, 3, TRUE),
+ ('VolumeUnitM3', 'cubic metre', 'm³', TRUE, 6, TRUE),
+ ('VolumeUnitMm3', 'cubic millimetre', 'mm³', TRUE, 1, TRUE),
+ ('VolumeUnitOunce', 'fluid ounce', 'fl oz', TRUE, 2, TRUE),
+ ('WeightUnitG', 'gram', 'g', TRUE, 1, TRUE),
+ ('WeightUnitKg', 'kilogram', 'kg', TRUE, 3, TRUE),
+ ('WeightUnitMg', 'milligram', 'mg', FALSE, 0, TRUE),
+ ('WeightUnitOunce', 'ounce', 'oz', TRUE, 2, TRUE),
+ ('WeightUnitPound', 'pound', 'lb', TRUE, 3, TRUE),
+ ('WeightUnitTon', 'metric tonne', 't', TRUE, 3, TRUE);
+
+DROP FUNCTION IF EXISTS merchant_do_insert_unit;
+CREATE FUNCTION merchant_do_insert_unit (
+ IN in_instance_id TEXT,
+ IN in_unit TEXT,
+ IN in_unit_name_long TEXT,
+ IN in_unit_name_short TEXT,
+ IN in_unit_name_long_i18n BYTEA,
+ IN in_unit_name_short_i18n BYTEA,
+ IN in_unit_allow_fraction BOOL,
+ IN in_unit_precision_level INT4,
+ IN in_unit_active BOOL,
+ OUT out_no_instance BOOL,
+ OUT out_conflict BOOL,
+ OUT out_unit_serial INT8)
+ LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_merchant_id INT8;
+BEGIN
+ SELECT merchant_serial
+ INTO my_merchant_id
+ FROM merchant_instances
+ WHERE merchant_id = in_instance_id;
+
+ IF NOT FOUND THEN
+ out_no_instance := TRUE;
+ out_conflict := FALSE;
+ out_unit_serial := NULL;
+ RETURN;
+ END IF;
+
+ out_no_instance := FALSE;
+
+ -- Reject attempts to shadow builtin identifiers.
+ IF EXISTS (
+ SELECT 1 FROM merchant_builtin_units bu WHERE bu.unit = in_unit
+ ) THEN
+ out_conflict := TRUE;
+ out_unit_serial := NULL;
+ RETURN;
+ END IF;
+
+ INSERT INTO merchant_custom_units (
+ merchant_serial,
+ unit,
+ unit_name_long,
+ unit_name_short,
+ unit_name_long_i18n,
+ unit_name_short_i18n,
+ unit_allow_fraction,
+ unit_precision_level,
+ unit_active)
+ VALUES (
+ my_merchant_id,
+ in_unit,
+ in_unit_name_long,
+ in_unit_name_short,
+ in_unit_name_long_i18n,
+ in_unit_name_short_i18n,
+ in_unit_allow_fraction,
+ in_unit_precision_level,
+ in_unit_active)
+ ON CONFLICT (merchant_serial, unit) DO NOTHING
+ RETURNING unit_serial
+ INTO out_unit_serial;
+
+ IF FOUND THEN
+ out_conflict := FALSE;
+ RETURN;
+ END IF;
+
+ -- Conflict: custom unit already exists.
+ SELECT unit_serial
+ INTO out_unit_serial
+ FROM merchant_custom_units
+ WHERE merchant_serial = my_merchant_id
+ AND unit = in_unit;
+
+ out_conflict := TRUE;
+END $$;
+
+DROP FUNCTION IF EXISTS merchant_do_update_unit;
+CREATE FUNCTION merchant_do_update_unit (
+ IN in_instance_id TEXT,
+ IN in_unit_id TEXT,
+ IN in_unit_name_long TEXT,
+ IN in_unit_name_long_i18n BYTEA,
+ IN in_unit_name_short TEXT,
+ IN in_unit_name_short_i18n BYTEA,
+ IN in_unit_allow_fraction BOOL,
+ IN in_unit_precision_level INT4,
+ IN in_unit_active BOOL,
+ OUT out_no_instance BOOL,
+ OUT out_no_unit BOOL,
+ OUT out_builtin_conflict BOOL)
+ LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_merchant_id INT8;
+ my_custom merchant_custom_units%ROWTYPE;
+ my_builtin merchant_builtin_units%ROWTYPE;
+ my_override merchant_builtin_unit_overrides%ROWTYPE;
+ new_unit_name_long TEXT;
+ new_unit_name_short TEXT;
+ new_unit_name_long_i18n BYTEA;
+ new_unit_name_short_i18n BYTEA;
+ new_unit_allow_fraction BOOL;
+ new_unit_precision_level INT4;
+ new_unit_active BOOL;
+ old_unit_allow_fraction BOOL;
+ old_unit_precision_level INT4;
+ old_unit_active BOOL;
+BEGIN
+ out_no_instance := FALSE;
+ out_no_unit := FALSE;
+ out_builtin_conflict := FALSE;
+
+ SELECT merchant_serial
+ INTO my_merchant_id
+ FROM merchant_instances
+ WHERE merchant_id = in_instance_id;
+
+ IF NOT FOUND THEN
+ out_no_instance := TRUE;
+ RETURN;
+ END IF;
+
+ SELECT *
+ INTO my_custom
+ FROM merchant_custom_units
+ WHERE merchant_serial = my_merchant_id
+ AND unit = in_unit_id
+ FOR UPDATE;
+
+ IF FOUND THEN
+ old_unit_allow_fraction := my_custom.unit_allow_fraction;
+ old_unit_precision_level := my_custom.unit_precision_level;
+ old_unit_active := my_custom.unit_active;
+
+ new_unit_name_long := COALESCE (in_unit_name_long, my_custom.unit_name_long);
+ new_unit_name_short := COALESCE (in_unit_name_short, my_custom.unit_name_short);
+ new_unit_name_long_i18n := COALESCE (in_unit_name_long_i18n,
+ my_custom.unit_name_long_i18n);
+ new_unit_name_short_i18n := COALESCE (in_unit_name_short_i18n,
+ my_custom.unit_name_short_i18n);
+ new_unit_allow_fraction := COALESCE (in_unit_allow_fraction,
+ my_custom.unit_allow_fraction);
+ new_unit_precision_level := COALESCE (in_unit_precision_level,
+ my_custom.unit_precision_level);
+ IF NOT new_unit_allow_fraction THEN
+ new_unit_precision_level := 0;
+ END IF;
+
+ new_unit_active := COALESCE (in_unit_active, my_custom.unit_active);
+
+ UPDATE merchant_custom_units SET
+ unit_name_long = new_unit_name_long
+ ,unit_name_long_i18n = new_unit_name_long_i18n
+ ,unit_name_short = new_unit_name_short
+ ,unit_name_short_i18n = new_unit_name_short_i18n
+ ,unit_allow_fraction = new_unit_allow_fraction
+ ,unit_precision_level = new_unit_precision_level
+ ,unit_active = new_unit_active
+ WHERE unit_serial = my_custom.unit_serial;
+
+ ASSERT FOUND,'SELECTED it earlier, should UPDATE it now';
+
+ IF old_unit_allow_fraction IS DISTINCT FROM new_unit_allow_fraction
+ OR old_unit_precision_level IS DISTINCT FROM new_unit_precision_level
+ THEN
+ UPDATE merchant_inventory SET
+ allow_fractional_quantity = new_unit_allow_fraction
+ , fractional_precision_level = new_unit_precision_level
+ WHERE merchant_serial = my_merchant_id
+ AND unit = in_unit_id
+ AND allow_fractional_quantity = old_unit_allow_fraction
+ AND fractional_precision_level = old_unit_precision_level;
+ END IF;
+ RETURN;
+ END IF;
+
+ -- Try builtin with overrides.
+ SELECT *
+ INTO my_builtin
+ FROM merchant_builtin_units
+ WHERE unit = in_unit_id;
+
+ IF NOT FOUND THEN
+ out_no_unit := TRUE;
+ RETURN;
+ END IF;
+
+ SELECT *
+ INTO my_override
+ FROM merchant_builtin_unit_overrides
+ WHERE merchant_serial = my_merchant_id
+ AND builtin_unit_serial = my_builtin.unit_serial
+ FOR UPDATE;
+
+ old_unit_allow_fraction := COALESCE (my_override.override_allow_fraction,
+ my_builtin.unit_allow_fraction);
+ old_unit_precision_level := COALESCE (my_override.override_precision_level,
+ my_builtin.unit_precision_level);
+ old_unit_active := COALESCE (my_override.override_active,
+ my_builtin.unit_active);
+
+ -- Only policy flags can change for builtin units.
+ IF in_unit_name_long IS NOT NULL
+ OR in_unit_name_short IS NOT NULL
+ OR in_unit_name_long_i18n IS NOT NULL
+ OR in_unit_name_short_i18n IS NOT NULL THEN
+ out_builtin_conflict := TRUE;
+ RETURN;
+ END IF;
+
+ new_unit_allow_fraction := COALESCE (in_unit_allow_fraction,
+ old_unit_allow_fraction);
+ new_unit_precision_level := COALESCE (in_unit_precision_level,
+ old_unit_precision_level);
+ IF NOT new_unit_allow_fraction THEN
+ new_unit_precision_level := 0;
+ END IF;
+ new_unit_active := COALESCE (in_unit_active, old_unit_active);
+
+ INSERT INTO merchant_builtin_unit_overrides (
+ merchant_serial,
+ builtin_unit_serial,
+ override_allow_fraction,
+ override_precision_level,
+ override_active)
+ VALUES (my_merchant_id,
+ my_builtin.unit_serial,
+ new_unit_allow_fraction,
+ new_unit_precision_level,
+ new_unit_active)
+ ON CONFLICT (merchant_serial, builtin_unit_serial)
+ DO UPDATE SET override_allow_fraction = EXCLUDED.override_allow_fraction
+ , override_precision_level = EXCLUDED.override_precision_level
+ , override_active = EXCLUDED.override_active;
+
+ IF old_unit_allow_fraction IS DISTINCT FROM new_unit_allow_fraction
+ OR old_unit_precision_level IS DISTINCT FROM new_unit_precision_level
+ THEN
+ UPDATE merchant_inventory SET
+ allow_fractional_quantity = new_unit_allow_fraction
+ , fractional_precision_level = new_unit_precision_level
+ WHERE merchant_serial = my_merchant_id
+ AND unit = in_unit_id
+ AND allow_fractional_quantity = old_unit_allow_fraction
+ AND fractional_precision_level = old_unit_precision_level;
+ END IF;
+
+ RETURN;
+END $$;
+
+DROP FUNCTION IF EXISTS merchant_do_delete_unit;
+CREATE FUNCTION merchant_do_delete_unit (
+ IN in_instance_id TEXT,
+ IN in_unit_id TEXT,
+ OUT out_no_instance BOOL,
+ OUT out_no_unit BOOL,
+ OUT out_builtin_conflict BOOL)
+ LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_merchant_id INT8;
+ my_unit merchant_custom_units%ROWTYPE;
+BEGIN
+ out_no_instance := FALSE;
+ out_no_unit := FALSE;
+ out_builtin_conflict := FALSE;
+
+ SELECT merchant_serial
+ INTO my_merchant_id
+ FROM merchant_instances
+ WHERE merchant_id = in_instance_id;
+
+ IF NOT FOUND THEN
+ out_no_instance := TRUE;
+ RETURN;
+ END IF;
+
+ SELECT *
+ INTO my_unit
+ FROM merchant_custom_units
+ WHERE merchant_serial = my_merchant_id
+ AND unit = in_unit_id
+ FOR UPDATE;
+
+ IF NOT FOUND THEN
+ IF EXISTS (SELECT 1 FROM merchant_builtin_units bu WHERE bu.unit = in_unit_id) THEN
+ out_builtin_conflict := TRUE;
+ ELSE
+ out_no_unit := TRUE;
+ END IF;
+ RETURN;
+ END IF;
+
+ DELETE FROM merchant_custom_units
+ WHERE unit_serial = my_unit.unit_serial;
+
+ RETURN;
+END $$;
+
+COMMIT;
diff --git a/src/backenddb/merchantdb_helper.c b/src/backenddb/merchantdb_helper.c
@@ -38,6 +38,9 @@ TALER_MERCHANTDB_product_details_free (
GNUNET_free (pd->image);
json_decref (pd->address);
pd->address = NULL;
+ GNUNET_free (pd->price_array);
+ pd->price_array = NULL;
+ pd->price_array_length = 0;
}
@@ -102,4 +105,18 @@ TALER_MERCHANTDB_category_details_free (
}
+void
+TALER_MERCHANTDB_unit_details_free (
+ struct TALER_MERCHANTDB_UnitDetails *ud)
+{
+ GNUNET_free (ud->unit);
+ GNUNET_free (ud->unit_name_long);
+ GNUNET_free (ud->unit_name_short);
+ json_decref (ud->unit_name_long_i18n);
+ ud->unit_name_long_i18n = NULL;
+ json_decref (ud->unit_name_short_i18n);
+ ud->unit_name_short_i18n = NULL;
+}
+
+
/* end of merchantdb_helper.c */
diff --git a/src/backenddb/pg_delete_unit.c b/src/backenddb/pg_delete_unit.c
@@ -0,0 +1,68 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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 <http://www.gnu.org/licenses/>
+ */
+/**
+ * @file backenddb/pg_delete_unit.c
+ * @brief Implementation of the delete_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_delete_unit.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_delete_unit (void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ bool *no_instance,
+ bool *no_unit,
+ bool *builtin_conflict)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_string (instance_id),
+ GNUNET_PQ_query_param_string (unit_id),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_no_instance",
+ no_instance),
+ GNUNET_PQ_result_spec_bool ("out_no_unit",
+ no_unit),
+ GNUNET_PQ_result_spec_bool ("out_builtin_conflict",
+ builtin_conflict),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ PREPARE (pg,
+ "delete_unit",
+ "SELECT"
+ " out_no_instance"
+ " ,out_no_unit"
+ " ,out_builtin_conflict"
+ " FROM merchant_do_delete_unit($1,$2);");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "delete_unit",
+ params,
+ rs);
+ GNUNET_PQ_cleanup_query_params_closures (params);
+ return qs;
+}
diff --git a/src/backenddb/pg_delete_unit.h b/src/backenddb/pg_delete_unit.h
@@ -0,0 +1,45 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_delete_unit.h
+ * @brief implementation of the delete_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef PG_DELETE_UNIT_H
+#define PG_DELETE_UNIT_H
+
+#include "taler_merchantdb_plugin.h"
+
+/**
+ * Delete a measurement unit.
+ *
+ * @param cls closure
+ * @param instance_id instance to delete unit from
+ * @param unit_id symbolic identifier
+ * @param[out] no_instance set to true if @a instance_id is unknown
+ * @param[out] no_unit set to true if the unit does not exist
+ * @param[out] builtin_conflict set to true if the unit cannot be deleted
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_delete_unit (void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ bool *no_instance,
+ bool *no_unit,
+ bool *builtin_conflict);
+
+#endif
diff --git a/src/backenddb/pg_insert_order_lock.c b/src/backenddb/pg_insert_order_lock.c
@@ -30,7 +30,8 @@ TMH_PG_insert_order_lock (void *cls,
const char *instance_id,
const char *order_id,
const char *product_id,
- uint64_t quantity)
+ uint64_t quantity,
+ uint32_t quantity_frac)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
@@ -38,6 +39,7 @@ TMH_PG_insert_order_lock (void *cls,
GNUNET_PQ_query_param_string (order_id),
GNUNET_PQ_query_param_string (product_id),
GNUNET_PQ_query_param_uint64 (&quantity),
+ GNUNET_PQ_query_param_uint32 (&quantity_frac),
GNUNET_PQ_query_param_end
};
@@ -49,8 +51,12 @@ TMH_PG_insert_order_lock (void *cls,
" product_serial"
" ,merchant_serial"
" ,total_stock"
+ " ,total_stock_frac"
" ,total_sold"
+ " ,total_sold_frac"
" ,total_lost"
+ " ,total_lost_frac"
+ " ,allow_fractional_quantity"
" FROM merchant_inventory"
" WHERE product_id=$3"
" AND merchant_serial="
@@ -60,18 +66,32 @@ TMH_PG_insert_order_lock (void *cls,
" INSERT INTO merchant_order_locks"
" (product_serial"
" ,total_locked"
+ " ,total_locked_frac"
" ,order_serial)"
- " SELECT tmp.product_serial, $4, order_serial"
+ " SELECT tmp.product_serial, $4::INT8, $5::INT4, order_serial"
" FROM merchant_orders"
" JOIN tmp USING(merchant_serial)"
- " WHERE order_id=$2 AND"
- " tmp.total_stock - tmp.total_sold - tmp.total_lost - $4 >= "
- " (SELECT COALESCE(SUM(total_locked), 0)"
- " FROM merchant_inventory_locks"
- " WHERE product_serial=tmp.product_serial) + "
- " (SELECT COALESCE(SUM(total_locked), 0)"
- " FROM merchant_order_locks"
- " WHERE product_serial=tmp.product_serial)");
+ " WHERE order_id=$2"
+ " AND (tmp.allow_fractional_quantity OR $5 = 0)"
+ " AND (tmp.total_stock = 9223372036854775807"
+ " OR ("
+ " (tmp.total_stock::NUMERIC * 100000000"
+ " + tmp.total_stock_frac::NUMERIC)"
+ " - (tmp.total_sold::NUMERIC * 100000000"
+ " + tmp.total_sold_frac::NUMERIC)"
+ " - (tmp.total_lost::NUMERIC * 100000000"
+ " + tmp.total_lost_frac::NUMERIC)"
+ " >= "
+ " (($4::NUMERIC * 100000000) + $5::NUMERIC)"
+ " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 100000000"
+ " + total_locked_frac::NUMERIC), 0)"
+ " FROM merchant_inventory_locks mil"
+ " WHERE mil.product_serial = tmp.product_serial)"
+ " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 100000000"
+ " + total_locked_frac::NUMERIC), 0)"
+ " FROM merchant_order_locks mol"
+ " WHERE mol.product_serial = tmp.product_serial)"
+ " ))");
return GNUNET_PQ_eval_prepared_non_select (pg->conn,
"insert_order_lock",
params);
diff --git a/src/backenddb/pg_insert_order_lock.h b/src/backenddb/pg_insert_order_lock.h
@@ -33,6 +33,8 @@
* @param order_id alphanumeric string that uniquely identifies the order
* @param product_id uniquely identifies the product to be locked
* @param quantity how many units should be locked to the @a order_id
+ * @param quantity_frac fractional component of the quantity in units of
+ * 1/100000000 of the base value
* @return transaction status,
* #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS means there are insufficient stocks
* #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success
@@ -42,6 +44,7 @@ TMH_PG_insert_order_lock (void *cls,
const char *instance_id,
const char *order_id,
const char *product_id,
- uint64_t quantity);
+ uint64_t quantity,
+ uint32_t quantity_frac);
#endif
diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c
@@ -49,7 +49,14 @@ TMH_PG_insert_product (void *cls,
TALER_PQ_query_param_json (pd->taxes),
TALER_PQ_query_param_amount_with_currency (pg->conn,
&pd->price),
+ TALER_PQ_query_param_array_amount_with_currency (
+ pd->price_array_length,
+ pd->price_array,
+ pg->conn),
GNUNET_PQ_query_param_uint64 (&pd->total_stock),
+ GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac),
+ GNUNET_PQ_query_param_bool (pd->allow_fractional_quantity),
+ GNUNET_PQ_query_param_uint32 (&pd->fractional_precision_level),
TALER_PQ_query_param_json (pd->address),
GNUNET_PQ_query_param_timestamp (&pd->next_restock),
GNUNET_PQ_query_param_uint32 (&pd->minimum_age),
@@ -79,11 +86,11 @@ TMH_PG_insert_product (void *cls,
"insert_product",
"SELECT"
" out_conflict AS conflict"
- ",out_no_cat AS no_cat"
",out_no_instance AS no_instance"
+ ",out_no_cat AS no_cat"
" FROM merchant_do_insert_product"
"($1, $2, $3, $4::TEXT::JSONB, $5, $6, $7::TEXT::JSONB, $8"
- ",$9, $10::TEXT::JSONB, $11, $12, $13, $14);");
+ ",$9, $10, $11, $12, $13, $14::TEXT::JSONB, $15, $16, $17, $18);");
qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"insert_product",
params,
diff --git a/src/backenddb/pg_insert_product.sql b/src/backenddb/pg_insert_product.sql
@@ -25,7 +25,11 @@ CREATE FUNCTION merchant_do_insert_product (
IN in_image TEXT,
IN in_taxes JSONB,
IN in_price taler_amount_currency,
+ IN ina_price_list taler_amount_currency[],
IN in_total_stock INT8,
+ IN in_total_stock_frac INT4,
+ IN in_allow_fractional_quantity BOOL,
+ IN in_fractional_precision_level INT4,
IN in_address JSONB,
IN in_next_restock INT8,
IN in_minimum_age INT4,
@@ -41,6 +45,8 @@ DECLARE
my_product_serial INT8;
i INT8;
ini_cat INT8;
+ my_price taler_amount_currency;
+ my_price_array taler_amount_currency[];
BEGIN
-- Which instance are we using?
@@ -58,6 +64,14 @@ THEN
END IF;
out_no_instance=FALSE;
+IF COALESCE (array_length(ina_price_list,1),0) = 0
+THEN
+ my_price_array := ARRAY[in_price]::taler_amount_currency[];
+ELSE
+ my_price_array := ina_price_list;
+END IF;
+my_price := my_price_array[1];
+
INSERT INTO merchant_inventory
(merchant_serial
,product_id
@@ -69,7 +83,11 @@ INSERT INTO merchant_inventory
,image_hash
,taxes
,price
+ ,price_array
,total_stock
+ ,total_stock_frac
+ ,allow_fractional_quantity
+ ,fractional_precision_level
,address
,next_restock
,minimum_age
@@ -89,8 +107,12 @@ INSERT INTO merchant_inventory
'hex')
END
,in_taxes
- ,in_price
+ ,my_price
+ ,my_price_array
,in_total_stock
+ ,in_total_stock_frac
+ ,in_allow_fractional_quantity
+ ,in_fractional_precision_level
,in_address
,in_next_restock
,in_minimum_age)
@@ -113,8 +135,13 @@ THEN
AND unit=in_unit
AND image=in_image
AND taxes=in_taxes
- AND price=in_price
+ AND price=my_price
+ AND to_jsonb(COALESCE(price_array, ARRAY[]::taler_amount_currency[]))
+ = to_jsonb(COALESCE(my_price_array, ARRAY[]::taler_amount_currency[]))
AND total_stock=in_total_stock
+ AND total_stock_frac=in_total_stock_frac
+ AND allow_fractional_quantity=in_allow_fractional_quantity
+ AND fractional_precision_level=in_fractional_precision_level
AND address=in_address
AND next_restock=in_next_restock
AND minimum_age=in_minimum_age;
diff --git a/src/backenddb/pg_insert_unit.c b/src/backenddb/pg_insert_unit.c
@@ -0,0 +1,84 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_insert_unit.c
+ * @brief Implementation of the insert_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_insert_unit.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_insert_unit (void *cls,
+ const char *instance_id,
+ const struct TALER_MERCHANTDB_UnitDetails *ud,
+ bool *no_instance,
+ bool *conflict,
+ uint64_t *unit_serial)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_string (instance_id),
+ GNUNET_PQ_query_param_string (ud->unit),
+ GNUNET_PQ_query_param_string (ud->unit_name_long),
+ GNUNET_PQ_query_param_string (ud->unit_name_short),
+ TALER_PQ_query_param_json (ud->unit_name_long_i18n),
+ TALER_PQ_query_param_json (ud->unit_name_short_i18n),
+ GNUNET_PQ_query_param_bool (ud->unit_allow_fraction),
+ GNUNET_PQ_query_param_uint32 (&ud->unit_precision_level),
+ GNUNET_PQ_query_param_bool (ud->unit_active),
+ GNUNET_PQ_query_param_end
+ };
+ bool unit_serial_present = true;
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("no_instance",
+ no_instance),
+ GNUNET_PQ_result_spec_bool ("conflict",
+ conflict),
+ GNUNET_PQ_result_spec_allow_null (
+ GNUNET_PQ_result_spec_uint64 ("unit_serial",
+ unit_serial),
+ &unit_serial_present),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ *no_instance = false;
+ *conflict = false;
+
+ check_connection (pg);
+ PREPARE (pg,
+ "insert_unit",
+ "SELECT"
+ " out_no_instance AS no_instance"
+ " ,out_conflict AS conflict"
+ " ,out_unit_serial AS unit_serial"
+ " FROM merchant_do_insert_unit"
+ " ($1,$2,$3,$4,$5,$6,$7,$8,$9);");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "insert_unit",
+ params,
+ rs);
+ GNUNET_PQ_cleanup_query_params_closures (params);
+ if (! unit_serial_present)
+ *unit_serial = 0;
+ return qs;
+}
diff --git a/src/backenddb/pg_insert_unit.h b/src/backenddb/pg_insert_unit.h
@@ -0,0 +1,47 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_insert_unit.h
+ * @brief implementation of the insert_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef PG_INSERT_UNIT_H
+#define PG_INSERT_UNIT_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+/**
+ * Insert a measurement unit definition.
+ *
+ * @param cls closure
+ * @param instance_id instance to insert unit for
+ * @param ud unit definition to store (unit_serial ignored)
+ * @param[out] no_instance set to true if the instance is unknown
+ * @param[out] conflict set to true if a conflicting unit already exists
+ * @param[out] unit_serial set to the generated serial on success (or the existing serial for idempotent inserts)
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_insert_unit (void *cls,
+ const char *instance_id,
+ const struct TALER_MERCHANTDB_UnitDetails *ud,
+ bool *no_instance,
+ bool *conflict,
+ uint64_t *unit_serial);
+
+#endif
diff --git a/src/backenddb/pg_lock_product.c b/src/backenddb/pg_lock_product.c
@@ -31,6 +31,7 @@ TMH_PG_lock_product (void *cls,
const char *product_id,
const struct GNUNET_Uuid *uuid,
uint64_t quantity,
+ uint32_t quantity_frac,
struct GNUNET_TIME_Timestamp expiration_time)
{
struct PostgresClosure *pg = cls;
@@ -39,6 +40,7 @@ TMH_PG_lock_product (void *cls,
GNUNET_PQ_query_param_string (product_id),
GNUNET_PQ_query_param_auto_from_type (uuid),
GNUNET_PQ_query_param_uint64 (&quantity),
+ GNUNET_PQ_query_param_uint32 (&quantity_frac),
GNUNET_PQ_query_param_timestamp (&expiration_time),
GNUNET_PQ_query_param_end
};
@@ -54,22 +56,46 @@ TMH_PG_lock_product (void *cls,
" (SELECT merchant_serial"
" FROM merchant_instances"
" WHERE merchant_id=$1))"
+ ",tmp AS"
+ " (SELECT"
+ " mi.product_serial"
+ " ,mi.total_stock"
+ " ,mi.total_stock_frac"
+ " ,mi.total_sold"
+ " ,mi.total_sold_frac"
+ " ,mi.total_lost"
+ " ,mi.total_lost_frac"
+ " ,mi.allow_fractional_quantity"
+ " FROM merchant_inventory mi"
+ " JOIN ps USING (product_serial))"
"INSERT INTO merchant_inventory_locks"
"(product_serial"
",lock_uuid"
",total_locked"
+ ",total_locked_frac"
",expiration)"
- " SELECT product_serial, $3, $4, $5"
- " FROM merchant_inventory"
- " JOIN ps USING (product_serial)"
- " WHERE "
- " total_stock - total_sold - total_lost - $4 >= "
- " (SELECT COALESCE(SUM(total_locked), 0)"
- " FROM merchant_inventory_locks"
- " WHERE product_serial=ps.product_serial) + "
- " (SELECT COALESCE(SUM(total_locked), 0)"
- " FROM merchant_order_locks"
- " WHERE product_serial=ps.product_serial)");
+ " SELECT tmp.product_serial, $3, $4::INT8, $5::INT4, $6"
+ " FROM tmp"
+ " WHERE (tmp.allow_fractional_quantity OR $5 = 0)"
+ " AND (tmp.total_stock = 9223372036854775807"
+ " OR ("
+ " (tmp.total_stock::NUMERIC * 1000000"
+ " + tmp.total_stock_frac::NUMERIC)"
+ " - (tmp.total_sold::NUMERIC * 1000000"
+ " + tmp.total_sold_frac::NUMERIC)"
+ " - (tmp.total_lost::NUMERIC * 1000000"
+ " + tmp.total_lost_frac::NUMERIC)"
+ " >= "
+ " (($4::NUMERIC * 1000000) + $5::NUMERIC)"
+ " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000"
+ " + total_locked_frac::NUMERIC), 0)"
+ " FROM merchant_inventory_locks mil"
+ " WHERE mil.product_serial = tmp.product_serial)"
+ " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000"
+ " + total_locked_frac::NUMERIC), 0)"
+ " FROM merchant_order_locks mol"
+ " WHERE mol.product_serial = tmp.product_serial)"
+ " ))");
return GNUNET_PQ_eval_prepared_non_select (pg->conn,
"lock_product",
params);
diff --git a/src/backenddb/pg_lock_product.h b/src/backenddb/pg_lock_product.h
@@ -34,6 +34,8 @@
* @param product_id product to lookup
* @param uuid the UUID that holds the lock
* @param quantity how many units should be locked
+ * @param quantity_frac fractional component of quantity in units of
+ * 1/100000000 of the base value
* @param expiration_time when should the lock expire
* @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the
* product is unknown OR if there insufficient stocks remaining
@@ -44,6 +46,7 @@ TMH_PG_lock_product (void *cls,
const char *product_id,
const struct GNUNET_Uuid *uuid,
uint64_t quantity,
+ uint32_t quantity_frac,
struct GNUNET_TIME_Timestamp expiration_time);
#endif
diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c
@@ -90,14 +90,28 @@ lookup_products_cb (void *cls,
&pd.unit),
TALER_PQ_result_spec_amount_with_currency ("price",
&pd.price),
+ TALER_PQ_result_spec_array_amount_with_currency (pg->conn,
+ "price_array",
+ &pd.price_array_length,
+ &pd.price_array),
TALER_PQ_result_spec_json ("taxes",
&pd.taxes),
GNUNET_PQ_result_spec_uint64 ("total_stock",
&pd.total_stock),
+ GNUNET_PQ_result_spec_uint32 ("total_stock_frac",
+ &pd.total_stock_frac),
+ GNUNET_PQ_result_spec_bool ("allow_fractional_quantity",
+ &pd.allow_fractional_quantity),
+ GNUNET_PQ_result_spec_uint32 ("fractional_precision_level",
+ &pd.fractional_precision_level),
GNUNET_PQ_result_spec_uint64 ("total_sold",
&pd.total_sold),
+ GNUNET_PQ_result_spec_uint32 ("total_sold_frac",
+ &pd.total_sold_frac),
GNUNET_PQ_result_spec_uint64 ("total_lost",
&pd.total_lost),
+ GNUNET_PQ_result_spec_uint32 ("total_lost_frac",
+ &pd.total_lost_frac),
GNUNET_PQ_result_spec_string ("image",
&pd.image),
TALER_PQ_result_spec_json ("address",
@@ -162,10 +176,16 @@ TMH_PG_lookup_all_products (void *cls,
",product_name"
",unit"
",price"
+ ",price_array"
",taxes::TEXT"
",total_stock"
+ ",total_stock_frac"
+ ",allow_fractional_quantity"
+ ",fractional_precision_level"
",total_sold"
+ ",total_sold_frac"
",total_lost"
+ ",total_lost_frac"
",image"
",minv.address::TEXT"
",next_restock"
diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c
@@ -49,10 +49,16 @@ TMH_PG_lookup_product (void *cls,
",mi.product_name"
",mi.unit"
",mi.price"
+ ",mi.price_array"
",mi.taxes::TEXT"
- ",mi.total_stock"
+ ",mi.total_stock"
+ ",mi.total_stock_frac"
+ ",mi.allow_fractional_quantity"
+ ",mi.fractional_precision_level"
",mi.total_sold"
+ ",mi.total_sold_frac"
",mi.total_lost"
+ ",mi.total_lost_frac"
",mi.image"
",mi.address::TEXT"
",mi.next_restock"
@@ -93,6 +99,8 @@ TMH_PG_lookup_product (void *cls,
json_t *my_address = NULL;
json_t *my_taxes = NULL;
uint64_t *my_categories = NULL;
+ struct TALER_Amount *my_price_array = NULL;
+ size_t my_price_array_length = 0;
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_string ("description",
&my_description),
@@ -104,14 +112,28 @@ TMH_PG_lookup_product (void *cls,
&my_unit),
TALER_PQ_result_spec_amount_with_currency ("price",
&pd->price),
+ TALER_PQ_result_spec_array_amount_with_currency (pg->conn,
+ "price_array",
+ &my_price_array_length,
+ &my_price_array),
TALER_PQ_result_spec_json ("taxes",
&my_taxes),
GNUNET_PQ_result_spec_uint64 ("total_stock",
&pd->total_stock),
+ GNUNET_PQ_result_spec_uint32 ("total_stock_frac",
+ &pd->total_stock_frac),
+ GNUNET_PQ_result_spec_bool ("allow_fractional_quantity",
+ &pd->allow_fractional_quantity),
+ GNUNET_PQ_result_spec_uint32 ("fractional_precision_level",
+ &pd->fractional_precision_level),
GNUNET_PQ_result_spec_uint64 ("total_sold",
&pd->total_sold),
+ GNUNET_PQ_result_spec_uint32 ("total_sold_frac",
+ &pd->total_sold_frac),
GNUNET_PQ_result_spec_uint64 ("total_lost",
&pd->total_lost),
+ GNUNET_PQ_result_spec_uint32 ("total_lost_frac",
+ &pd->total_lost_frac),
GNUNET_PQ_result_spec_string ("image",
&my_image),
TALER_PQ_result_spec_json ("address",
@@ -140,6 +162,8 @@ TMH_PG_lookup_product (void *cls,
pd->taxes = my_taxes;
pd->image = my_image;
pd->address = my_address;
+ pd->price_array = my_price_array;
+ pd->price_array_length = my_price_array_length;
*categories = my_categories;
/* Clear original pointers to that cleanup_result doesn't squash them */
my_name = NULL;
@@ -149,6 +173,8 @@ TMH_PG_lookup_product (void *cls,
my_taxes = NULL;
my_image = NULL;
my_address = NULL;
+ my_price_array = NULL;
+ my_price_array_length = 0;
my_categories = NULL;
GNUNET_PQ_cleanup_result (rs);
return qs;
diff --git a/src/backenddb/pg_lookup_units.c b/src/backenddb/pg_lookup_units.c
@@ -0,0 +1,152 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_lookup_units.c
+ * @brief Implementation of the lookup_units function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_lookup_units.h"
+#include "pg_helper.h"
+
+
+/**
+ * Context used for TMH_PG_lookup_units().
+ */
+struct LookupUnitsContext
+{
+ TALER_MERCHANTDB_UnitsCallback cb;
+ void *cb_cls;
+ bool extract_failed;
+};
+
+
+static void
+lookup_units_cb (void *cls,
+ PGresult *result,
+ unsigned int num_results)
+{
+ struct LookupUnitsContext *luc = cls;
+
+ for (unsigned int i = 0; i<num_results; i++)
+ {
+ struct TALER_MERCHANTDB_UnitDetails ud = { 0 };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint64 ("unit_serial",
+ &ud.unit_serial),
+ GNUNET_PQ_result_spec_string ("unit",
+ &ud.unit),
+ GNUNET_PQ_result_spec_string ("unit_name_long",
+ &ud.unit_name_long),
+ GNUNET_PQ_result_spec_string ("unit_name_short",
+ &ud.unit_name_short),
+ TALER_PQ_result_spec_json ("unit_name_long_i18n",
+ &ud.unit_name_long_i18n),
+ TALER_PQ_result_spec_json ("unit_name_short_i18n",
+ &ud.unit_name_short_i18n),
+ GNUNET_PQ_result_spec_bool ("unit_allow_fraction",
+ &ud.unit_allow_fraction),
+ GNUNET_PQ_result_spec_uint32 ("unit_precision_level",
+ &ud.unit_precision_level),
+ GNUNET_PQ_result_spec_bool ("unit_active",
+ &ud.unit_active),
+ GNUNET_PQ_result_spec_bool ("unit_builtin",
+ &ud.unit_builtin),
+ GNUNET_PQ_result_spec_end
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_extract_result (result,
+ rs,
+ i))
+ {
+ GNUNET_break (0);
+ luc->extract_failed = true;
+ return;
+ }
+ luc->cb (luc->cb_cls,
+ ud.unit_serial,
+ &ud);
+ GNUNET_PQ_cleanup_result (rs);
+ }
+}
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_lookup_units (void *cls,
+ const char *instance_id,
+ TALER_MERCHANTDB_UnitsCallback cb,
+ void *cb_cls)
+{
+ struct PostgresClosure *pg = cls;
+ struct LookupUnitsContext luc = {
+ .cb = cb,
+ .cb_cls = cb_cls,
+ .extract_failed = false
+ };
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_string (instance_id),
+ GNUNET_PQ_query_param_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ PREPARE (pg,
+ "lookup_units",
+ "WITH mi AS ("
+ " SELECT merchant_serial FROM merchant_instances WHERE merchant_id=$1"
+ ")"
+ "SELECT cu.unit_serial"
+ " ,cu.unit"
+ " ,cu.unit_name_long"
+ " ,cu.unit_name_short"
+ " ,cu.unit_name_long_i18n"
+ " ,cu.unit_name_short_i18n"
+ " ,cu.unit_allow_fraction"
+ " ,cu.unit_precision_level"
+ " ,cu.unit_active"
+ " ,FALSE AS unit_builtin"
+ " FROM merchant_custom_units cu"
+ " JOIN mi ON cu.merchant_serial = mi.merchant_serial"
+ " UNION ALL "
+ "SELECT bu.unit_serial"
+ " ,bu.unit"
+ " ,bu.unit_name_long"
+ " ,bu.unit_name_short"
+ " ,bu.unit_name_long_i18n"
+ " ,bu.unit_name_short_i18n"
+ " ,COALESCE(bo.override_allow_fraction, bu.unit_allow_fraction)"
+ " ,COALESCE(bo.override_precision_level, bu.unit_precision_level)"
+ " ,COALESCE(bo.override_active, bu.unit_active)"
+ " ,TRUE AS unit_builtin"
+ " FROM merchant_builtin_units bu"
+ " JOIN mi ON TRUE"
+ " LEFT JOIN merchant_builtin_unit_overrides bo"
+ " ON bo.builtin_unit_serial = bu.unit_serial"
+ " AND bo.merchant_serial = mi.merchant_serial"
+ " ORDER BY unit");
+ qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
+ "lookup_units",
+ params,
+ &lookup_units_cb,
+ &luc);
+ if (luc.extract_failed)
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ return qs;
+}
diff --git a/src/backenddb/pg_lookup_units.h b/src/backenddb/pg_lookup_units.h
@@ -0,0 +1,43 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_lookup_units.h
+ * @brief implementation of the lookup_units function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef PG_LOOKUP_UNITS_H
+#define PG_LOOKUP_UNITS_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+/**
+ * Lookup all measurement units for an instance.
+ *
+ * @param cls closure
+ * @param instance_id instance to fetch units for
+ * @param cb function to call with each unit
+ * @param cb_cls closure for @a cb
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_lookup_units (void *cls,
+ const char *instance_id,
+ TALER_MERCHANTDB_UnitsCallback cb,
+ void *cb_cls);
+
+#endif
diff --git a/src/backenddb/pg_mark_contract_paid.c b/src/backenddb/pg_mark_contract_paid.c
@@ -78,8 +78,14 @@ TMH_PG_mark_contract_paid (
PREPARE (pg,
"mark_inventory_sold",
"UPDATE merchant_inventory SET"
- " total_sold=total_sold + order_locks.total_locked"
- " FROM (SELECT total_locked,product_serial"
+ " total_sold = total_sold"
+ " + order_locks.total_locked"
+ " + ((merchant_inventory.total_sold_frac::BIGINT"
+ " + order_locks.total_locked_frac::BIGINT) / 100000000)"
+ " ,total_sold_frac ="
+ " ((merchant_inventory.total_sold_frac::BIGINT"
+ " + order_locks.total_locked_frac::BIGINT) % 100000000)::INT4"
+ " FROM (SELECT total_locked,total_locked_frac,product_serial"
" FROM merchant_order_locks"
" WHERE order_serial="
" (SELECT order_serial"
diff --git a/src/backenddb/pg_select_unit.c b/src/backenddb/pg_select_unit.c
@@ -0,0 +1,135 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_select_unit.c
+ * @brief Implementation of the select_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_select_unit.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_select_unit (void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ struct TALER_MERCHANTDB_UnitDetails *ud)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_string (instance_id),
+ GNUNET_PQ_query_param_string (unit_id),
+ GNUNET_PQ_query_param_end
+ };
+
+ if (NULL == ud)
+ {
+ struct GNUNET_PQ_ResultSpec rs_null[] = {
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+ return GNUNET_PQ_eval_prepared_singleton_select (
+ pg->conn,
+ "select_unit",
+ params,
+ rs_null);
+ }
+ else
+ {
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint64 ("unit_serial",
+ &ud->unit_serial),
+ GNUNET_PQ_result_spec_string ("unit",
+ &ud->unit),
+ GNUNET_PQ_result_spec_string ("unit_name_long",
+ &ud->unit_name_long),
+ GNUNET_PQ_result_spec_string ("unit_name_short",
+ &ud->unit_name_short),
+ TALER_PQ_result_spec_json ("unit_name_long_i18n",
+ &ud->unit_name_long_i18n),
+ TALER_PQ_result_spec_json ("unit_name_short_i18n",
+ &ud->unit_name_short_i18n),
+ GNUNET_PQ_result_spec_bool ("unit_allow_fraction",
+ &ud->unit_allow_fraction),
+ GNUNET_PQ_result_spec_uint32 ("unit_precision_level",
+ &ud->unit_precision_level),
+ GNUNET_PQ_result_spec_bool ("unit_active",
+ &ud->unit_active),
+ GNUNET_PQ_result_spec_bool ("unit_builtin",
+ &ud->unit_builtin),
+ GNUNET_PQ_result_spec_end
+ };
+
+ check_connection (pg);
+
+ PREPARE (pg,
+ "select_unit_custom",
+ "SELECT"
+ " cu.unit_serial"
+ " ,cu.unit"
+ " ,cu.unit_name_long"
+ " ,cu.unit_name_short"
+ " ,cu.unit_name_long_i18n"
+ " ,cu.unit_name_short_i18n"
+ " ,cu.unit_allow_fraction"
+ " ,cu.unit_precision_level"
+ " ,cu.unit_active"
+ " ,FALSE AS unit_builtin"
+ " FROM merchant_custom_units cu"
+ " JOIN merchant_instances inst"
+ " USING (merchant_serial)"
+ " WHERE inst.merchant_id=$1"
+ " AND cu.unit=$2");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "select_unit_custom",
+ params,
+ rs);
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs)
+ return qs;
+
+ PREPARE (pg,
+ "select_unit_builtin",
+ "SELECT"
+ " bu.unit_serial"
+ " ,bu.unit"
+ " ,bu.unit_name_long"
+ " ,bu.unit_name_short"
+ " ,bu.unit_name_long_i18n"
+ " ,bu.unit_name_short_i18n"
+ " ,COALESCE(bo.override_allow_fraction, bu.unit_allow_fraction)"
+ " ,COALESCE(bo.override_precision_level, bu.unit_precision_level)"
+ " ,COALESCE(bo.override_active, bu.unit_active)"
+ " ,TRUE AS unit_builtin"
+ " FROM merchant_builtin_units bu"
+ " JOIN merchant_instances inst"
+ " ON TRUE"
+ " LEFT JOIN merchant_builtin_unit_overrides bo"
+ " ON bo.builtin_unit_serial = bu.unit_serial"
+ " AND bo.merchant_serial = inst.merchant_serial"
+ " WHERE inst.merchant_id=$1"
+ " AND bu.unit=$2");
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "select_unit_builtin",
+ params,
+ rs);
+ }
+}
diff --git a/src/backenddb/pg_select_unit.h b/src/backenddb/pg_select_unit.h
@@ -0,0 +1,43 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_select_unit.h
+ * @brief implementation of the select_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef PG_SELECT_UNIT_H
+#define PG_SELECT_UNIT_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+/**
+ * Lookup a measurement unit by identifier.
+ *
+ * @param cls closure
+ * @param instance_id instance owning the unit
+ * @param unit_id symbolic identifier
+ * @param[out] ud unit details on success; may be NULL to test for existence
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_select_unit (void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ struct TALER_MERCHANTDB_UnitDetails *ud);
+
+#endif
diff --git a/src/backenddb/pg_update_product.c b/src/backenddb/pg_update_product.c
@@ -52,7 +52,14 @@ TMH_PG_update_product (void *cls,
TALER_PQ_query_param_json (pd->taxes),
TALER_PQ_query_param_amount_with_currency (pg->conn,
&pd->price), /* $8 */
- GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */
+ TALER_PQ_query_param_array_amount_with_currency (
+ pd->price_array_length,
+ pd->price_array,
+ pg->conn), /* $9 */
+ GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $10 */
+ GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $11 */
+ GNUNET_PQ_query_param_bool (pd->allow_fractional_quantity), /* $12 */
+ GNUNET_PQ_query_param_uint32 (&pd->fractional_precision_level),
GNUNET_PQ_query_param_uint64 (&pd->total_lost),
TALER_PQ_query_param_json (pd->address),
GNUNET_PQ_query_param_timestamp (&pd->next_restock),
@@ -103,7 +110,7 @@ TMH_PG_update_product (void *cls,
",out_no_instance AS no_instance"
" FROM merchant_do_update_product"
"($1,$2,$3,$4::TEXT::JSONB,$5,$6,$7::TEXT::JSONB,$8,$9"
- ",$10,$11::TEXT::JSONB,$12,$13,$14,$15);");
+ ",$10,$11,$12,$13,$14,$15::TEXT::JSONB,$16,$17,$18,$19);");
qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"update_product",
params,
diff --git a/src/backenddb/pg_update_product.sql b/src/backenddb/pg_update_product.sql
@@ -25,7 +25,11 @@ CREATE FUNCTION merchant_do_update_product (
IN in_image TEXT,
IN in_taxes JSONB,
IN in_price taler_amount_currency,
+ IN ina_price_list taler_amount_currency[],
IN in_total_stock INT8,
+ IN in_total_stock_frac INT4,
+ IN in_allow_fractional_quantity BOOL,
+ IN in_fractional_precision_level INT4,
IN in_total_lost INT8,
IN in_address JSONB,
IN in_next_restock INT8,
@@ -46,6 +50,8 @@ DECLARE
i INT8;
ini_cat INT8;
rec RECORD;
+ my_price taler_amount_currency;
+ my_price_array taler_amount_currency[];
BEGIN
out_no_instance=FALSE;
@@ -69,7 +75,9 @@ END IF;
-- Check existing entry satisfies constraints
SELECT total_stock
+ ,total_stock_frac
,total_lost
+ ,allow_fractional_quantity
,product_serial
INTO rec
FROM merchant_inventory
@@ -84,6 +92,14 @@ END IF;
my_product_serial = rec.product_serial;
+IF COALESCE (array_length(ina_price_list,1),0) = 0
+THEN
+ my_price_array := ARRAY[in_price]::taler_amount_currency[];
+ELSE
+ my_price_array := ina_price_list;
+END IF;
+my_price := my_price_array[1];
+
IF rec.total_stock > in_total_stock
THEN
out_stocked_reduced=TRUE;
@@ -95,6 +111,14 @@ THEN
out_lost_reduced=TRUE;
RETURN;
END IF;
+IF rec.allow_fractional_quantity
+ AND (NOT in_allow_fractional_quantity)
+THEN
+ DELETE
+ FROM merchant_inventory_locks
+ WHERE product_serial = my_product_serial
+ AND total_locked_frac <> 0;
+END IF;
-- Remove old categories
DELETE FROM merchant_product_categories
@@ -134,8 +158,12 @@ UPDATE merchant_inventory SET
'hex')
END
,taxes=in_taxes
- ,price=in_price
+ ,price=my_price
+ ,price_array=my_price_array
,total_stock=in_total_stock
+ ,total_stock_frac=in_total_stock_frac
+ ,allow_fractional_quantity=in_allow_fractional_quantity
+ ,fractional_precision_level=in_fractional_precision_level
,total_lost=in_total_lost
,address=in_address
,next_restock=in_next_restock
diff --git a/src/backenddb/pg_update_unit.c b/src/backenddb/pg_update_unit.c
@@ -0,0 +1,97 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_update_unit.c
+ * @brief Implementation of the update_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_update_unit.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_update_unit (void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ const char *unit_name_long,
+ const json_t *unit_name_long_i18n,
+ const char *unit_name_short,
+ const json_t *unit_name_short_i18n,
+ const bool *unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ const bool *unit_active,
+ bool *no_instance,
+ bool *no_unit,
+ bool *builtin_conflict)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_string (instance_id),
+ GNUNET_PQ_query_param_string (unit_id),
+ (NULL == unit_name_long)
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_string (unit_name_long),
+ (NULL == unit_name_long_i18n)
+ ? GNUNET_PQ_query_param_null ()
+ : TALER_PQ_query_param_json ((json_t *) unit_name_long_i18n),
+ (NULL == unit_name_short)
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_string (unit_name_short),
+ (NULL == unit_name_short_i18n)
+ ? GNUNET_PQ_query_param_null ()
+ : TALER_PQ_query_param_json ((json_t *) unit_name_short_i18n),
+ (NULL == unit_allow_fraction)
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_bool (*unit_allow_fraction),
+ (NULL == unit_precision_level)
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_uint32 ((uint32_t *) unit_precision_level),
+ (NULL == unit_active)
+ ? GNUNET_PQ_query_param_null ()
+ : GNUNET_PQ_query_param_bool (*unit_active),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_bool ("out_no_instance",
+ no_instance),
+ GNUNET_PQ_result_spec_bool ("out_no_unit",
+ no_unit),
+ GNUNET_PQ_result_spec_bool ("out_builtin_conflict",
+ builtin_conflict),
+ GNUNET_PQ_result_spec_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ check_connection (pg);
+ PREPARE (pg,
+ "update_unit",
+ "SELECT"
+ " out_no_instance"
+ " ,out_no_unit"
+ " ,out_builtin_conflict"
+ " FROM merchant_do_update_unit"
+ "($1,$2,$3,$4,$5,$6,$7,$8,$9);");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "update_unit",
+ params,
+ rs);
+ GNUNET_PQ_cleanup_query_params_closures (params);
+ return qs;
+}
diff --git a/src/backenddb/pg_update_unit.h b/src/backenddb/pg_update_unit.h
@@ -0,0 +1,61 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+ */
+/**
+ * @file backenddb/pg_update_unit.h
+ * @brief implementation of the update_unit function for Postgres
+ * @author Bohdan Potuzhnyi
+ */
+#ifndef PG_UPDATE_UNIT_H
+#define PG_UPDATE_UNIT_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+/**
+ * Update an existing measurement unit definition.
+ *
+ * @param cls closure
+ * @param instance_id instance owning the unit
+ * @param unit_id symbolic unit identifier
+ * @param unit_name_long optional new long name (NULL to keep existing)
+ * @param unit_name_long_i18n optional new long-name translations
+ * @param unit_name_short optional new short name
+ * @param unit_name_short_i18n optional new short-name translations
+ * @param unit_allow_fraction optional new fractional toggle
+ * @param unit_precision_level optional new fractional precision
+ * @param unit_active optional new visibility flag
+ * @param[out] no_instance set if instance unknown
+ * @param[out] no_unit set if unit unknown
+ * @param[out] builtin_conflict set if immutable builtin fields touched
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_update_unit (void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ const char *unit_name_long,
+ const json_t *unit_name_long_i18n,
+ const char *unit_name_short,
+ const json_t *unit_name_short_i18n,
+ const bool *unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ const bool *unit_active,
+ bool *no_instance,
+ bool *no_unit,
+ bool *builtin_conflict);
+
+#endif
diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c
@@ -50,10 +50,15 @@
#include "pg_lookup_transfers.h"
#include "pg_lookup_pending_deposits.h"
#include "pg_lookup_categories.h"
+#include "pg_lookup_units.h"
#include "pg_select_category.h"
#include "pg_update_category.h"
#include "pg_insert_category.h"
#include "pg_delete_category.h"
+#include "pg_select_unit.h"
+#include "pg_insert_unit.h"
+#include "pg_update_unit.h"
+#include "pg_delete_unit.h"
#include "pg_update_wirewatch_progress.h"
#include "pg_select_wirewatch_accounts.h"
#include "pg_select_open_transfers.h"
@@ -622,6 +627,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls)
= &TMH_PG_update_pending_webhook;
plugin->lookup_categories
= &TMH_PG_lookup_categories;
+ plugin->lookup_units
+ = &TMH_PG_lookup_units;
plugin->select_category_by_name
= &TMH_PG_select_category_by_name;
plugin->get_kyc_status
@@ -632,12 +639,20 @@ libtaler_plugin_merchantdb_postgres_init (void *cls)
= &TMH_PG_get_kyc_limits;
plugin->select_category
= &TMH_PG_select_category;
+ plugin->select_unit
+ = &TMH_PG_select_unit;
plugin->update_category
= &TMH_PG_update_category;
+ plugin->update_unit
+ = &TMH_PG_update_unit;
plugin->insert_category
= &TMH_PG_insert_category;
+ plugin->insert_unit
+ = &TMH_PG_insert_unit;
plugin->delete_category
= &TMH_PG_delete_category;
+ plugin->delete_unit
+ = &TMH_PG_delete_unit;
plugin->delete_exchange_accounts
= &TMH_PG_delete_exchange_accounts;
plugin->select_accounts_by_exchange
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -44,28 +44,28 @@ static struct TALER_MERCHANTDB_Plugin *plugin;
* @param test 0 on success, non-zero on failure
*/
#define TEST_WITH_FAIL_CLAUSE(test, on_fail) \
- if ((test)) \
- { \
- GNUNET_break (0); \
- on_fail \
- }
+ if ((test)) \
+ { \
+ GNUNET_break (0); \
+ on_fail \
+ }
#define TEST_COND_RET_ON_FAIL(cond, msg) \
- if (! (cond)) \
- { \
- GNUNET_break (0); \
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \
- msg); \
- return 1; \
- }
+ if (! (cond)) \
+ { \
+ GNUNET_break (0); \
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \
+ msg); \
+ return 1; \
+ }
/**
* @param __test 0 on success, non-zero on failure
*/
#define TEST_RET_ON_FAIL(__test) \
- TEST_WITH_FAIL_CLAUSE (__test, \
- return 1; \
- )
+ TEST_WITH_FAIL_CLAUSE (__test, \
+ return 1; \
+ )
/* ********** Instances ********** */
@@ -717,6 +717,9 @@ static void
make_product (const char *id,
struct ProductData *product)
{
+ memset (product,
+ 0,
+ sizeof (*product));
product->id = id;
product->product.product_name = "Test product";
product->product.description = "This is a test product";
@@ -1326,6 +1329,7 @@ run_test_products (struct TestProducts_Closure *cls)
cls->products[0].id,
&uuid,
256,
+ 0,
refund_deadline))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
@@ -1337,6 +1341,7 @@ run_test_products (struct TestProducts_Closure *cls)
cls->products[0].id,
&uuid,
1,
+ 0,
refund_deadline))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
@@ -2247,7 +2252,8 @@ run_test_orders (struct TestOrders_Closure *cls)
cls->instance.instance.id,
cls->orders[0].id,
cls->product.id,
- 5))
+ 5,
+ 0))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Insert order lock failed\n");
diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h
@@ -1622,6 +1622,16 @@ struct TALER_MERCHANT_ProductGetResponse
struct TALER_Amount price;
/**
+ * Optional list of price tiers as provided by the backend.
+ */
+ const struct TALER_Amount *unit_price;
+
+ /**
+ * Size of unit_price array
+ */
+ size_t unit_price_len;
+
+ /**
* base64-encoded product image, can be NULL if none is set.
*/
const char *image;
@@ -1640,6 +1650,21 @@ struct TALER_MERCHANT_ProductGetResponse
int64_t total_stock;
/**
+ * Set to true if fractional quantities are allowed for this product.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Suggested fractional precision for fractional quantities.
+ */
+ uint32_t unit_precision_level;
+
+ /**
+ * Stock level encoded as a decimal string. Preferred source of truth for fractional stock.
+ */
+ const char *unit_total_stock;
+
+ /**
* in @e units, total number of @e unit of product sold
*/
uint64_t total_sold;
@@ -1942,6 +1967,61 @@ TALER_MERCHANT_products_post3 (
/**
+ * Make a POST /products request to add a product to the
+ * inventory.
+ *
+ * @param ctx the context
+ * @param backend_url HTTP base URL for the backend
+ * @param product_id identifier to use for the product
+ * @param description description of the product
+ * @param description_i18n Map from IETF BCP 47 language tags to localized descriptions
+ * @param unit unit in which the product is measured (liters, kilograms, packages, etc.)
+ * @param unit_prices array of price tiers (at least one entry)
+ * @param unit_price_len length of @a unit_prices
+ * @param image base64-encoded product image
+ * @param taxes list of taxes paid by the merchant
+ * @param total_stock integer quantity to use when @a unit_allow_fraction is false;
+ * set to -1 for unlimited stock
+ * @param total_stock_frac fractional component (0-#MERCHANT_UNIT_FRAC_BASE) to use when
+ * @a unit_allow_fraction is true; ignored otherwise. Clients should use 0
+ * when there is no fractional part or when @a total_stock is -1 (unlimited).
+ * @param unit_allow_fraction whether fractional quantity purchases are allowed
+ * @param unit_precision_level optional override for the fractional precision; pass NULL to use the default derived from @a unit
+ * @param address where the product is in stock
+ * @param next_restock when the next restocking is expected to happen, 0 for unknown
+ * @param minimum_age minimum age the buyer must have
+ * @param num_cats length of the @a cats array
+ * @param cats array of categories the product is in
+ * @param cb function to call with the backend's result
+ * @param cb_cls closure for @a cb
+ * @return the request handle; NULL upon error
+ */
+struct TALER_MERCHANT_ProductsPostHandle *
+TALER_MERCHANT_products_post4 (
+ struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *product_id,
+ const char *description,
+ const json_t *description_i18n,
+ const char *unit,
+ const struct TALER_Amount *unit_prices,
+ size_t unit_price_len,
+ const char *image,
+ const json_t *taxes,
+ int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ const json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ uint32_t minimum_age,
+ unsigned int num_cats,
+ const uint64_t *cats,
+ TALER_MERCHANT_ProductsPostCallback cb,
+ void *cb_cls);
+
+
+/**
* Cancel POST /products operation.
*
* @param pph operation to cancel
@@ -2018,6 +2098,55 @@ TALER_MERCHANT_product_patch (
/**
+ * Make a PATCH /products request to update a product using the
+ * extended inventory fields.
+ *
+ * @param ctx the context
+ * @param backend_url HTTP base URL for the backend
+ * @param product_id identifier of the product to modify
+ * @param description description of the product
+ * @param description_i18n localized descriptions
+ * @param unit sales unit
+ * @param unit_prices array of price tiers (at least one entry)
+ * @param unit_price_len number of entries in @a unit_prices
+ * @param image base64-encoded image
+ * @param taxes list of taxes
+ * @param total_stock integer quantity when @a unit_allow_fraction is false
+ * @param total_stock_frac fractional quantity component (0-#MERCHANT_UNIT_FRAC_BASE) when
+ * @a unit_allow_fraction is true; ignored otherwise
+ * @param unit_allow_fraction whether fractional quantity purchases are allowed
+ * @param unit_precision_level optional override for the fractional precision; pass NULL to use the default derived from @a unit
+ * @param total_lost stock lost so far
+ * @param address inventory address
+ * @param next_restock expected restock date
+ * @param cb function to call with the backend's result
+ * @param cb_cls closure for @a cb
+ * @return the request handle; NULL upon error
+ */
+struct TALER_MERCHANT_ProductPatchHandle *
+TALER_MERCHANT_product_patch2 (
+ struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *product_id,
+ const char *description,
+ const json_t *description_i18n,
+ const char *unit,
+ const struct TALER_Amount *unit_prices,
+ size_t unit_price_len,
+ const char *image,
+ const json_t *taxes,
+ int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ uint64_t total_lost,
+ const json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ TALER_MERCHANT_ProductPatchCallback cb,
+ void *cb_cls);
+
+
+/**
* Cancel PATCH /products operation.
*
* @param pph operation to cancel
@@ -2054,12 +2183,34 @@ typedef void
* @param product_id identifier of the product
* @param uuid UUID that identifies the client holding the lock
* @param duration how long should the lock be held
- * @param quantity how much product should be locked
+ * @param quantity how much product should be locked (integer part)
+ * @param quantity_frac fractional component to lock when
+ * @a use_fractional_quantity is true; value is expressed in units of
+ * 1/100000000 of the base unit
+ * @param use_fractional_quantity set to true when @a quantity_frac is used
* @param cb function to call with the backend's lock status
* @param cb_cls closure for @a cb
* @return the request handle; NULL upon error
*/
struct TALER_MERCHANT_ProductLockHandle *
+TALER_MERCHANT_product_lock2 (
+ struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *product_id,
+ const char *uuid,
+ struct GNUNET_TIME_Relative duration,
+ uint64_t quantity,
+ uint32_t quantity_frac,
+ bool use_fractional_quantity,
+ TALER_MERCHANT_ProductLockCallback cb,
+ void *cb_cls);
+
+
+/**
+ * Legacy helper that locks products using integer quantities only.
+ * Prefer using #TALER_MERCHANT_product_lock2 for fractional quantities.
+ */
+struct TALER_MERCHANT_ProductLockHandle *
TALER_MERCHANT_product_lock (
struct GNUNET_CURL_Context *ctx,
const char *backend_url,
@@ -2397,11 +2548,24 @@ struct TALER_MERCHANT_PostOrdersReply
uint64_t requested_quantity;
/**
+ * Fractional component of the requested quantity in units of
+ * 1/#TALER_AMOUNT_FRAC_BASE. Absent or zero when the request did not
+ * include a fractional part.
+ */
+ uint32_t requested_quantity_frac;
+
+ /**
* How many units are actually still in stock.
*/
uint64_t available_quantity;
/**
+ * Fractional component of the available quantity in units of
+ * 1/#TALER_AMOUNT_FRAC_BASE.
+ */
+ uint32_t available_quantity_frac;
+
+ /**
* When does the backend expect the stock to be
* restocked? 0 for unknown.
*/
@@ -2464,7 +2628,18 @@ struct TALER_MERCHANT_InventoryProduct
/**
* How many units of this product should be ordered.
*/
- unsigned int quantity;
+ uint64_t quantity;
+
+ /**
+ * Fractional component of the quantity in units of 1/100000000 of the
+ * base value.
+ */
+ uint32_t quantity_frac;
+
+ /**
+ * Set to true if this product uses fractional quantity fields.
+ */
+ bool use_fractional_quantity;
};
@@ -5173,6 +5348,146 @@ TALER_MERCHANT_otp_device_delete_cancel (
struct TALER_MERCHANT_OtpDeviceDeleteHandle *tdh);
+/* ********************* /units *********************** */
+
+struct TALER_MERCHANT_UnitEntry
+{
+ const char *unit;
+ const char *unit_name_long;
+ const char *unit_name_short;
+ const json_t *unit_name_long_i18n;
+ const json_t *unit_name_short_i18n;
+ bool unit_allow_fraction;
+ uint32_t unit_precision_level;
+ bool unit_active;
+ bool unit_builtin;
+};
+
+struct TALER_MERCHANT_UnitsGetResponse
+{
+ struct TALER_MERCHANT_HttpResponse hr;
+ union
+ {
+ struct
+ {
+ struct TALER_MERCHANT_UnitEntry *units;
+ unsigned int units_length;
+ } ok;
+ } details;
+};
+
+struct TALER_MERCHANT_UnitGetResponse
+{
+ struct TALER_MERCHANT_HttpResponse hr;
+ union
+ {
+ struct
+ {
+ struct TALER_MERCHANT_UnitEntry unit;
+ } ok;
+ } details;
+};
+
+typedef void
+(*TALER_MERCHANT_UnitsGetCallback)(
+ void *cls,
+ const struct TALER_MERCHANT_UnitsGetResponse *ugr);
+
+typedef void
+(*TALER_MERCHANT_UnitGetCallback)(
+ void *cls,
+ const struct TALER_MERCHANT_UnitGetResponse *ugr);
+
+typedef void
+(*TALER_MERCHANT_UnitsPostCallback)(
+ void *cls,
+ const struct TALER_MERCHANT_HttpResponse *hr);
+
+typedef void
+(*TALER_MERCHANT_UnitPatchCallback)(
+ void *cls,
+ const struct TALER_MERCHANT_HttpResponse *hr);
+
+typedef void
+(*TALER_MERCHANT_UnitDeleteCallback)(
+ void *cls,
+ const struct TALER_MERCHANT_HttpResponse *hr);
+
+struct TALER_MERCHANT_UnitsGetHandle;
+struct TALER_MERCHANT_UnitGetHandle;
+struct TALER_MERCHANT_UnitsPostHandle;
+struct TALER_MERCHANT_UnitPatchHandle;
+struct TALER_MERCHANT_UnitDeleteHandle;
+
+struct TALER_MERCHANT_UnitsGetHandle *
+TALER_MERCHANT_units_get (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ TALER_MERCHANT_UnitsGetCallback cb,
+ void *cb_cls);
+
+void
+TALER_MERCHANT_units_get_cancel (
+ struct TALER_MERCHANT_UnitsGetHandle *ugh);
+
+struct TALER_MERCHANT_UnitGetHandle *
+TALER_MERCHANT_unit_get (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ TALER_MERCHANT_UnitGetCallback cb,
+ void *cb_cls);
+
+void
+TALER_MERCHANT_unit_get_cancel (
+ struct TALER_MERCHANT_UnitGetHandle *ugh);
+
+struct TALER_MERCHANT_UnitsPostHandle *
+TALER_MERCHANT_units_post (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ bool unit_allow_fraction,
+ uint32_t unit_precision_level,
+ bool unit_active,
+ const json_t *unit_name_long_i18n,
+ const json_t *unit_name_short_i18n,
+ TALER_MERCHANT_UnitsPostCallback cb,
+ void *cb_cls);
+
+void
+TALER_MERCHANT_units_post_cancel (
+ struct TALER_MERCHANT_UnitsPostHandle *uph);
+
+struct TALER_MERCHANT_UnitPatchHandle *
+TALER_MERCHANT_unit_patch (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ const json_t *unit_name_long_i18n,
+ const json_t *unit_name_short_i18n,
+ const bool *unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ const bool *unit_active,
+ TALER_MERCHANT_UnitPatchCallback cb,
+ void *cb_cls);
+
+void
+TALER_MERCHANT_unit_patch_cancel (
+ struct TALER_MERCHANT_UnitPatchHandle *uph);
+
+struct TALER_MERCHANT_UnitDeleteHandle *
+TALER_MERCHANT_unit_delete (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ TALER_MERCHANT_UnitDeleteCallback cb,
+ void *cb_cls);
+
+void
+TALER_MERCHANT_unit_delete_cancel (
+ struct TALER_MERCHANT_UnitDeleteHandle *udh);
+
+
/* ********************* /templates *********************** */
diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h
@@ -394,6 +394,26 @@ TALER_TESTING_cmd_merchant_post_products2 (
unsigned int http_status);
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_post_products3 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ const char *description,
+ json_t *description_i18n,
+ const char *unit,
+ const char *price,
+ const char *image,
+ json_t *taxes,
+ int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ uint32_t minimum_age,
+ json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ unsigned int http_status);
+
+
/**
* Define a "POST /products" CMD, simple version
*
@@ -459,6 +479,26 @@ TALER_TESTING_cmd_merchant_patch_product (
unsigned int http_status);
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_patch_product2 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ const char *description,
+ json_t *description_i18n,
+ const char *unit,
+ const char *price,
+ const char *image,
+ json_t *taxes,
+ int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ uint64_t total_lost,
+ json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ unsigned int http_status);
+
+
/**
* Define a "GET /products" CMD.
*
@@ -477,6 +517,32 @@ TALER_TESTING_cmd_merchant_get_products (const char *label,
unsigned int http_status,
...);
+/**
+ * Expectations for fractional unit fields when checking a product.
+ */
+struct TALER_TESTING_ProductUnitExpectations
+{
+ /**
+ * Whether @e unit_allow_fraction is provided.
+ */
+ bool have_unit_allow_fraction;
+
+ /**
+ * Expected fractional flag.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Whether @e unit_precision_level is provided.
+ */
+ bool have_unit_precision_level;
+
+ /**
+ * Expected fractional precision.
+ */
+ uint32_t unit_precision_level;
+};
+
/**
* Define a "GET product" CMD.
@@ -488,9 +554,23 @@ TALER_TESTING_cmd_merchant_get_products (const char *label,
* @param http_status expected HTTP response code.
* @param product_reference reference to a "POST /products" or "PATCH /products/$ID" CMD
* that will provide what we expect the backend to return to us
+ * @param unit_expectations optional explicit expectations for the fractional fields
* @return the command.
*/
struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_get_product2 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ unsigned int http_status,
+ const char *product_reference,
+ const struct TALER_TESTING_ProductUnitExpectations *unit_expectations);
+
+
+/**
+ * Legacy helper kept for compatibility.
+ */
+struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_get_product (const char *label,
const char *merchant_url,
const char *product_id,
@@ -527,6 +607,18 @@ TALER_TESTING_cmd_merchant_lock_product (
unsigned int http_status);
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_lock_product2 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ struct GNUNET_TIME_Relative duration,
+ uint32_t quantity,
+ uint32_t quantity_frac,
+ bool use_fractional_quantity,
+ unsigned int http_status);
+
+
/**
* Define a "DELETE product" CMD.
*
@@ -1522,6 +1614,118 @@ TALER_TESTING_cmd_merchant_delete_otp_device (const char *label,
/* ****** Templates ******* */
/**
+ * Define a "POST /units" CMD.
+ *
+ * @param label command label.
+ * @param merchant_url base URL of the merchant serving the request.
+ * @param unit_id identifier of the unit.
+ * @param unit_name_long long label to store.
+ * @param unit_name_short short label to store.
+ * @param unit_allow_fraction whether fractional quantities are allowed.
+ * @param unit_precision_level fractional precision level.
+ * @param unit_active whether the unit starts active.
+ * @param unit_name_long_i18n optional translations for the long label.
+ * @param unit_name_short_i18n optional translations for the short label.
+ * @param http_status expected HTTP response code.
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_post_units (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ bool unit_allow_fraction,
+ uint32_t unit_precision_level,
+ bool unit_active,
+ json_t *unit_name_long_i18n,
+ json_t *unit_name_short_i18n,
+ unsigned int http_status);
+
+
+/**
+ * Define a "PATCH /units/$ID" CMD.
+ *
+ * @param label command label.
+ * @param merchant_url base URL of the merchant serving the request.
+ * @param unit_id identifier of the unit to patch.
+ * @param unit_name_long optional new long label (NULL to skip).
+ * @param unit_name_short optional new short label (NULL to skip).
+ * @param unit_name_long_i18n optional new long label translations (NULL to skip).
+ * @param unit_name_short_i18n optional new short label translations (NULL to skip).
+ * @param unit_allow_fraction optional pointer to new fractional flag (NULL to skip).
+ * @param unit_precision_level optional pointer to new precision level (NULL to skip).
+ * @param unit_active optional pointer to new active flag (NULL to skip).
+ * @param http_status expected HTTP response code.
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_patch_unit (
+ const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ json_t *unit_name_long_i18n,
+ json_t *unit_name_short_i18n,
+ const bool unit_allow_fraction,
+ const uint32_t unit_precision_level,
+ const bool unit_active,
+ unsigned int http_status);
+
+
+/**
+ * Define a "GET /units" CMD.
+ *
+ * @param label command label.
+ * @param merchant_url base URL of the merchant serving the request.
+ * @param http_status expected HTTP response code.
+ * @param ... NULL-terminated list of labels (const char *) whose units must
+ * appear in the response (verified when @a http_status is #MHD_HTTP_OK).
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_get_units (const char *label,
+ const char *merchant_url,
+ unsigned int http_status,
+ ...);
+
+
+/**
+ * Define a "GET /units/$ID" CMD.
+ *
+ * @param label command label.
+ * @param merchant_url base URL of the merchant serving the request.
+ * @param unit_id identifier to fetch.
+ * @param http_status expected HTTP response code.
+ * @param reference optional label of a command providing expected unit traits.
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_get_unit (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ unsigned int http_status,
+ const char *reference);
+
+
+/**
+ * Define a "DELETE /units/$ID" CMD.
+ *
+ * @param label command label.
+ * @param merchant_url base URL of the merchant serving the request.
+ * @param unit_id identifier to delete.
+ * @param http_status expected HTTP response code.
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_delete_unit (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ unsigned int http_status);
+
+
+/**
* Define a "POST /templates" CMD.
*
* @param label command label.
@@ -2062,8 +2266,20 @@ TALER_TESTING_cmd_merchant_get_statisticsamount (const char *label,
op (product_description, const char) \
op (product_image, const char) \
op (product_stock, const int64_t) \
+ op (product_unit_total_stock, const char) \
+ op (product_unit_precision_level, const uint32_t) \
+ op (product_unit_allow_fraction, const bool) \
op (product_unit, const char) \
op (product_id, const char) \
+ op (unit_id, const char) \
+ op (unit_name_long, const char) \
+ op (unit_name_short, const char) \
+ op (unit_allow_fraction, const bool) \
+ op (unit_precision_level, const uint32_t) \
+ op (unit_active, const bool) \
+ op (unit_builtin, const bool) \
+ op (unit_name_long_i18n, const json_t) \
+ op (unit_name_short_i18n, const json_t) \
op (reason, const char) \
op (lock_uuid, const char) \
op (auth_token, const char) \
diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h
@@ -29,6 +29,13 @@
#include <jansson.h>
/**
+ * Fixed-point base for inventory quantities (powers of 10).
+ * Six decimal digits are supported to match the maximum unit precision.
+ */
+#define MERCHANT_UNIT_FRAC_BASE 1000000U
+#define MERCHANT_UNIT_FRAC_MAX_DIGITS 6U
+
+/**
* Return default project data used by Taler merchant.
*/
const struct GNUNET_OS_ProjectData *
diff --git a/src/include/taler_merchantdb_lib.h b/src/include/taler_merchantdb_lib.h
@@ -107,6 +107,15 @@ void
TALER_MERCHANTDB_category_details_free (
struct TALER_MERCHANTDB_CategoryDetails *cd);
+/**
+ * Free members of @a ud, but not @a ud itself.
+ *
+ * @param[in] ud unit details to clean up
+ */
+void
+TALER_MERCHANTDB_unit_details_free (
+ struct TALER_MERCHANTDB_UnitDetails *ud);
+
#endif /* MERCHANT_DB_H */
/* end of taler_merchantdb_lib.h */
diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h
@@ -350,6 +350,17 @@ struct TALER_MERCHANTDB_ProductDetails
struct TALER_Amount price;
/**
+ * Optional list of per-unit prices. When NULL or empty, @e price
+ * must be used as the canonical single price.
+ */
+ struct TALER_Amount *price_array;
+
+ /**
+ * Number of entries in @e price_array.
+ */
+ size_t price_array_length;
+
+ /**
* Base64-encoded product image, or an empty string.
*/
char *image;
@@ -368,16 +379,42 @@ struct TALER_MERCHANTDB_ProductDetails
uint64_t total_stock;
/**
+ * Fractional part of stock in units of 1/100000000 of the base value.
+ */
+ uint32_t total_stock_frac;
+
+ /**
+ * Honor fractional stock if TRUE, else only integer stock.
+ */
+ bool allow_fractional_quantity;
+
+ /**
+ * Precision level (number of decimal places) to apply when
+ * fractional quantities are enabled.
+ */
+ uint32_t fractional_precision_level;
+
+ /**
* Number of units of the product in sold, in product-specific units.
*/
uint64_t total_sold;
/**
+ * Fractional part of units sold in units of 1/100000000 of the base value.
+ */
+ uint32_t total_sold_frac;
+
+ /**
* Number of units of stock lost.
*/
uint64_t total_lost;
/**
+ * Fractional part of lost units in units of 1/100000000 of the base value.
+ */
+ uint32_t total_lost_frac;
+
+ /**
* Identifies where the product is in stock, possibly an empty map.
*/
json_t *address;
@@ -398,6 +435,64 @@ struct TALER_MERCHANTDB_ProductDetails
/**
+ * Details about an inventory measurement unit.
+ */
+struct TALER_MERCHANTDB_UnitDetails
+{
+
+ /**
+ * Database serial.
+ */
+ uint64_t unit_serial;
+
+ /**
+ * Backend identifier used in product payloads.
+ */
+ char *unit;
+
+ /**
+ * Default long label (fallback string).
+ */
+ char *unit_name_long;
+
+ /**
+ * Default short label (fallback string).
+ */
+ char *unit_name_short;
+
+ /**
+ * Internationalised long labels.
+ */
+ json_t *unit_name_long_i18n;
+
+ /**
+ * Internationalised short labels.
+ */
+ json_t *unit_name_short_i18n;
+
+ /**
+ * Whether fractional quantities are enabled by default.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Maximum number of fractional digits honoured by default.
+ */
+ uint32_t unit_precision_level;
+
+ /**
+ * Hidden from selectors when false.
+ */
+ bool unit_active;
+
+ /**
+ * Built-in units cannot be deleted.
+ */
+ bool unit_builtin;
+};
+
+
+/**
* Typically called by `lookup_all_products`.
*
* @param cls a `json_t *` JSON array to build
@@ -543,6 +638,20 @@ typedef void
/**
+ * Typically called by `lookup_units`.
+ *
+ * @param cls closure
+ * @param unit_serial database identifier
+ * @param ud measurement unit details (borrowed)
+ */
+typedef void
+(*TALER_MERCHANTDB_UnitsCallback)(
+ void *cls,
+ uint64_t unit_serial,
+ const struct TALER_MERCHANTDB_UnitDetails *ud);
+
+
+/**
* Details about a product category.
*/
struct TALER_MERCHANTDB_CategoryDetails
@@ -2215,7 +2324,6 @@ struct TALER_MERCHANTDB_Plugin
struct TALER_MERCHANTDB_ProductDetails *pd,
size_t *num_categories,
uint64_t **categories);
-
/**
* Lookup product image by its hash.
*
@@ -2316,6 +2424,8 @@ struct TALER_MERCHANTDB_Plugin
* @param product_id product to lookup
* @param uuid the UUID that holds the lock
* @param quantity how many units should be locked
+ * @param quantity_frac fractional component of units to lock, in units of
+ * 1/100000000 of the base value
* @param expiration_time when should the lock expire
* @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the
* product is unknown OR if there insufficient stocks remaining
@@ -2326,6 +2436,7 @@ struct TALER_MERCHANTDB_Plugin
const char *product_id,
const struct GNUNET_Uuid *uuid,
uint64_t quantity,
+ uint32_t quantity_frac,
struct GNUNET_TIME_Timestamp expiration_time);
@@ -2486,6 +2597,8 @@ struct TALER_MERCHANTDB_Plugin
* @param order_id alphanumeric string that uniquely identifies the order
* @param product_id uniquely identifies the product to be locked
* @param quantity how many units should be locked to the @a order_id
+ * @param quantity_frac fractional component of the quantity to be locked,
+ * in units of 1/100000000 of the base value
* @return transaction status,
* #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS means there are insufficient stocks
* #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success
@@ -2495,7 +2608,8 @@ struct TALER_MERCHANTDB_Plugin
const char *instance_id,
const char *order_id,
const char *product_id,
- uint64_t quantity);
+ uint64_t quantity,
+ uint32_t quantity_frac);
/**
@@ -3687,6 +3801,107 @@ struct TALER_MERCHANTDB_Plugin
const char *otp_id,
uint64_t *serial);
+ /**
+ * Delete information about a measurement unit.
+ *
+ * @param cls closure
+ * @param instance_id instance to delete unit from
+ * @param unit_id symbolic identifier of the unit
+ * @param[out] no_instance set to true if @a instance_id is unknown
+ * @param[out] no_unit set to true if the unit does not exist
+ * @param[out] builtin_conflict set to true if the unit is builtin and may not be deleted
+ * @return DB status code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*delete_unit)(void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ bool *no_instance,
+ bool *no_unit,
+ bool *builtin_conflict);
+
+ /**
+ * Insert a measurement unit definition.
+ *
+ * @param cls closure
+ * @param instance_id instance to insert unit for
+ * @param ud unit details to store (unit_serial ignored)
+ * @param[out] no_instance set to true if @a instance_id is unknown
+ * @param[out] conflict set to true if a conflicting unit already exists
+ * @param[out] unit_serial set to the inserted serial on success
+ * @return database result code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*insert_unit)(void *cls,
+ const char *instance_id,
+ const struct TALER_MERCHANTDB_UnitDetails *ud,
+ bool *no_instance,
+ bool *conflict,
+ uint64_t *unit_serial);
+
+ /**
+ * Update a measurement unit definition.
+ *
+ * @param cls closure
+ * @param instance_id instance owning the unit
+ * @param unit_id symbolic identifier of the unit
+ * @param unit_name_long optional new long label (NULL to keep current)
+ * @param unit_name_long_i18n optional new long-label translations (NULL to keep current)
+ * @param unit_name_short optional new short label (NULL to keep current)
+ * @param unit_name_short_i18n optional new short-label translations (NULL to keep current)
+ * @param unit_allow_fraction optional new fractional toggle (NULL to keep current)
+ * @param unit_precision_level optional new fractional precision (NULL to keep current)
+ * @param unit_active optional new visibility flag (NULL to keep current)
+ * @param[out] no_instance set if instance unknown
+ * @param[out] no_unit set if unit unknown
+ * @param[out] builtin_conflict set if immutable builtin fields touched
+ * @return database result code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*update_unit)(void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ const char *unit_name_long,
+ const json_t *unit_name_long_i18n,
+ const char *unit_name_short,
+ const json_t *unit_name_short_i18n,
+ const bool *unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ const bool *unit_active,
+ bool *no_instance,
+ bool *no_unit,
+ bool *builtin_conflict);
+
+ /**
+ * Lookup all measurement units of an instance.
+ *
+ * @param cls closure
+ * @param instance_id instance to fetch units for
+ * @param cb function to call per unit
+ * @param cb_cls closure for @a cb
+ * @return database result code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*lookup_units)(void *cls,
+ const char *instance_id,
+ TALER_MERCHANTDB_UnitsCallback cb,
+ void *cb_cls);
+
+ /**
+ * Lookup a single measurement unit.
+ *
+ * @param cls closure
+ * @param instance_id instance to fetch unit for
+ * @param unit_id symbolic identifier
+ * @param[out] ud unit details on success; may be NULL to test existence
+ * @return database result code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*select_unit)(void *cls,
+ const char *instance_id,
+ const char *unit_id,
+ struct TALER_MERCHANTDB_UnitDetails *ud);
+
/**
* Delete information about a product category.
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
@@ -23,6 +23,7 @@ libtalermerchant_la_SOURCES = \
merchant_api_delete_otp_device.c \
merchant_api_delete_product.c \
merchant_api_delete_template.c \
+ merchant_api_delete_unit.c \
merchant_api_delete_transfer.c \
merchant_api_delete_webhook.c \
merchant_api_get_account.c \
@@ -40,6 +41,8 @@ libtalermerchant_la_SOURCES = \
merchant_api_get_statistics.c \
merchant_api_get_transfers.c \
merchant_api_get_template.c \
+ merchant_api_get_unit.c \
+ merchant_api_get_units.c \
merchant_api_get_templates.c \
merchant_api_get_webhook.c \
merchant_api_get_webhooks.c \
@@ -51,6 +54,7 @@ libtalermerchant_la_SOURCES = \
merchant_api_patch_otp_device.c \
merchant_api_patch_product.c \
merchant_api_patch_template.c \
+ merchant_api_patch_unit.c \
merchant_api_patch_webhook.c \
merchant_api_post_account.c \
merchant_api_post_instance_auth.c \
@@ -64,6 +68,7 @@ libtalermerchant_la_SOURCES = \
merchant_api_post_order_refund.c \
merchant_api_post_otp_devices.c \
merchant_api_post_products.c \
+ merchant_api_post_units.c \
merchant_api_post_transfers.c \
merchant_api_post_templates.c \
merchant_api_post_tokenfamilies.c \
diff --git a/src/lib/merchant_api_common.c b/src/lib/merchant_api_common.c
@@ -27,6 +27,87 @@
#include "merchant_api_common.h"
#include <gnunet/gnunet_uri_lib.h>
#include <taler/taler_json_lib.h>
+#include "taler_merchant_util.h"
+
+
+static void
+TALER_MERCHANT_format_fractional_string (uint64_t integer,
+ uint32_t fractional,
+ char *buffer,
+ size_t buffer_length)
+{
+ GNUNET_assert (NULL != buffer);
+ GNUNET_assert (0 < buffer_length);
+ GNUNET_assert (fractional < MERCHANT_UNIT_FRAC_BASE);
+
+ if (0 == fractional)
+ {
+ GNUNET_snprintf (buffer,
+ buffer_length,
+ "%lu",
+ integer);
+ return;
+ }
+ {
+ char frac_buf[MERCHANT_UNIT_FRAC_MAX_DIGITS + 1];
+ size_t idx;
+
+ GNUNET_snprintf (frac_buf,
+ sizeof (frac_buf),
+ "%0*u",
+ MERCHANT_UNIT_FRAC_MAX_DIGITS,
+ (unsigned int) fractional);
+ for (idx = strlen (frac_buf); idx > 0; idx--)
+ {
+ if ('0' != frac_buf[idx - 1])
+ break;
+ frac_buf[idx - 1] = '\0';
+ }
+ if ('\0' == frac_buf[0])
+ GNUNET_strlcpy (frac_buf,
+ "0",
+ sizeof (frac_buf));
+ GNUNET_snprintf (buffer,
+ buffer_length,
+ "%lu.%s",
+ integer,
+ frac_buf);
+ }
+}
+
+
+void
+TALER_MERCHANT_format_quantity_string (uint64_t quantity,
+ uint32_t quantity_frac,
+ char *buffer,
+ size_t buffer_length)
+{
+ TALER_MERCHANT_format_fractional_string (quantity,
+ quantity_frac,
+ buffer,
+ buffer_length);
+}
+
+
+void
+TALER_MERCHANT_format_stock_string (uint64_t total_stock,
+ uint32_t total_stock_frac,
+ char *buffer,
+ size_t buffer_length)
+{
+ if ( (INT64_MAX == (int64_t) total_stock) &&
+ (INT32_MAX == (int32_t) total_stock_frac) )
+ {
+ GNUNET_snprintf (buffer,
+ buffer_length,
+ "-1");
+ return;
+ }
+ TALER_MERCHANT_format_fractional_string (total_stock,
+ total_stock_frac,
+ buffer,
+ buffer_length);
+}
void
@@ -447,6 +528,8 @@ TALER_MERCHANT_handle_order_creation_response_ (
case MHD_HTTP_GONE:
/* The quantity of some product requested was not available. */
{
+ bool rq_frac_missing;
+ bool aq_frac_missing;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string (
@@ -455,10 +538,20 @@ TALER_MERCHANT_handle_order_creation_response_ (
GNUNET_JSON_spec_uint64 (
"requested_quantity",
&por.details.gone.requested_quantity),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 (
+ "requested_quantity_frac",
+ &por.details.gone.requested_quantity_frac),
+ &rq_frac_missing),
GNUNET_JSON_spec_uint64 (
"available_quantity",
&por.details.gone.available_quantity),
GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 (
+ "available_quantity_frac",
+ &por.details.gone.available_quantity_frac),
+ &aq_frac_missing),
+ GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_timestamp (
"restock_expected",
&por.details.gone.restock_expected),
@@ -475,6 +568,13 @@ TALER_MERCHANT_handle_order_creation_response_ (
por.hr.http_status = 0;
por.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
}
+ else
+ {
+ if (rq_frac_missing)
+ por.details.gone.requested_quantity_frac = 0;
+ if (aq_frac_missing)
+ por.details.gone.available_quantity_frac = 0;
+ }
break;
}
case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
diff --git a/src/lib/merchant_api_common.h b/src/lib/merchant_api_common.h
@@ -58,4 +58,43 @@ TALER_MERCHANT_parse_error_details_ (const json_t *response,
struct TALER_MERCHANT_HttpResponse *hr);
+enum GNUNET_GenericReturnValue
+TALER_MERCHANT_parse_fractional_string (bool allow_negative,
+ const char *value,
+ int64_t *integer_part,
+ uint32_t *fractional_part);
+
+
+/**
+ * Format a quantity into its decimal string representation using the merchant
+ * fixed-point base (MERCHANT_UNIT_FRAC_BASE).
+ *
+ * @param quantity integer part
+ * @param quantity_frac fractional part (0..MERCHANT_UNIT_FRAC_BASE-1)
+ * @param[out] buffer output buffer
+ * @param buffer_length size of @a buffer
+ */
+void
+TALER_MERCHANT_format_quantity_string (uint64_t quantity,
+ uint32_t quantity_frac,
+ char *buffer,
+ size_t buffer_length);
+
+
+/**
+ * Format a stock value into its decimal string representation using the
+ * merchant fixed-point base (MERCHANT_UNIT_FRAC_BASE).
+ *
+ * @param total_stock integer part
+ * @param total_stock_frac fractional part (0..MERCHANT_UNIT_FRAC_BASE-1)
+ * @param[out] buffer output buffer
+ * @param buffer_length size of @a buffer
+ */
+void
+TALER_MERCHANT_format_stock_string (uint64_t total_stock,
+ uint32_t total_stock_frac,
+ char *buffer,
+ size_t buffer_length);
+
+
#endif
diff --git a/src/lib/merchant_api_delete_unit.c b/src/lib/merchant_api_delete_unit.c
@@ -0,0 +1,177 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 2.1, 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ TALER; see the file COPYING.LGPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file merchant_api_delete_unit.c
+ * @brief Implementation of DELETE /private/units/$ID
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_merchant_service.h"
+#include "merchant_api_curl_defaults.h"
+#include "merchant_api_common.h"
+#include <taler/taler_json_lib.h>
+
+
+/**
+ * Handle for a DELETE /private/units/$ID operation.
+ */
+struct TALER_MERCHANT_UnitDeleteHandle
+{
+ /**
+ * Fully qualified request URL.
+ */
+ char *url;
+
+ /**
+ * In-flight CURL job.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Completion callback.
+ */
+ TALER_MERCHANT_UnitDeleteCallback cb;
+
+ /**
+ * Closure for @a cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Called when the HTTP request finishes.
+ *
+ * @param cls operation handle
+ * @param response_code HTTP status (0 on failure)
+ * @param response parsed JSON reply (NULL if unavailable)
+ */
+static void
+handle_delete_unit_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct TALER_MERCHANT_UnitDeleteHandle *udh = cls;
+ const json_t *json = response;
+ struct TALER_MERCHANT_HttpResponse hr = {
+ .http_status = (unsigned int) response_code,
+ .reply = json
+ };
+
+ udh->job = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "DELETE /private/units finished with status %u\n",
+ (unsigned int) response_code);
+ switch (response_code)
+ {
+ case MHD_HTTP_NO_CONTENT:
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ case MHD_HTTP_UNAUTHORIZED:
+ case MHD_HTTP_FORBIDDEN:
+ case MHD_HTTP_NOT_FOUND:
+ case MHD_HTTP_CONFLICT:
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ hr.ec = TALER_JSON_get_error_code (json);
+ hr.hint = TALER_JSON_get_error_hint (json);
+ break;
+ case 0:
+ hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ default:
+ TALER_MERCHANT_parse_error_details_ (json,
+ response_code,
+ &hr);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unexpected response %u/%d for DELETE /private/units\n",
+ (unsigned int) response_code,
+ (int) hr.ec);
+ GNUNET_break_op (0);
+ break;
+ }
+ udh->cb (udh->cb_cls,
+ &hr);
+ TALER_MERCHANT_unit_delete_cancel (udh);
+}
+
+
+struct TALER_MERCHANT_UnitDeleteHandle *
+TALER_MERCHANT_unit_delete (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ TALER_MERCHANT_UnitDeleteCallback cb,
+ void *cb_cls)
+{
+ struct TALER_MERCHANT_UnitDeleteHandle *udh;
+ CURL *eh;
+ char *path;
+
+ GNUNET_asprintf (&path,
+ "private/units/%s",
+ unit_id);
+ udh = GNUNET_new (struct TALER_MERCHANT_UnitDeleteHandle);
+ udh->ctx = ctx;
+ udh->cb = cb;
+ udh->cb_cls = cb_cls;
+ udh->url = TALER_url_join (backend_url,
+ path,
+ NULL);
+ GNUNET_free (path);
+ if (NULL == udh->url)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to build /private/units/%s URL\n",
+ unit_id);
+ GNUNET_free (udh);
+ return NULL;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Requesting DELETE on '%s'\n",
+ udh->url);
+ eh = TALER_MERCHANT_curl_easy_get_ (udh->url);
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_CUSTOMREQUEST,
+ MHD_HTTP_METHOD_DELETE));
+ udh->job = GNUNET_CURL_job_add (ctx,
+ eh,
+ &handle_delete_unit_finished,
+ udh);
+ return udh;
+}
+
+
+void
+TALER_MERCHANT_unit_delete_cancel (struct TALER_MERCHANT_UnitDeleteHandle *udh)
+{
+ if (NULL != udh->job)
+ GNUNET_CURL_job_cancel (udh->job);
+ GNUNET_free (udh->url);
+ GNUNET_free (udh);
+}
+
+
+/* end of merchant_api_delete_unit.c */
diff --git a/src/lib/merchant_api_get_product.c b/src/lib/merchant_api_get_product.c
@@ -105,6 +105,10 @@ handle_get_product_finished (void *cls,
GNUNET_JSON_spec_string (
"unit",
&pgr.details.ok.unit),
+ TALER_JSON_spec_amount_any_array (
+ "unit_price",
+ &pgr.details.ok.unit_price_len,
+ (struct TALER_Amount **) &pgr.details.ok.unit_price),
TALER_JSON_spec_amount_any (
"price",
&pgr.details.ok.price),
@@ -121,6 +125,15 @@ handle_get_product_finished (void *cls,
GNUNET_JSON_spec_int64 (
"total_stock",
&pgr.details.ok.total_stock),
+ GNUNET_JSON_spec_string (
+ "unit_total_stock",
+ &pgr.details.ok.unit_total_stock),
+ GNUNET_JSON_spec_bool (
+ "unit_allow_fraction",
+ &pgr.details.ok.unit_allow_fraction),
+ GNUNET_JSON_spec_uint32 (
+ "unit_precision_level",
+ &pgr.details.ok.unit_precision_level),
GNUNET_JSON_spec_uint64 (
"total_sold",
&pgr.details.ok.total_sold),
diff --git a/src/lib/merchant_api_get_unit.c b/src/lib/merchant_api_get_unit.c
@@ -0,0 +1,249 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free Software
+ Foundation; either version 2.1, 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ TALER; see the file COPYING.LGPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file merchant_api_get_unit.c
+ * @brief Implementation of GET /private/units/$ID
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_merchant_service.h"
+#include "merchant_api_curl_defaults.h"
+#include <taler/taler_json_lib.h>
+
+
+/**
+ * Handle for a GET /private/units/$ID operation.
+ */
+struct TALER_MERCHANT_UnitGetHandle
+{
+ /**
+ * Fully qualified request URL.
+ */
+ char *url;
+
+ /**
+ * In-flight job handle.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Callback to invoke with the response.
+ */
+ TALER_MERCHANT_UnitGetCallback cb;
+
+ /**
+ * Closure for @a cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Parse the JSON response into @a ugr.
+ *
+ * @param json full JSON reply
+ * @param ugr response descriptor to populate
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_unit (const json_t *json,
+ struct TALER_MERCHANT_UnitGetResponse *ugr)
+{
+ struct TALER_MERCHANT_UnitEntry *entry = &ugr->details.ok.unit;
+ const char *unit;
+ const char *unit_name_long;
+ const char *unit_name_short;
+ const json_t *unit_name_long_i18n = NULL;
+ const json_t *unit_name_short_i18n = NULL;
+ bool unit_allow_fraction;
+ bool unit_active;
+ bool unit_builtin;
+ uint32_t unit_precision_level;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("unit",
+ &unit),
+ GNUNET_JSON_spec_string ("unit_name_long",
+ &unit_name_long),
+ GNUNET_JSON_spec_string ("unit_name_short",
+ &unit_name_short),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("unit_name_long_i18n",
+ &unit_name_long_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("unit_name_short_i18n",
+ &unit_name_short_i18n),
+ NULL),
+ GNUNET_JSON_spec_bool ("unit_allow_fraction",
+ &unit_allow_fraction),
+ GNUNET_JSON_spec_uint32 ("unit_precision_level",
+ &unit_precision_level),
+ GNUNET_JSON_spec_bool ("unit_active",
+ &unit_active),
+ GNUNET_JSON_spec_bool ("unit_builtin",
+ &unit_builtin),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (json,
+ spec,
+ NULL,
+ NULL))
+ {
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_JSON_parse_free (spec);
+ entry->unit = unit;
+ entry->unit_name_long = unit_name_long;
+ entry->unit_name_short = unit_name_short;
+ entry->unit_name_long_i18n = unit_name_long_i18n;
+ entry->unit_name_short_i18n = unit_name_short_i18n;
+ entry->unit_allow_fraction = unit_allow_fraction;
+ entry->unit_precision_level = unit_precision_level;
+ entry->unit_active = unit_active;
+ entry->unit_builtin = unit_builtin;
+ return GNUNET_OK;
+}
+
+
+/**
+ * Called once the HTTP request completes.
+ *
+ * @param cls operation handle
+ * @param response_code HTTP status (0 on client-side errors)
+ * @param response parsed JSON reply (NULL if parsing failed)
+ */
+static void
+handle_get_unit_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct TALER_MERCHANT_UnitGetHandle *ugh = cls;
+ const json_t *json = response;
+ struct TALER_MERCHANT_UnitGetResponse ugr = {
+ .hr.http_status = (unsigned int) response_code,
+ .hr.reply = json
+ };
+
+ ugh->job = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "GET /private/units/$ID finished with status %u\n",
+ (unsigned int) response_code);
+ switch (response_code)
+ {
+ case MHD_HTTP_OK:
+ if (GNUNET_OK !=
+ parse_unit (json,
+ &ugr))
+ {
+ ugr.hr.http_status = 0;
+ ugr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ }
+ ugh->cb (ugh->cb_cls,
+ &ugr);
+ TALER_MERCHANT_unit_get_cancel (ugh);
+ return;
+ case MHD_HTTP_UNAUTHORIZED:
+ case MHD_HTTP_FORBIDDEN:
+ case MHD_HTTP_NOT_FOUND:
+ ugr.hr.ec = TALER_JSON_get_error_code (json);
+ ugr.hr.hint = TALER_JSON_get_error_hint (json);
+ break;
+ case 0:
+ ugr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ default:
+ ugr.hr.ec = TALER_JSON_get_error_code (json);
+ ugr.hr.hint = TALER_JSON_get_error_hint (json);
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Unexpected response %u/%d for GET /private/units/$ID\n",
+ (unsigned int) response_code,
+ (int) ugr.hr.ec);
+ break;
+ }
+ ugh->cb (ugh->cb_cls,
+ &ugr);
+ TALER_MERCHANT_unit_get_cancel (ugh);
+}
+
+
+struct TALER_MERCHANT_UnitGetHandle *
+TALER_MERCHANT_unit_get (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ TALER_MERCHANT_UnitGetCallback cb,
+ void *cb_cls)
+{
+ struct TALER_MERCHANT_UnitGetHandle *ugh;
+ CURL *eh;
+ char *path;
+
+ GNUNET_asprintf (&path,
+ "private/units/%s",
+ unit_id);
+ ugh = GNUNET_new (struct TALER_MERCHANT_UnitGetHandle);
+ ugh->ctx = ctx;
+ ugh->cb = cb;
+ ugh->cb_cls = cb_cls;
+ ugh->url = TALER_url_join (backend_url,
+ path,
+ NULL);
+ GNUNET_free (path);
+ if (NULL == ugh->url)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to build /private/units/%s URL\n",
+ unit_id);
+ GNUNET_free (ugh);
+ return NULL;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Requesting URL '%s'\n",
+ ugh->url);
+ eh = TALER_MERCHANT_curl_easy_get_ (ugh->url);
+ ugh->job = GNUNET_CURL_job_add (ctx,
+ eh,
+ &handle_get_unit_finished,
+ ugh);
+ return ugh;
+}
+
+
+void
+TALER_MERCHANT_unit_get_cancel (struct TALER_MERCHANT_UnitGetHandle *ugh)
+{
+ if (NULL != ugh->job)
+ GNUNET_CURL_job_cancel (ugh->job);
+ GNUNET_free (ugh->url);
+ GNUNET_free (ugh);
+}
+
+
+/* end of merchant_api_get_unit.c */
diff --git a/src/lib/merchant_api_get_units.c b/src/lib/merchant_api_get_units.c
@@ -0,0 +1,329 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free
+ Software Foundation; either version 2.1, 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 Lesser General Public License for more
+ details.
+
+ You should have received a copy of the GNU Lesser General Public License along
+ with TALER; see the file COPYING.LGPL. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file merchant_api_get_units.c
+ * @brief Implementation of GET /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_merchant_service.h"
+#include "merchant_api_curl_defaults.h"
+#include <taler/taler_json_lib.h>
+
+
+/**
+ * Maximum number of units returned in a single response.
+ */
+#define MAX_UNITS 1024
+
+
+/**
+ * Handle for a GET /private/units operation.
+ */
+struct TALER_MERCHANT_UnitsGetHandle
+{
+ /**
+ * Fully qualified request URL.
+ */
+ char *url;
+
+ /**
+ * In-flight job handle.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Callback to invoke with the outcome.
+ */
+ TALER_MERCHANT_UnitsGetCallback cb;
+
+ /**
+ * Closure for @e cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Parse an individual unit entry from @a value.
+ *
+ * @param value JSON object describing the unit
+ * @param[out] ue set to the parsed values
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_unit_entry (const json_t *value,
+ struct TALER_MERCHANT_UnitEntry *ue)
+{
+ const char *unit;
+ const char *unit_name_long;
+ const char *unit_name_short;
+ const json_t *unit_name_long_i18n = NULL;
+ const json_t *unit_name_short_i18n = NULL;
+ bool unit_allow_fraction;
+ bool unit_active;
+ bool unit_builtin;
+ uint32_t unit_precision_level;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("unit",
+ &unit),
+ GNUNET_JSON_spec_string ("unit_name_long",
+ &unit_name_long),
+ GNUNET_JSON_spec_string ("unit_name_short",
+ &unit_name_short),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("unit_name_long_i18n",
+ &unit_name_long_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("unit_name_short_i18n",
+ &unit_name_short_i18n),
+ NULL),
+ GNUNET_JSON_spec_bool ("unit_allow_fraction",
+ &unit_allow_fraction),
+ GNUNET_JSON_spec_uint32 ("unit_precision_level",
+ &unit_precision_level),
+ GNUNET_JSON_spec_bool ("unit_active",
+ &unit_active),
+ GNUNET_JSON_spec_bool ("unit_builtin",
+ &unit_builtin),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (value,
+ spec,
+ NULL,
+ NULL))
+ {
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_JSON_parse_free (spec);
+ ue->unit = unit;
+ ue->unit_name_long = unit_name_long;
+ ue->unit_name_short = unit_name_short;
+ ue->unit_name_long_i18n = unit_name_long_i18n;
+ ue->unit_name_short_i18n = unit_name_short_i18n;
+ ue->unit_allow_fraction = unit_allow_fraction;
+ ue->unit_precision_level = unit_precision_level;
+ ue->unit_active = unit_active;
+ ue->unit_builtin = unit_builtin;
+ return GNUNET_OK;
+}
+
+
+/**
+ * Parse the list of units from @a units and call the callback.
+ *
+ * @param json complete response JSON
+ * @param units array of units
+ * @param ugh ongoing operation handle
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_units (const json_t *json,
+ const json_t *units,
+ struct TALER_MERCHANT_UnitsGetHandle *ugh)
+{
+ size_t len;
+
+ len = json_array_size (units);
+ if (len > MAX_UNITS)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+
+ {
+ struct TALER_MERCHANT_UnitEntry entries[GNUNET_NZL (len)];
+ size_t idx;
+ json_t *value;
+
+ json_array_foreach (units, idx, value) {
+ if (GNUNET_OK !=
+ parse_unit_entry (value,
+ &entries[idx]))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ }
+ {
+ struct TALER_MERCHANT_UnitsGetResponse ugr = {
+ .hr.http_status = MHD_HTTP_OK,
+ .hr.reply = json,
+ .details = {
+ .ok = {
+ .units = entries,
+ .units_length = (unsigned int) len
+ }
+
+
+ }
+
+
+ };
+
+ ugh->cb (ugh->cb_cls,
+ &ugr);
+ }
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Called when the HTTP transfer finishes.
+ *
+ * @param cls closure, the operation handle
+ * @param response_code HTTP status (0 on network errors)
+ * @param response parsed JSON body (NULL if parsing failed)
+ */
+static void
+handle_get_units_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct TALER_MERCHANT_UnitsGetHandle *ugh = cls;
+ const json_t *json = response;
+ struct TALER_MERCHANT_UnitsGetResponse ugr = {
+ .hr.http_status = (unsigned int) response_code,
+ .hr.reply = json
+ };
+
+ ugh->job = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "GET /private/units finished with status %u\n",
+ (unsigned int) response_code);
+ switch (response_code)
+ {
+ case MHD_HTTP_OK:
+ {
+ const json_t *units;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_array_const ("units",
+ &units),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (json,
+ spec,
+ NULL,
+ NULL))
+ {
+ GNUNET_break_op (0);
+ ugr.hr.http_status = 0;
+ ugr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ }
+ if (GNUNET_OK ==
+ parse_units (json,
+ units,
+ ugh))
+ {
+ TALER_MERCHANT_units_get_cancel (ugh);
+ return;
+ }
+ ugr.hr.http_status = 0;
+ ugr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ }
+ case MHD_HTTP_UNAUTHORIZED:
+ case MHD_HTTP_FORBIDDEN:
+ case MHD_HTTP_NOT_FOUND:
+ case MHD_HTTP_CONFLICT:
+ ugr.hr.ec = TALER_JSON_get_error_code (json);
+ ugr.hr.hint = TALER_JSON_get_error_hint (json);
+ break;
+ case 0:
+ ugr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ default:
+ ugr.hr.ec = TALER_JSON_get_error_code (json);
+ ugr.hr.hint = TALER_JSON_get_error_hint (json);
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Unexpected response code %u/%d for GET /private/units\n",
+ (unsigned int) response_code,
+ (int) ugr.hr.ec);
+ break;
+ }
+ ugh->cb (ugh->cb_cls,
+ &ugr);
+ TALER_MERCHANT_units_get_cancel (ugh);
+}
+
+
+struct TALER_MERCHANT_UnitsGetHandle *
+TALER_MERCHANT_units_get (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ TALER_MERCHANT_UnitsGetCallback cb,
+ void *cb_cls)
+{
+ struct TALER_MERCHANT_UnitsGetHandle *ugh;
+ CURL *eh;
+
+ ugh = GNUNET_new (struct TALER_MERCHANT_UnitsGetHandle);
+ ugh->ctx = ctx;
+ ugh->cb = cb;
+ ugh->cb_cls = cb_cls;
+ ugh->url = TALER_url_join (backend_url,
+ "private/units",
+ NULL);
+ if (NULL == ugh->url)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to build /private/units URL\n");
+ GNUNET_free (ugh);
+ return NULL;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Requesting URL '%s'\n",
+ ugh->url);
+ eh = TALER_MERCHANT_curl_easy_get_ (ugh->url);
+ ugh->job = GNUNET_CURL_job_add (ctx,
+ eh,
+ &handle_get_units_finished,
+ ugh);
+ return ugh;
+}
+
+
+void
+TALER_MERCHANT_units_get_cancel (struct TALER_MERCHANT_UnitsGetHandle *ugh)
+{
+ if (NULL != ugh->job)
+ GNUNET_CURL_job_cancel (ugh->job);
+ GNUNET_free (ugh->url);
+ GNUNET_free (ugh);
+}
+
+
+/* end of merchant_api_get_units.c */
diff --git a/src/lib/merchant_api_lock_product.c b/src/lib/merchant_api_lock_product.c
@@ -157,26 +157,36 @@ handle_lock_product_finished (void *cls,
struct TALER_MERCHANT_ProductLockHandle *
-TALER_MERCHANT_product_lock (
+TALER_MERCHANT_product_lock2 (
struct GNUNET_CURL_Context *ctx,
const char *backend_url,
const char *product_id,
const char *uuid,
struct GNUNET_TIME_Relative duration,
- uint32_t quantity,
+ uint64_t quantity,
+ uint32_t quantity_frac,
+ bool use_fractional_quantity,
TALER_MERCHANT_ProductLockCallback cb,
void *cb_cls)
{
struct TALER_MERCHANT_ProductLockHandle *plh;
json_t *req_obj;
+ char unit_quantity_buf[64];
+
+ TALER_MERCHANT_format_quantity_string (quantity,
+ quantity_frac,
+ unit_quantity_buf,
+ sizeof (unit_quantity_buf));
req_obj = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("lock_uuid",
uuid),
GNUNET_JSON_pack_time_rel ("duration",
duration),
- GNUNET_JSON_pack_uint64 ("quantity",
- quantity));
+ GNUNET_JSON_pack_string ("unit_quantity",
+ unit_quantity_buf));
+ (void) use_fractional_quantity;
+ GNUNET_assert ( (0 == quantity_frac) || use_fractional_quantity);
plh = GNUNET_new (struct TALER_MERCHANT_ProductLockHandle);
plh->ctx = ctx;
plh->cb = cb;
@@ -242,4 +252,28 @@ TALER_MERCHANT_product_lock_cancel (
}
+struct TALER_MERCHANT_ProductLockHandle *
+TALER_MERCHANT_product_lock (
+ struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *product_id,
+ const char *uuid,
+ struct GNUNET_TIME_Relative duration,
+ uint32_t quantity,
+ TALER_MERCHANT_ProductLockCallback cb,
+ void *cb_cls)
+{
+ return TALER_MERCHANT_product_lock2 (ctx,
+ backend_url,
+ product_id,
+ uuid,
+ duration,
+ quantity,
+ 0,
+ false,
+ cb,
+ cb_cls);
+}
+
+
/* end of merchant_api_lock_product.c */
diff --git a/src/lib/merchant_api_patch_product.c b/src/lib/merchant_api_patch_product.c
@@ -157,17 +157,21 @@ handle_patch_product_finished (void *cls,
struct TALER_MERCHANT_ProductPatchHandle *
-TALER_MERCHANT_product_patch (
+TALER_MERCHANT_product_patch2 (
struct GNUNET_CURL_Context *ctx,
const char *backend_url,
const char *product_id,
const char *description,
const json_t *description_i18n,
const char *unit,
- const struct TALER_Amount *price,
+ const struct TALER_Amount *unit_prices,
+ size_t unit_price_len,
const char *image,
const json_t *taxes,
int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ const uint32_t *unit_precision_level,
uint64_t total_lost,
const json_t *address,
struct GNUNET_TIME_Timestamp next_restock,
@@ -176,32 +180,61 @@ TALER_MERCHANT_product_patch (
{
struct TALER_MERCHANT_ProductPatchHandle *pph;
json_t *req_obj;
+ char unit_total_stock_buf[64];
- req_obj = GNUNET_JSON_PACK (
- /* FIXME: once we move to the new-style API,
- allow applications to set the product name properly! */
- GNUNET_JSON_pack_string ("product_name",
- description),
- GNUNET_JSON_pack_string ("description",
- description),
- GNUNET_JSON_pack_object_incref ("description_i18n",
- (json_t *) description_i18n),
- GNUNET_JSON_pack_string ("unit",
- unit),
- TALER_JSON_pack_amount ("price",
- price),
- GNUNET_JSON_pack_string ("image",
- image),
- GNUNET_JSON_pack_array_incref ("taxes",
- (json_t *) taxes),
- GNUNET_JSON_pack_uint64 ("total_stock",
- total_stock),
- GNUNET_JSON_pack_uint64 ("total_lost",
- total_lost),
- GNUNET_JSON_pack_object_incref ("address",
- (json_t *) address),
- GNUNET_JSON_pack_timestamp ("next_restock",
- next_restock));
+ TALER_MERCHANT_format_stock_string (total_stock,
+ total_stock_frac,
+ unit_total_stock_buf,
+ sizeof (unit_total_stock_buf));
+
+ {
+ req_obj = GNUNET_JSON_PACK (
+ /* FIXME: once we move to the new-style API,
+ allow applications to set the product name properly! */
+ GNUNET_JSON_pack_string ("product_name",
+ description),
+ GNUNET_JSON_pack_string ("description",
+ description),
+ GNUNET_JSON_pack_object_incref ("description_i18n",
+ (json_t *) description_i18n),
+ GNUNET_JSON_pack_string ("unit",
+ unit),
+ TALER_JSON_pack_amount_array ("unit_price",
+ unit_price_len,
+ unit_prices),
+ GNUNET_JSON_pack_string ("image",
+ image),
+ GNUNET_JSON_pack_array_incref ("taxes",
+ (json_t *) taxes),
+ GNUNET_JSON_pack_string ("unit_total_stock",
+ unit_total_stock_buf),
+ GNUNET_JSON_pack_bool ("unit_allow_fraction",
+ unit_allow_fraction),
+ GNUNET_JSON_pack_uint64 ("total_lost",
+ total_lost),
+ GNUNET_JSON_pack_object_incref ("address",
+ (json_t *) address),
+ GNUNET_JSON_pack_timestamp ("next_restock",
+ next_restock));
+ }
+ if (NULL != unit_precision_level)
+ {
+ GNUNET_assert (0 ==
+ json_object_set_new (req_obj,
+ "unit_precision_level",
+ json_integer (
+ *unit_precision_level)));
+ }
+ if (! unit_allow_fraction)
+ {
+ GNUNET_assert (0 ==
+ json_object_del (req_obj,
+ "unit_allow_fraction"));
+ if (NULL != unit_precision_level)
+ GNUNET_assert (0 ==
+ json_object_del (req_obj,
+ "unit_precision_level"));
+ }
pph = GNUNET_new (struct TALER_MERCHANT_ProductPatchHandle);
pph->ctx = ctx;
pph->cb = cb;
@@ -255,6 +288,46 @@ TALER_MERCHANT_product_patch (
}
+struct TALER_MERCHANT_ProductPatchHandle *
+TALER_MERCHANT_product_patch (
+ struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *product_id,
+ const char *description,
+ const json_t *description_i18n,
+ const char *unit,
+ const struct TALER_Amount *price,
+ const char *image,
+ const json_t *taxes,
+ int64_t total_stock,
+ uint64_t total_lost,
+ const json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ TALER_MERCHANT_ProductPatchCallback cb,
+ void *cb_cls)
+{
+ return TALER_MERCHANT_product_patch2 (ctx,
+ backend_url,
+ product_id,
+ description,
+ description_i18n,
+ unit,
+ price,
+ 1,
+ image,
+ taxes,
+ total_stock,
+ 0,
+ false,
+ NULL,
+ total_lost,
+ address,
+ next_restock,
+ cb,
+ cb_cls);
+}
+
+
void
TALER_MERCHANT_product_patch_cancel (
struct TALER_MERCHANT_ProductPatchHandle *pph)
diff --git a/src/lib/merchant_api_patch_unit.c b/src/lib/merchant_api_patch_unit.c
@@ -0,0 +1,291 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free
+ Software Foundation; either version 2.1, 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ TALER; see the file COPYING.LGPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file merchant_api_patch_unit.c
+ * @brief Implementation of PATCH /private/units/$ID
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include "taler_merchant_service.h"
+#include "merchant_api_curl_defaults.h"
+#include "merchant_api_common.h"
+#include <taler/taler_json_lib.h>
+#include <taler/taler_curl_lib.h>
+
+
+/**
+ * Handle for a PATCH /private/units/$ID operation.
+ */
+struct TALER_MERCHANT_UnitPatchHandle
+{
+ /**
+ * Fully qualified request URL.
+ */
+ char *url;
+
+ /**
+ * In-flight CURL job.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Completion callback.
+ */
+ TALER_MERCHANT_UnitPatchCallback cb;
+
+ /**
+ * Closure for @a cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+
+ /**
+ * Keeps POST body and headers alive.
+ */
+ struct TALER_CURL_PostContext post_ctx;
+};
+
+
+/**
+ * Called when the HTTP transfer finishes.
+ *
+ * @param cls operation handle
+ * @param response_code HTTP status (0 on failure)
+ * @param response parsed JSON reply (NULL if unavailable)
+ */
+static void
+handle_patch_unit_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct TALER_MERCHANT_UnitPatchHandle *uph = cls;
+ const json_t *json = response;
+ struct TALER_MERCHANT_HttpResponse hr = {
+ .http_status = (unsigned int) response_code,
+ .reply = json
+ };
+
+ uph->job = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "PATCH /private/units completed with status %u\n",
+ (unsigned int) response_code);
+ switch (response_code)
+ {
+ case MHD_HTTP_NO_CONTENT:
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ case MHD_HTTP_UNAUTHORIZED:
+ case MHD_HTTP_FORBIDDEN:
+ case MHD_HTTP_NOT_FOUND:
+ case MHD_HTTP_CONFLICT:
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ hr.ec = TALER_JSON_get_error_code (json);
+ hr.hint = TALER_JSON_get_error_hint (json);
+ break;
+ case 0:
+ hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ default:
+ TALER_MERCHANT_parse_error_details_ (json,
+ response_code,
+ &hr);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unexpected response %u/%d for PATCH /private/units\n",
+ (unsigned int) response_code,
+ (int) hr.ec);
+ GNUNET_break_op (0);
+ break;
+ }
+ uph->cb (uph->cb_cls,
+ &hr);
+ TALER_MERCHANT_unit_patch_cancel (uph);
+}
+
+
+struct TALER_MERCHANT_UnitPatchHandle *
+TALER_MERCHANT_unit_patch (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ const json_t *unit_name_long_i18n,
+ const json_t *unit_name_short_i18n,
+ const bool *unit_allow_fraction,
+ const uint32_t *unit_precision_level,
+ const bool *unit_active,
+ TALER_MERCHANT_UnitPatchCallback cb,
+ void *cb_cls)
+{
+ struct TALER_MERCHANT_UnitPatchHandle *uph;
+ json_t *req_obj;
+ char *path;
+
+ req_obj = json_object ();
+ if (NULL == req_obj)
+ return NULL;
+ if (NULL != unit_name_long)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_name_long",
+ json_string (unit_name_long)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (NULL != unit_name_short)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_name_short",
+ json_string (unit_name_short)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (NULL != unit_name_long_i18n)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_name_long_i18n",
+ json_incref ((json_t *) unit_name_long_i18n)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (NULL != unit_name_short_i18n)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_name_short_i18n",
+ json_incref (
+ (json_t *) unit_name_short_i18n)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (NULL != unit_allow_fraction)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_allow_fraction",
+ json_boolean (*unit_allow_fraction)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (NULL != unit_precision_level)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_precision_level",
+ json_integer (
+ (json_int_t) *unit_precision_level)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (NULL != unit_active)
+ {
+ if (0 != json_object_set_new (req_obj,
+ "unit_active",
+ json_boolean (*unit_active)))
+ {
+ json_decref (req_obj);
+ return NULL;
+ }
+ }
+ if (0 == json_object_size (req_obj))
+ {
+ json_decref (req_obj);
+ GNUNET_break (0);
+ return NULL;
+ }
+
+ GNUNET_asprintf (&path,
+ "private/units/%s",
+ unit_id);
+ uph = GNUNET_new (struct TALER_MERCHANT_UnitPatchHandle);
+ uph->ctx = ctx;
+ uph->cb = cb;
+ uph->cb_cls = cb_cls;
+ uph->url = TALER_url_join (backend_url,
+ path,
+ NULL);
+ GNUNET_free (path);
+ if (NULL == uph->url)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to build /private/units/%s URL\n",
+ unit_id);
+ json_decref (req_obj);
+ GNUNET_free (uph);
+ return NULL;
+ }
+ {
+ CURL *eh;
+
+ eh = TALER_MERCHANT_curl_easy_get_ (uph->url);
+ if (GNUNET_OK !=
+ TALER_curl_easy_post (&uph->post_ctx,
+ eh,
+ req_obj))
+ {
+ GNUNET_break (0);
+ curl_easy_cleanup (eh);
+ json_decref (req_obj);
+ GNUNET_free (uph->url);
+ GNUNET_free (uph);
+ return NULL;
+ }
+ json_decref (req_obj);
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_CUSTOMREQUEST,
+ MHD_HTTP_METHOD_PATCH));
+ uph->job = GNUNET_CURL_job_add2 (ctx,
+ eh,
+ uph->post_ctx.headers,
+ &handle_patch_unit_finished,
+ uph);
+ }
+ return uph;
+}
+
+
+void
+TALER_MERCHANT_unit_patch_cancel (struct TALER_MERCHANT_UnitPatchHandle *uph)
+{
+ if (NULL != uph->job)
+ {
+ GNUNET_CURL_job_cancel (uph->job);
+ uph->job = NULL;
+ }
+ TALER_curl_easy_post_finished (&uph->post_ctx);
+ GNUNET_free (uph->url);
+ GNUNET_free (uph);
+}
+
+
+/* end of merchant_api_patch_unit.c */
diff --git a/src/lib/merchant_api_post_orders.c b/src/lib/merchant_api_post_orders.c
@@ -208,12 +208,18 @@ TALER_MERCHANT_orders_post3 (
for (unsigned int i = 0; i<inventory_products_length; i++)
{
json_t *ip;
+ char unit_quantity_buf[64];
+
+ TALER_MERCHANT_format_quantity_string (inventory_products[i].quantity,
+ inventory_products[i].quantity_frac,
+ unit_quantity_buf,
+ sizeof (unit_quantity_buf));
ip = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("product_id",
inventory_products[i].product_id),
- GNUNET_JSON_pack_uint64 ("quantity",
- inventory_products[i].quantity));
+ GNUNET_JSON_pack_string ("unit_quantity",
+ unit_quantity_buf));
GNUNET_assert (NULL != ip);
GNUNET_assert (0 ==
json_array_append_new (ipa,
diff --git a/src/lib/merchant_api_post_products.c b/src/lib/merchant_api_post_products.c
@@ -159,17 +159,21 @@ handle_post_products_finished (void *cls,
struct TALER_MERCHANT_ProductsPostHandle *
-TALER_MERCHANT_products_post3 (
+TALER_MERCHANT_products_post4 (
struct GNUNET_CURL_Context *ctx,
const char *backend_url,
const char *product_id,
const char *description,
const json_t *description_i18n,
const char *unit,
- const struct TALER_Amount *price,
+ const struct TALER_Amount *unit_prices,
+ size_t unit_price_len,
const char *image,
const json_t *taxes,
int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ const uint32_t *unit_precision_level,
const json_t *address,
struct GNUNET_TIME_Timestamp next_restock,
uint32_t minimum_age,
@@ -181,6 +185,12 @@ TALER_MERCHANT_products_post3 (
struct TALER_MERCHANT_ProductsPostHandle *pph;
json_t *req_obj;
json_t *categories;
+ char unit_total_stock_buf[64];
+
+ TALER_MERCHANT_format_stock_string (total_stock,
+ total_stock_frac,
+ unit_total_stock_buf,
+ sizeof (unit_total_stock_buf));
if (0 == num_cats)
{
@@ -195,41 +205,64 @@ TALER_MERCHANT_products_post3 (
json_array_append_new (categories,
json_integer (cats[i])));
}
- req_obj = GNUNET_JSON_PACK (
- GNUNET_JSON_pack_string ("product_id",
- product_id),
- /* FIXME: once we move to the new-style API,
- allow applications to set the product name properly! */
- GNUNET_JSON_pack_string ("product_name",
- description),
- GNUNET_JSON_pack_string ("description",
- description),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_incref ("description_i18n",
- (json_t *) description_i18n)),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_array_steal ("categories",
- categories)),
- GNUNET_JSON_pack_string ("unit",
- unit),
- TALER_JSON_pack_amount ("price",
- price),
- GNUNET_JSON_pack_string ("image",
- image),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_array_incref ("taxes",
- (json_t *) taxes)),
- GNUNET_JSON_pack_uint64 ("total_stock",
- total_stock),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_uint64 ("minimum_age",
- minimum_age)),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_object_incref ("address",
- (json_t *) address)),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_timestamp ("next_restock",
- next_restock)));
+ {
+ req_obj = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("product_id",
+ product_id),
+ /* FIXME: once we move to the new-style API,
+ allow applications to set the product name properly! */
+ GNUNET_JSON_pack_string ("product_name",
+ description),
+ GNUNET_JSON_pack_string ("description",
+ description),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_incref ("description_i18n",
+ (json_t *) description_i18n)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_array_steal ("categories",
+ categories)),
+ GNUNET_JSON_pack_string ("unit",
+ unit),
+ TALER_JSON_pack_amount_array ("unit_price",
+ unit_price_len,
+ unit_prices),
+ GNUNET_JSON_pack_string ("image",
+ image),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_array_incref ("taxes",
+ (json_t *) taxes)),
+ GNUNET_JSON_pack_string ("unit_total_stock",
+ unit_total_stock_buf),
+ GNUNET_JSON_pack_bool ("unit_allow_fraction",
+ unit_allow_fraction),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_uint64 ("minimum_age",
+ minimum_age)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_incref ("address",
+ (json_t *) address)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_timestamp ("next_restock",
+ next_restock)));
+ }
+ if (NULL != unit_precision_level)
+ {
+ GNUNET_assert (0 ==
+ json_object_set_new (req_obj,
+ "unit_precision_level",
+ json_integer (
+ *unit_precision_level)));
+ }
+ if (! unit_allow_fraction)
+ {
+ GNUNET_assert (0 ==
+ json_object_del (req_obj,
+ "unit_allow_fraction"));
+ if (NULL != unit_precision_level)
+ GNUNET_assert (0 ==
+ json_object_del (req_obj,
+ "unit_precision_level"));
+ }
pph = GNUNET_new (struct TALER_MERCHANT_ProductsPostHandle);
pph->ctx = ctx;
pph->cb = cb;
@@ -266,6 +299,50 @@ TALER_MERCHANT_products_post3 (
struct TALER_MERCHANT_ProductsPostHandle *
+TALER_MERCHANT_products_post3 (
+ struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *product_id,
+ const char *description,
+ const json_t *description_i18n,
+ const char *unit,
+ const struct TALER_Amount *price,
+ const char *image,
+ const json_t *taxes,
+ int64_t total_stock,
+ const json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ uint32_t minimum_age,
+ unsigned int num_cats,
+ const uint64_t *cats,
+ TALER_MERCHANT_ProductsPostCallback cb,
+ void *cb_cls)
+{
+ return TALER_MERCHANT_products_post4 (ctx,
+ backend_url,
+ product_id,
+ description,
+ description_i18n,
+ unit,
+ price,
+ 1,
+ image,
+ taxes,
+ total_stock,
+ 0,
+ false,
+ NULL,
+ address,
+ next_restock,
+ minimum_age,
+ num_cats,
+ cats,
+ cb,
+ cb_cls);
+}
+
+
+struct TALER_MERCHANT_ProductsPostHandle *
TALER_MERCHANT_products_post2 (
struct GNUNET_CURL_Context *ctx,
const char *backend_url,
diff --git a/src/lib/merchant_api_post_units.c b/src/lib/merchant_api_post_units.c
@@ -0,0 +1,218 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Lesser General Public License as published by the Free
+ Software Foundation; either version 2.1, 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ TALER; see the file COPYING.LGPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file merchant_api_post_units.c
+ * @brief Implementation of POST /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h>
+#include <gnunet/gnunet_util_lib.h>
+#include "taler_merchant_service.h"
+#include "merchant_api_curl_defaults.h"
+#include "merchant_api_common.h"
+#include <taler/taler_json_lib.h>
+#include <taler/taler_curl_lib.h>
+
+
+/**
+ * Handle for a POST /private/units operation.
+ */
+struct TALER_MERCHANT_UnitsPostHandle
+{
+ /**
+ * Fully qualified request URL.
+ */
+ char *url;
+
+ /**
+ * CURL job handle.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Callback to invoke with the result.
+ */
+ TALER_MERCHANT_UnitsPostCallback cb;
+
+ /**
+ * Closure for @a cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+
+ /**
+ * Helper keeping POST body and headers alive.
+ */
+ struct TALER_CURL_PostContext post_ctx;
+};
+
+
+/**
+ * Called when the HTTP transfer finishes.
+ *
+ * @param cls operation handle
+ * @param response_code HTTP status (0 on network / parsing failures)
+ * @param response parsed JSON reply (NULL if unavailable)
+ */
+static void
+handle_post_units_finished (void *cls,
+ long response_code,
+ const void *response)
+{
+ struct TALER_MERCHANT_UnitsPostHandle *uph = cls;
+ const json_t *json = response;
+ struct TALER_MERCHANT_HttpResponse hr = {
+ .http_status = (unsigned int) response_code,
+ .reply = json
+ };
+
+ uph->job = NULL;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "POST /private/units completed with status %u\n",
+ (unsigned int) response_code);
+ switch (response_code)
+ {
+ case MHD_HTTP_NO_CONTENT:
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ case MHD_HTTP_UNAUTHORIZED:
+ case MHD_HTTP_FORBIDDEN:
+ case MHD_HTTP_NOT_FOUND:
+ case MHD_HTTP_CONFLICT:
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ hr.ec = TALER_JSON_get_error_code (json);
+ hr.hint = TALER_JSON_get_error_hint (json);
+ break;
+ case 0:
+ hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+ break;
+ default:
+ TALER_MERCHANT_parse_error_details_ (json,
+ response_code,
+ &hr);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unexpected response %u/%d for POST /private/units\n",
+ (unsigned int) response_code,
+ (int) hr.ec);
+ GNUNET_break_op (0);
+ break;
+ }
+ uph->cb (uph->cb_cls,
+ &hr);
+ TALER_MERCHANT_units_post_cancel (uph);
+}
+
+
+struct TALER_MERCHANT_UnitsPostHandle *
+TALER_MERCHANT_units_post (struct GNUNET_CURL_Context *ctx,
+ const char *backend_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ bool unit_allow_fraction,
+ uint32_t unit_precision_level,
+ bool unit_active,
+ const json_t *unit_name_long_i18n,
+ const json_t *unit_name_short_i18n,
+ TALER_MERCHANT_UnitsPostCallback cb,
+ void *cb_cls)
+{
+ struct TALER_MERCHANT_UnitsPostHandle *uph;
+ json_t *req_obj;
+
+ req_obj = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("unit",
+ unit_id),
+ GNUNET_JSON_pack_string ("unit_name_long",
+ unit_name_long),
+ GNUNET_JSON_pack_string ("unit_name_short",
+ unit_name_short),
+ GNUNET_JSON_pack_bool ("unit_allow_fraction",
+ unit_allow_fraction),
+ GNUNET_JSON_pack_uint64 ("unit_precision_level",
+ (uint64_t) unit_precision_level),
+ GNUNET_JSON_pack_bool ("unit_active",
+ unit_active),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_incref ("unit_name_long_i18n",
+ (json_t *) unit_name_long_i18n)),
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_object_incref ("unit_name_short_i18n",
+ (json_t *) unit_name_short_i18n)));
+ uph = GNUNET_new (struct TALER_MERCHANT_UnitsPostHandle);
+ uph->ctx = ctx;
+ uph->cb = cb;
+ uph->cb_cls = cb_cls;
+ uph->url = TALER_url_join (backend_url,
+ "private/units",
+ NULL);
+ if (NULL == uph->url)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to build /private/units URL\n");
+ json_decref (req_obj);
+ GNUNET_free (uph);
+ return NULL;
+ }
+ {
+ CURL *eh;
+
+ eh = TALER_MERCHANT_curl_easy_get_ (uph->url);
+ if (GNUNET_OK !=
+ TALER_curl_easy_post (&uph->post_ctx,
+ eh,
+ req_obj))
+ {
+ GNUNET_break (0);
+ curl_easy_cleanup (eh);
+ json_decref (req_obj);
+ GNUNET_free (uph->url);
+ GNUNET_free (uph);
+ return NULL;
+ }
+ json_decref (req_obj);
+ uph->job = GNUNET_CURL_job_add2 (ctx,
+ eh,
+ uph->post_ctx.headers,
+ &handle_post_units_finished,
+ uph);
+ }
+ return uph;
+}
+
+
+void
+TALER_MERCHANT_units_post_cancel (struct TALER_MERCHANT_UnitsPostHandle *uph)
+{
+ if (NULL != uph->job)
+ {
+ GNUNET_CURL_job_cancel (uph->job);
+ uph->job = NULL;
+ }
+ TALER_curl_easy_post_finished (&uph->post_ctx);
+ GNUNET_free (uph->url);
+ GNUNET_free (uph);
+}
+
+
+/* end of merchant_api_post_units.c */
diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am
@@ -1,5 +1,5 @@
# This Makefile.am is in the public domain
-AM_CPPFLAGS = -I$(top_srcdir)/src/include -I$(top_srcdir)/src/backend
+AM_CPPFLAGS = -I$(top_srcdir)/src/include -I$(top_srcdir)/src/backend -I$(top_srcdir)/src/lib
if USE_COVERAGE
AM_CFLAGS = --coverage -O0
@@ -45,6 +45,8 @@ libtalermerchanttesting_la_SOURCES = \
testing_api_cmd_get_transfers.c \
testing_api_cmd_get_templates.c \
testing_api_cmd_get_template.c \
+ testing_api_cmd_get_unit.c \
+ testing_api_cmd_get_units.c \
testing_api_cmd_get_webhooks.c \
testing_api_cmd_get_webhook.c \
testing_api_cmd_delete_account.c \
@@ -53,6 +55,7 @@ libtalermerchanttesting_la_SOURCES = \
testing_api_cmd_delete_otp_device.c \
testing_api_cmd_delete_product.c \
testing_api_cmd_delete_template.c \
+ testing_api_cmd_delete_unit.c \
testing_api_cmd_delete_webhook.c \
testing_api_cmd_delete_transfer.c \
testing_api_cmd_forget_order.c \
@@ -65,6 +68,7 @@ libtalermerchanttesting_la_SOURCES = \
testing_api_cmd_patch_otp_device.c \
testing_api_cmd_patch_product.c \
testing_api_cmd_patch_template.c \
+ testing_api_cmd_patch_unit.c \
testing_api_cmd_patch_webhook.c \
testing_api_cmd_pay_order.c \
testing_api_cmd_post_account.c \
@@ -75,6 +79,7 @@ libtalermerchanttesting_la_SOURCES = \
testing_api_cmd_post_products.c \
testing_api_cmd_post_transfers.c \
testing_api_cmd_post_templates.c \
+ testing_api_cmd_post_units.c \
testing_api_cmd_post_tokenfamilies.c \
testing_api_cmd_post_using_templates.c \
testing_api_cmd_post_webhooks.c \
diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c
@@ -36,6 +36,7 @@
#include <taler/taler_fakebank_lib.h>
#include <taler/taler_testing_lib.h>
#include <taler/taler_error_codes.h>
+#include "taler_merchant_util.h"
#include "taler_merchant_testing_lib.h"
#ifdef HAVE_DONAU_DONAU_SERVICE_H
@@ -142,6 +143,22 @@ static const char *order_1_forgets_3[] = {
NULL
};
+static const struct TALER_TESTING_ProductUnitExpectations
+ expect_unicorn_defaults = {
+ .have_unit_allow_fraction = true,
+ .unit_allow_fraction = true,
+ .have_unit_precision_level = true,
+ .unit_precision_level = 1
+};
+
+static const struct TALER_TESTING_ProductUnitExpectations
+ expect_unicorn_patched = {
+ .have_unit_allow_fraction = true,
+ .unit_allow_fraction = false,
+ .have_unit_precision_level = true,
+ .unit_precision_level = 0
+};
+
/**
* Execute the taler-merchant-webhook command with
* our configuration file.
@@ -628,6 +645,30 @@ run (void *cls,
NULL,
GNUNET_TIME_UNIT_FOREVER_TS,
MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_products3 ("post-products-frac",
+ merchant_url,
+ "product-frac",
+ "fractional product",
+ json_pack ("{s:s}",
+ "en",
+ "fractional product"),
+ "kg",
+ "EUR:1.5",
+ "",
+ json_array (),
+ 1,
+ (MERCHANT_UNIT_FRAC_BASE * 3)
+ / 4,
+ true,
+ 0,
+ NULL,
+ GNUNET_TIME_UNIT_ZERO_TS,
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_product ("get-product-frac-post",
+ merchant_url,
+ "product-frac",
+ MHD_HTTP_OK,
+ "post-products-frac"),
TALER_TESTING_cmd_merchant_patch_product ("patch-products-p3",
merchant_url,
"product-3",
@@ -644,12 +685,286 @@ run (void *cls,
(
GNUNET_TIME_UNIT_MINUTES),
MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_patch_product2 ("patch-product-frac",
+ merchant_url,
+ "product-frac",
+ "fractional product patched",
+ json_pack ("{s:s}",
+ "en",
+ "fractional product patched"),
+ "kg",
+ "EUR:1.6",
+ "",
+ json_array (),
+ 2,
+ MERCHANT_UNIT_FRAC_BASE / 4,
+ true,
+ 0,
+ json_object (),
+ GNUNET_TIME_relative_to_timestamp (
+ GNUNET_TIME_UNIT_MINUTES),
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_product ("get-product-frac-patched",
+ merchant_url,
+ "product-frac",
+ MHD_HTTP_OK,
+ "patch-product-frac"),
+ TALER_TESTING_cmd_merchant_delete_unit ("delete-unit-piece-denied",
+ merchant_url,
+ "Piece",
+ MHD_HTTP_CONFLICT),
+ TALER_TESTING_cmd_merchant_post_units ("post-unit-piece-conflict",
+ merchant_url,
+ "Piece",
+ "piece",
+ "pc",
+ false,
+ 0,
+ true,
+ NULL,
+ NULL,
+ MHD_HTTP_CONFLICT),
+ TALER_TESTING_cmd_merchant_post_units ("post-unit-bucket",
+ merchant_url,
+ "BucketCustomUnit",
+ "bucket",
+ "bkt",
+ false,
+ 0,
+ true,
+ json_pack ("{s:s}", "en", "bucket"),
+ json_pack ("{s:s}", "en", "bkt"),
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_unit ("get-unit-bucket",
+ merchant_url,
+ "BucketCustomUnit",
+ MHD_HTTP_OK,
+ "post-unit-bucket"),
+ TALER_TESTING_cmd_merchant_get_units ("get-units-bucket-list",
+ merchant_url,
+ MHD_HTTP_OK,
+ "post-unit-bucket",
+ NULL),
+ TALER_TESTING_cmd_merchant_patch_unit ("patch-unit-bucket",
+ merchant_url,
+ "BucketCustomUnit",
+ "bucket-updated",
+ "bkt-upd",
+ json_pack ("{s:s}", "en",
+ "bucket-updated"),
+ json_pack ("{s:s}", "en", "bkt-upd"),
+ true,
+ 0,
+ false,
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_unit ("get-unit-bucket-patched",
+ merchant_url,
+ "BucketCustomUnit",
+ MHD_HTTP_OK,
+ "patch-unit-bucket"),
+ TALER_TESTING_cmd_merchant_get_units ("get-units-bucket-updated",
+ merchant_url,
+ MHD_HTTP_OK,
+ "patch-unit-bucket",
+ NULL),
+ TALER_TESTING_cmd_merchant_delete_unit ("delete-unit-bucket",
+ merchant_url,
+ "BucketCustomUnit",
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_unit ("get-unit-bucket-deleted",
+ merchant_url,
+ "BucketCustomUnit",
+ MHD_HTTP_NOT_FOUND,
+ NULL),
+ TALER_TESTING_cmd_merchant_post_units ("post-unit-unicorn",
+ merchant_url,
+ "UnitUnicorn",
+ "unicorn",
+ "uni",
+ true,
+ 1,
+ true,
+ json_pack ("{s:s}", "en", "unicorn"),
+ json_pack ("{s:s}", "en", "uni"),
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_products2 ("post-product-unicorn",
+ merchant_url,
+ "product-unicorn",
+ "a unicorn snack",
+ json_pack ("{s:s}",
+ "en",
+ "a unicorn snack"),
+ "UnitUnicorn",
+ "EUR:3.5",
+ "",
+ json_array (),
+ 7,
+ 0,
+ json_object (),
+ GNUNET_TIME_UNIT_ZERO_TS,
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_product2 ("get-product-unicorn-default",
+ merchant_url,
+ "product-unicorn",
+ MHD_HTTP_OK,
+ "post-product-unicorn",
+ &expect_unicorn_defaults),
+ TALER_TESTING_cmd_merchant_patch_unit ("patch-unit-unicorn",
+ merchant_url,
+ "UnitUnicorn",
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ false,
+ 0,
+ true,
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_get_product2 ("get-product-unicorn-patched",
+ merchant_url,
+ "product-unicorn",
+ MHD_HTTP_OK,
+ "post-product-unicorn",
+ &expect_unicorn_patched),
+ TALER_TESTING_cmd_merchant_delete_product ("delete-product-unicorn",
+ merchant_url,
+ "product-unicorn",
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_delete_unit ("delete-unit-unicorn",
+ merchant_url,
+ "UnitUnicorn",
+ MHD_HTTP_NO_CONTENT),
TALER_TESTING_cmd_merchant_lock_product ("lock-product-p3",
merchant_url,
"product-3",
GNUNET_TIME_UNIT_MINUTES,
2,
MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_lock_product2 (
+ "lock-product-p3-float-denied",
+ merchant_url,
+ "product-3",
+ GNUNET_TIME_UNIT_MINUTES,
+ 1,
+ MERCHANT_UNIT_FRAC_BASE / 2,
+ true,
+ MHD_HTTP_BAD_REQUEST),
+ TALER_TESTING_cmd_merchant_post_orders2 ("create-proposal-p3-float-denied",
+ cred.cfg,
+ merchant_url,
+ MHD_HTTP_BAD_REQUEST,
+ "order-p3-float-denied",
+ GNUNET_TIME_UNIT_ZERO_TS,
+ GNUNET_TIME_UNIT_FOREVER_TS,
+ true,
+ "EUR:5.0",
+ "x-taler-bank",
+ "product-3/1.5",
+ "",
+ NULL),
+ TALER_TESTING_cmd_merchant_patch_product2 (
+ "patch-product-3-allow-float",
+ merchant_url,
+ "product-3",
+ "a product allow fractional",
+ json_pack ("{s:s}",
+ "en",
+ "a product allow fractional"),
+ "can",
+ "EUR:1",
+ "data:image/jpeg;base64,RAWDATA",
+ json_array (),
+ 5,
+ 0,
+ true,
+ 0,
+ json_object (),
+ GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_MINUTES),
+ MHD_HTTP_NO_CONTENT),
+ cmd_transfer_to_exchange ("create-reserve-p3-float",
+ "EUR:5.01"),
+ cmd_exec_wirewatch ("wirewatch-p3-float"),
+ TALER_TESTING_cmd_check_bank_admin_transfer (
+ "check_bank_transfer-p3-float",
+ "EUR:5.01",
+ payer_payto,
+ exchange_payto,
+ "create-reserve-p3-float"),
+ TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-p3-float",
+ "create-reserve-p3-float",
+ "EUR:5",
+ 0,
+ MHD_HTTP_OK),
+ TALER_TESTING_cmd_merchant_lock_product2 (
+ "lock-product-p3-float",
+ merchant_url,
+ "product-3",
+ GNUNET_TIME_UNIT_MINUTES,
+ 1,
+ MERCHANT_UNIT_FRAC_BASE / 2,
+ true,
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_orders2 ("create-proposal-p3-float",
+ cred.cfg,
+ merchant_url,
+ MHD_HTTP_OK,
+ "order-p3-float",
+ GNUNET_TIME_UNIT_ZERO_TS,
+ GNUNET_TIME_UNIT_FOREVER_TS,
+ true,
+ "EUR:5.0",
+ "x-taler-bank",
+ "product-3/1.5",
+ "",
+ NULL),
+ TALER_TESTING_cmd_merchant_claim_order ("claim-order-p3-float",
+ merchant_url,
+ MHD_HTTP_OK,
+ "create-proposal-p3-float",
+ NULL),
+ TALER_TESTING_cmd_merchant_pay_order ("pay-order-p3-float",
+ merchant_url,
+ MHD_HTTP_OK,
+ "create-proposal-p3-float",
+ "withdraw-coin-p3-float",
+ "EUR:5",
+ "EUR:4.99",
+ "session-p3-float"),
+ TALER_TESTING_cmd_sleep (
+ "Wait for wire transfer deadline-p3-float",
+ 3),
+ CMD_EXEC_AGGREGATOR ("run-aggregator-p3-float"),
+ TALER_TESTING_cmd_check_bank_transfer ("check_bank_transfer-p3-float",
+ EXCHANGE_URL,
+ "EUR:4.98",
+ exchange_payto,
+ merchant_payto),
+ TALER_TESTING_cmd_merchant_get_order ("get-order-merchant-p3-float-paid",
+ merchant_url,
+ "create-proposal-p3-float",
+ TALER_MERCHANT_OSC_PAID,
+ false,
+ MHD_HTTP_OK,
+ NULL),
+ TALER_TESTING_cmd_merchant_patch_product2 (
+ "patch-product-3-restore",
+ merchant_url,
+ "product-3",
+ "a product",
+ json_pack ("{s:s}",
+ "en",
+ "a product"),
+ "can",
+ "EUR:1",
+ "data:image/jpeg;base64,RAWDATA",
+ json_array (),
+ 5,
+ 0,
+ false,
+ 0,
+ json_object (),
+ GNUNET_TIME_relative_to_timestamp (GNUNET_TIME_UNIT_MINUTES),
+ MHD_HTTP_NO_CONTENT),
TALER_TESTING_cmd_merchant_post_orders2 ("create-proposal-p3-wm-nx",
cred.cfg,
merchant_url,
diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh
@@ -391,7 +391,7 @@ echo "OK"
echo -n "Creating product..."
STATUS=$(curl 'http://localhost:9966/private/products' \
- -d '{"product_id":"2","description":"product with id 2 and price :15","price":"TESTKUDOS:15","total_stock":2,"description_i18n":{},"unit":"","image":"'$RANDOM_IMG'","taxes":[],"address":{},"next_restock":{"t_s":"never"}}' \
+ -d '{"product_id":"2","description":"product with id 2 and price :15","unit_price":["TESTKUDOS:15"],"unit_total_stock":"2","description_i18n":{},"unit":"","image":"'$RANDOM_IMG'","taxes":[],"address":{},"next_restock":{"t_s":"never"}}' \
-w "%{http_code}" -s -o /dev/null)
if [ "$STATUS" != "204" ]
@@ -402,7 +402,7 @@ echo "OK"
echo -n "Creating order with inventory products..."
STATUS=$(curl 'http://localhost:9966/private/orders' \
- -d '{"order":{"amount":"TESTKUDOS:7","summary":"3"},"inventory_products":[{"product_id":"2","quantity":1}]}' \
+ -d '{"order":{"amount":"TESTKUDOS:7","summary":"3"},"inventory_products":[{"product_id":"2","unit_quantity":"1"}]}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
@@ -440,7 +440,7 @@ echo "OK"
STATUS=$(curl 'http://localhost:9966/private/products' \
- -d '{"product_id":"1","description":"product with id 1 and price :15","price":"USD:15","total_stock":1,"description_i18n":{},"unit":"","image":"","taxes":[],"address":{},"next_restock":{"t_s":"never"}}' \
+ -d '{"product_id":"1","description":"product with id 1 and price :15","unit_price":["USD:15"],"unit_total_stock":"1","description_i18n":{},"unit":"","image":"","taxes":[],"address":{},"next_restock":{"t_s":"never"}}' \
-w "%{http_code}" -s -o /dev/null)
if [ "$STATUS" != "204" ]
@@ -454,7 +454,7 @@ fi
echo -n "Creating order to be paid..."
STATUS=$(curl 'http://localhost:9966/private/orders' \
- -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"inventory_products":[{"product_id":"2","quantity":1}]}' \
+ -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"inventory_products":[{"product_id":"2","unit_quantity":"1"}]}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "200" ]
diff --git a/src/testing/test_merchant_product_creation.sh b/src/testing/test_merchant_product_creation.sh
@@ -89,8 +89,8 @@ fi
echo "OK"
RANDOM_IMG='data:image/png;base64,abcdefg'
-INFINITE_PRODUCT_TEMPLATE='{"product_id":"2","product_name":"stuff","description":"product with id 2 and price :15","price":"TESTKUDOS:15","total_stock":-1,"unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}'
-MANAGED_PRODUCT_TEMPLATE='{"product_id":"3","product_name":"more stuff","description":"product with id 3 and price :10","price":"TESTKUDOS:150","total_stock":2,"unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}'
+INFINITE_PRODUCT_TEMPLATE='{"product_id":"2","product_name":"stuff","description":"product with id 2 and price :15","unit_price":["TESTKUDOS:15"],"unit_total_stock":"-1","unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}'
+MANAGED_PRODUCT_TEMPLATE='{"product_id":"3","product_name":"more stuff","description":"product with id 3 and price :10","unit_price":["TESTKUDOS:150"],"unit_total_stock":"2","unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}'
echo -n "Creating products..."
STATUS=$(curl 'http://localhost:9966/private/products' \
@@ -143,7 +143,7 @@ MANAGED_PRODUCT_ID=$(echo "$MANAGED_PRODUCT_TEMPLATE" | jq -r '.product_id')
echo -n "Locking inventory ..."
STATUS=$(curl "http://localhost:9966/private/products/${MANAGED_PRODUCT_ID}/lock" \
- -d '{"lock_uuid":"luck","duration":{"d_us": 100000000},"quantity":10}' \
+ -d '{"lock_uuid":"luck","duration":{"d_us": 100000000},"unit_quantity":"10"}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "410" ]
@@ -155,7 +155,7 @@ fi
echo -n "."
STATUS=$(curl "http://localhost:9966/private/products/${MANAGED_PRODUCT_ID}/lock" \
- -d '{"lock_uuid":"luck","duration":{"d_us": 100000000},"quantity":1}' \
+ -d '{"lock_uuid":"luck","duration":{"d_us": 100000000},"unit_quantity":"1"}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "204" ]
@@ -167,7 +167,7 @@ echo " OK"
echo -n "Creating order to be paid..."
STATUS=$(curl 'http://localhost:9966/private/orders' \
- -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"inventory_products":[{"product_id":"'"${MANAGED_PRODUCT_ID}"'","quantity":1}]}' \
+ -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"inventory_products":[{"product_id":"'"${MANAGED_PRODUCT_ID}"'","unit_quantity":"1"}]}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "200" ]
@@ -192,7 +192,7 @@ PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE")
echo -n "."
STATUS=$(curl 'http://localhost:9966/private/orders' \
- -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"inventory_products":[{"product_id":"'"${MANAGED_PRODUCT_ID}"'","quantity":1}]}' \
+ -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"inventory_products":[{"product_id":"'"${MANAGED_PRODUCT_ID}"'","unit_quantity":"1"}]}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "410" ]
@@ -205,7 +205,7 @@ echo -n "."
# Using the 'luck' inventory lock, order creation should work.
STATUS=$(curl 'http://localhost:9966/private/orders' \
- -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"lock_uuids":["luck"],"inventory_products":[{"product_id":"'"$MANAGED_PRODUCT_ID"'","quantity":1}]}' \
+ -d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"},"lock_uuids":["luck"],"inventory_products":[{"product_id":"'"$MANAGED_PRODUCT_ID"'","unit_quantity":"1"}]}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "200" ]
diff --git a/src/testing/testing_api_cmd_delete_unit.c b/src/testing/testing_api_cmd_delete_unit.c
@@ -0,0 +1,176 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+*/
+/**
+ * @file testing_api_cmd_delete_unit.c
+ * @brief command to test DELETE /private/units/$ID
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_testing_lib.h>
+#include "taler_merchant_service.h"
+#include "taler_merchant_testing_lib.h"
+
+
+/**
+ * State for a DELETE /private/units/$ID command.
+ */
+struct DeleteUnitState
+{
+ /**
+ * In-flight request handle.
+ */
+ struct TALER_MERCHANT_UnitDeleteHandle *udh;
+
+ /**
+ * Interpreter context.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Merchant backend base URL.
+ */
+ const char *merchant_url;
+
+ /**
+ * Unit identifier to delete.
+ */
+ const char *unit_id;
+
+ /**
+ * Expected HTTP status.
+ */
+ unsigned int http_status;
+};
+
+
+/**
+ * Completion callback.
+ */
+static void
+delete_unit_cb (void *cls,
+ const struct TALER_MERCHANT_HttpResponse *hr)
+{
+ struct DeleteUnitState *dus = cls;
+
+ dus->udh = NULL;
+ if (dus->http_status != hr->http_status)
+ {
+ TALER_TESTING_unexpected_status_with_body (dus->is,
+ hr->http_status,
+ dus->http_status,
+ hr->reply);
+ return;
+ }
+ TALER_TESTING_interpreter_next (dus->is);
+}
+
+
+/**
+ * Issue DELETE request.
+ */
+static void
+delete_unit_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct DeleteUnitState *dus = cls;
+
+ dus->is = is;
+ dus->udh = TALER_MERCHANT_unit_delete (
+ TALER_TESTING_interpreter_get_context (is),
+ dus->merchant_url,
+ dus->unit_id,
+ &delete_unit_cb,
+ dus);
+ if (NULL == dus->udh)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ }
+}
+
+
+/**
+ * Provide traits.
+ */
+static enum GNUNET_GenericReturnValue
+delete_unit_traits (void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct DeleteUnitState *dus = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_unit_id (dus->unit_id),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+}
+
+
+/**
+ * Cleanup.
+ */
+static void
+delete_unit_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct DeleteUnitState *dus = cls;
+
+ if (NULL != dus->udh)
+ {
+ TALER_MERCHANT_unit_delete_cancel (dus->udh);
+ dus->udh = NULL;
+ }
+ GNUNET_free (dus);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_delete_unit (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ unsigned int http_status)
+{
+ struct DeleteUnitState *dus;
+
+ dus = GNUNET_new (struct DeleteUnitState);
+ dus->merchant_url = merchant_url;
+ dus->unit_id = unit_id;
+ dus->http_status = http_status;
+
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = dus,
+ .label = label,
+ .run = &delete_unit_run,
+ .cleanup = &delete_unit_cleanup,
+ .traits = &delete_unit_traits
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_delete_unit.c */
diff --git a/src/testing/testing_api_cmd_get_product.c b/src/testing/testing_api_cmd_get_product.c
@@ -64,6 +64,11 @@ struct GetProductState
*/
unsigned int http_status;
+ /**
+ * Optional overrides for fractional fields.
+ */
+ const struct TALER_TESTING_ProductUnitExpectations *unit_expectations;
+
};
@@ -79,6 +84,8 @@ get_product_cb (void *cls,
{
struct GetProductState *gis = cls;
const struct TALER_TESTING_Command *product_cmd;
+ const struct TALER_TESTING_ProductUnitExpectations *ue =
+ gis->unit_expectations;
gis->igh = NULL;
if (gis->http_status != pgr->hr.http_status)
@@ -149,6 +156,112 @@ get_product_cb (void *cls,
}
}
{
+ const bool *expected_allow;
+ bool have_allow = GNUNET_OK ==
+ TALER_TESTING_get_trait_product_unit_allow_fraction (
+ product_cmd,
+ &expected_allow);
+ bool override_allow = (NULL != ue) &&
+ ue->have_unit_allow_fraction;
+
+ if (override_allow)
+ {
+ if (pgr->details.ok.unit_allow_fraction !=
+ ue->unit_allow_fraction)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional flag does not match expectation\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ else if (! have_allow)
+ {
+ if (pgr->details.ok.unit_allow_fraction)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional flag unexpected\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ else
+ {
+ if (pgr->details.ok.unit_allow_fraction != *expected_allow)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional flag does not match\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ {
+ const char *expected_unit_total_stock;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_product_unit_total_stock (
+ product_cmd,
+ &expected_unit_total_stock))
+ TALER_TESTING_interpreter_fail (gis->is);
+ else if (0 != strcmp (pgr->details.ok.unit_total_stock,
+ expected_unit_total_stock))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product stock string does not match\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ }
+ }
+ {
+ const uint32_t *expected_precision;
+ bool have_precision = GNUNET_OK ==
+ TALER_TESTING_get_trait_product_unit_precision_level (
+ product_cmd,
+ &expected_precision);
+ bool override_precision = (NULL != ue) &&
+ ue->have_unit_precision_level;
+
+ if (override_precision)
+ {
+ if (pgr->details.ok.unit_precision_level !=
+ ue->unit_precision_level)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional precision does not match expectation\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ else if (have_precision)
+ {
+ if (pgr->details.ok.unit_precision_level != *expected_precision)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional precision does not match\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ else if (! pgr->details.ok.unit_allow_fraction)
+ {
+ if (0 != pgr->details.ok.unit_precision_level)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional precision should be zero when disallowed\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ else if (pgr->details.ok.unit_precision_level > 8)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product fractional precision exceeds supported range\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
+ }
+ {
const char *expected_image;
if (GNUNET_OK !=
@@ -203,13 +316,16 @@ get_product_cb (void *cls,
TALER_TESTING_get_trait_address (product_cmd,
&expected_location))
TALER_TESTING_interpreter_fail (gis->is);
- if (1 != json_equal (pgr->details.ok.location,
- expected_location))
+ if (NULL != expected_location)
{
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Product location does not match\n");
- TALER_TESTING_interpreter_fail (gis->is);
- return;
+ if (1 != json_equal (pgr->details.ok.location,
+ expected_location))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product location does not match\n");
+ TALER_TESTING_interpreter_fail (gis->is);
+ return;
+ }
}
}
{
@@ -314,6 +430,24 @@ TALER_TESTING_cmd_merchant_get_product (const char *label,
unsigned int http_status,
const char *product_reference)
{
+ return TALER_TESTING_cmd_merchant_get_product2 (label,
+ merchant_url,
+ product_id,
+ http_status,
+ product_reference,
+ NULL);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_get_product2 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ unsigned int http_status,
+ const char *product_reference,
+ const struct TALER_TESTING_ProductUnitExpectations *unit_expectations)
+{
struct GetProductState *gis;
gis = GNUNET_new (struct GetProductState);
@@ -321,6 +455,7 @@ TALER_TESTING_cmd_merchant_get_product (const char *label,
gis->product_id = product_id;
gis->http_status = http_status;
gis->product_reference = product_reference;
+ gis->unit_expectations = unit_expectations;
{
struct TALER_TESTING_Command cmd = {
.cls = gis,
diff --git a/src/testing/testing_api_cmd_get_unit.c b/src/testing/testing_api_cmd_get_unit.c
@@ -0,0 +1,352 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+*/
+/**
+ * @file testing_api_cmd_get_unit.c
+ * @brief command to test GET /private/units/$ID
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <taler/taler_testing_lib.h>
+#include "taler_merchant_service.h"
+#include "taler_merchant_testing_lib.h"
+
+
+/**
+ * State for a GET /private/units/$ID command.
+ */
+struct GetUnitState
+{
+ /**
+ * In-flight request handle.
+ */
+ struct TALER_MERCHANT_UnitGetHandle *ugh;
+
+ /**
+ * Interpreter context.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Merchant backend base URL.
+ */
+ const char *merchant_url;
+
+ /**
+ * Unit identifier to fetch.
+ */
+ const char *unit_id;
+
+ /**
+ * Expected HTTP status.
+ */
+ unsigned int http_status;
+
+ /**
+ * Optional command label providing expected traits.
+ */
+ const char *reference;
+};
+
+
+/**
+ * Compare response @a entry with traits from @a ref_cmd.
+ */
+static bool
+unit_matches_reference (const struct TALER_MERCHANT_UnitEntry *entry,
+ const struct TALER_TESTING_Command *ref_cmd)
+{
+ const char *unit_id;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_unit_id (ref_cmd,
+ &unit_id))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit trait resolution failed for reference `%s'\n",
+ ref_cmd->label);
+ return false;
+ }
+ if (0 != strcmp (entry->unit,
+ unit_id))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit mismatch: expected id %s got %s\n",
+ unit_id,
+ entry->unit);
+ return false;
+ }
+
+ {
+ const char *unit_name_long = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_long (ref_cmd,
+ &unit_name_long))
+ {
+ if ( (NULL != unit_name_long) &&
+ (0 != strcmp (entry->unit_name_long,
+ unit_name_long)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected long label '%s' got '%s'\n",
+ entry->unit,
+ unit_name_long,
+ entry->unit_name_long);
+ return false;
+ }
+ }
+ }
+ {
+ const char *unit_name_short = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_short (ref_cmd,
+ &unit_name_short))
+ {
+ if ( (NULL != unit_name_short) &&
+ (0 != strcmp (entry->unit_name_short,
+ unit_name_short)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected short label '%s' got '%s'\n",
+ entry->unit,
+ unit_name_short,
+ entry->unit_name_short);
+ return false;
+ }
+ }
+ }
+ {
+ const bool *unit_allow_fraction = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_allow_fraction (ref_cmd,
+ &unit_allow_fraction))
+ {
+ if ( (NULL != unit_allow_fraction) &&
+ (*unit_allow_fraction != entry->unit_allow_fraction) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected allow_fraction %d got %d\n",
+ entry->unit,
+ (int) *unit_allow_fraction,
+ (int) entry->unit_allow_fraction);
+ return false;
+ }
+ }
+ }
+ {
+ const uint32_t *unit_precision_level = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_precision_level (ref_cmd,
+ &unit_precision_level))
+ {
+ if ( (NULL != unit_precision_level) &&
+ (*unit_precision_level != entry->unit_precision_level) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected precision %u got %u\n",
+ entry->unit,
+ (unsigned int) *unit_precision_level,
+ (unsigned int) entry->unit_precision_level);
+ return false;
+ }
+ }
+ }
+ {
+ const bool *unit_active = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_active (ref_cmd,
+ &unit_active))
+ {
+ if ( (NULL != unit_active) &&
+ (*unit_active != entry->unit_active) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected active flag %d got %d\n",
+ entry->unit,
+ (int) *unit_active,
+ (int) entry->unit_active);
+ return false;
+ }
+ }
+ }
+ {
+ const json_t *unit_name_long_i18n = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_long_i18n (ref_cmd,
+ &unit_name_long_i18n))
+ {
+ if ( (NULL != unit_name_long_i18n) &&
+ ( (NULL == entry->unit_name_long_i18n) ||
+ (1 != json_equal (unit_name_long_i18n,
+ entry->unit_name_long_i18n)) ) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: long_i18n differs\n",
+ entry->unit);
+ return false;
+ }
+ }
+ }
+ {
+ const json_t *unit_name_short_i18n = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_short_i18n (ref_cmd,
+ &unit_name_short_i18n))
+ {
+ if ( (NULL != unit_name_short_i18n) &&
+ ( (NULL == entry->unit_name_short_i18n) ||
+ (1 != json_equal (unit_name_short_i18n,
+ entry->unit_name_short_i18n)) ) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: short_i18n differs\n",
+ entry->unit);
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+
+/**
+ * Completion callback.
+ */
+static void
+get_unit_cb (void *cls,
+ const struct TALER_MERCHANT_UnitGetResponse *ugr)
+{
+ struct GetUnitState *gug = cls;
+
+ gug->ugh = NULL;
+ if (gug->http_status != ugr->hr.http_status)
+ {
+ TALER_TESTING_unexpected_status_with_body (gug->is,
+ ugr->hr.http_status,
+ gug->http_status,
+ ugr->hr.reply);
+ return;
+ }
+ if ( (MHD_HTTP_OK == ugr->hr.http_status) &&
+ (NULL != gug->reference) )
+ {
+ const struct TALER_TESTING_Command *ref_cmd;
+
+ ref_cmd = TALER_TESTING_interpreter_lookup_command (gug->is,
+ gug->reference);
+ if (NULL == ref_cmd)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (gug->is);
+ return;
+ }
+ if (! unit_matches_reference (&ugr->details.ok.unit,
+ ref_cmd))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "GET /private/units/%s response does not match expectation from `%s'\n",
+ gug->unit_id,
+ gug->reference);
+ TALER_TESTING_interpreter_fail (gug->is);
+ return;
+ }
+ }
+ TALER_TESTING_interpreter_next (gug->is);
+}
+
+
+/**
+ * Issue GET request.
+ */
+static void
+get_unit_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct GetUnitState *gug = cls;
+
+ gug->is = is;
+ gug->ugh = TALER_MERCHANT_unit_get (
+ TALER_TESTING_interpreter_get_context (is),
+ gug->merchant_url,
+ gug->unit_id,
+ &get_unit_cb,
+ gug);
+ if (NULL == gug->ugh)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ }
+}
+
+
+/**
+ * Cleanup.
+ */
+static void
+get_unit_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct GetUnitState *gug = cls;
+
+ if (NULL != gug->ugh)
+ {
+ TALER_MERCHANT_unit_get_cancel (gug->ugh);
+ gug->ugh = NULL;
+ }
+ GNUNET_free (gug);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_get_unit (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ unsigned int http_status,
+ const char *reference)
+{
+ struct GetUnitState *gug;
+
+ gug = GNUNET_new (struct GetUnitState);
+ gug->merchant_url = merchant_url;
+ gug->unit_id = unit_id;
+ gug->http_status = http_status;
+ gug->reference = reference;
+
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = gug,
+ .label = label,
+ .run = &get_unit_run,
+ .cleanup = &get_unit_cleanup
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_get_unit.c */
diff --git a/src/testing/testing_api_cmd_get_units.c b/src/testing/testing_api_cmd_get_units.c
@@ -0,0 +1,361 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+*/
+/**
+ * @file testing_api_cmd_get_units.c
+ * @brief command to test GET /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <jansson.h>
+#include <taler/taler_testing_lib.h>
+#include "taler_merchant_service.h"
+#include "taler_merchant_testing_lib.h"
+
+
+/**
+ * State for a GET /private/units command.
+ */
+struct GetUnitsState
+{
+ /**
+ * In-flight request handle.
+ */
+ struct TALER_MERCHANT_UnitsGetHandle *ugh;
+
+ /**
+ * Interpreter context.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Merchant backend base URL.
+ */
+ const char *merchant_url;
+
+ /**
+ * Expected HTTP status.
+ */
+ unsigned int http_status;
+
+ /**
+ * Expected references that must appear in the listing.
+ */
+ const char **references;
+
+ /**
+ * Length of @e references.
+ */
+ unsigned int references_length;
+};
+
+
+/**
+ * Verify that @a entry matches the traits from @a ref_cmd.
+ */
+static enum GNUNET_GenericReturnValue
+check_unit_matches (const struct TALER_MERCHANT_UnitEntry *entry,
+ const struct TALER_TESTING_Command *ref_cmd)
+{
+ const char *unit_id = NULL;
+
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_unit_id (ref_cmd,
+ &unit_id))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Internal error: command `%s' lacks unit_id trait\n",
+ ref_cmd->label);
+ return GNUNET_SYSERR;
+ }
+ if (0 != strcmp (entry->unit,
+ unit_id))
+ return GNUNET_NO;
+
+ {
+ const char *unit_name_long = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_long (ref_cmd,
+ &unit_name_long))
+ {
+ if ( (NULL != unit_name_long) &&
+ (0 != strcmp (entry->unit_name_long,
+ unit_name_long)) )
+ return GNUNET_SYSERR;
+ }
+ }
+ {
+ const char *unit_name_short = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_short (ref_cmd,
+ &unit_name_short))
+ {
+ if ( (NULL != unit_name_short) &&
+ (0 != strcmp (entry->unit_name_short,
+ unit_name_short)) )
+ return GNUNET_SYSERR;
+ }
+ }
+ {
+ const bool *unit_allow_fraction = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_allow_fraction (ref_cmd,
+ &unit_allow_fraction))
+ {
+ if ( (NULL != unit_allow_fraction) &&
+ (*unit_allow_fraction != entry->unit_allow_fraction) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected allow_fraction %d got %d\n",
+ entry->unit,
+ (int) *unit_allow_fraction,
+ (int) entry->unit_allow_fraction);
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+ {
+ const uint32_t *unit_precision_level = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_precision_level (ref_cmd,
+ &unit_precision_level))
+ {
+ if ( (NULL != unit_precision_level) &&
+ (*unit_precision_level != entry->unit_precision_level) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected precision %u got %u\n",
+ entry->unit,
+ (unsigned int) *unit_precision_level,
+ (unsigned int) entry->unit_precision_level);
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+ {
+ const bool *unit_active = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_active (ref_cmd,
+ &unit_active))
+ {
+ if ( (NULL != unit_active) &&
+ (*unit_active != entry->unit_active) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: expected active %d got %d\n",
+ entry->unit,
+ (int) *unit_active,
+ (int) entry->unit_active);
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+ {
+ const json_t *unit_name_long_i18n = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_long_i18n (ref_cmd,
+ &unit_name_long_i18n))
+ {
+ if ( (NULL != unit_name_long_i18n) &&
+ ( (NULL == entry->unit_name_long_i18n) ||
+ (1 != json_equal (unit_name_long_i18n,
+ entry->unit_name_long_i18n)) ) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: long_i18n differs\n",
+ entry->unit);
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+ {
+ const json_t *unit_name_short_i18n = NULL;
+
+ if (GNUNET_OK ==
+ TALER_TESTING_get_trait_unit_name_short_i18n (ref_cmd,
+ &unit_name_short_i18n))
+ {
+ if ( (NULL != unit_name_short_i18n) &&
+ ( (NULL == entry->unit_name_short_i18n) ||
+ (1 != json_equal (unit_name_short_i18n,
+ entry->unit_name_short_i18n)) ) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit %s mismatch: short_i18n differs\n",
+ entry->unit);
+ return GNUNET_SYSERR;
+ }
+ }
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Completion callback for GET /private/units.
+ */
+static void
+get_units_cb (void *cls,
+ const struct TALER_MERCHANT_UnitsGetResponse *ugr)
+{
+ struct GetUnitsState *gus = cls;
+
+ gus->ugh = NULL;
+ if (gus->http_status != ugr->hr.http_status)
+ {
+ TALER_TESTING_unexpected_status_with_body (gus->is,
+ ugr->hr.http_status,
+ gus->http_status,
+ ugr->hr.reply);
+ return;
+ }
+ if (MHD_HTTP_OK == ugr->hr.http_status)
+ {
+ for (unsigned int i = 0; i < gus->references_length; ++i)
+ {
+ const char *label = gus->references[i];
+ const struct TALER_TESTING_Command *ref_cmd;
+ enum GNUNET_GenericReturnValue match = GNUNET_NO;
+
+ ref_cmd = TALER_TESTING_interpreter_lookup_command (gus->is,
+ label);
+ if (NULL == ref_cmd)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (gus->is);
+ return;
+ }
+ for (unsigned int j = 0;
+ j < ugr->details.ok.units_length;
+ ++j)
+ {
+ match = check_unit_matches (&ugr->details.ok.units[j],
+ ref_cmd);
+ if (GNUNET_SYSERR == match)
+ break;
+ if (GNUNET_OK == match)
+ break;
+ }
+ if (GNUNET_SYSERR == match)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (gus->is);
+ return;
+ }
+ if (GNUNET_OK != match)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unit referenced by `%s' not found in GET /private/units response\n",
+ label);
+ TALER_TESTING_interpreter_fail (gus->is);
+ return;
+ }
+ }
+ }
+ TALER_TESTING_interpreter_next (gus->is);
+}
+
+
+/**
+ * Issue the GET request.
+ */
+static void
+get_units_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct GetUnitsState *gus = cls;
+
+ gus->is = is;
+ gus->ugh = TALER_MERCHANT_units_get (
+ TALER_TESTING_interpreter_get_context (is),
+ gus->merchant_url,
+ &get_units_cb,
+ gus);
+ if (NULL == gus->ugh)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ }
+}
+
+
+/**
+ * Cleanup.
+ */
+static void
+get_units_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct GetUnitsState *gus = cls;
+
+ if (NULL != gus->ugh)
+ {
+ TALER_MERCHANT_units_get_cancel (gus->ugh);
+ gus->ugh = NULL;
+ }
+ GNUNET_array_grow (gus->references,
+ gus->references_length,
+ 0);
+ GNUNET_free (gus);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_get_units (const char *label,
+ const char *merchant_url,
+ unsigned int http_status,
+ ...)
+{
+ struct GetUnitsState *gus;
+ va_list ap;
+ const char *ref;
+
+ gus = GNUNET_new (struct GetUnitsState);
+ gus->merchant_url = merchant_url;
+ gus->http_status = http_status;
+
+ va_start (ap, http_status);
+ while (NULL != (ref = va_arg (ap, const char *)))
+ {
+ GNUNET_array_append (gus->references,
+ gus->references_length,
+ ref);
+ }
+ va_end (ap);
+
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = gus,
+ .label = label,
+ .run = &get_units_run,
+ .cleanup = &get_units_cleanup
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_get_units.c */
diff --git a/src/testing/testing_api_cmd_lock_product.c b/src/testing/testing_api_cmd_lock_product.c
@@ -70,6 +70,17 @@ struct LockProductState
uint32_t quantity;
/**
+ * Fractional component of the quantity (units of 1/MERCHANT_UNIT_FRAC_BASE) when
+ * @e use_fractional_quantity is true.
+ */
+ uint32_t quantity_frac;
+
+ /**
+ * Set to true if @e quantity_frac should be sent along with @e quantity.
+ */
+ bool use_fractional_quantity;
+
+ /**
* Expected HTTP response code.
*/
unsigned int http_status;
@@ -135,15 +146,32 @@ lock_product_run (void *cls,
struct LockProductState *pis = cls;
pis->is = is;
- pis->iph = TALER_MERCHANT_product_lock (
- TALER_TESTING_interpreter_get_context (is),
- pis->merchant_url,
- pis->product_id,
- pis->uuid,
- pis->duration,
- pis->quantity,
- &lock_product_cb,
- pis);
+ if (pis->use_fractional_quantity)
+ {
+ pis->iph = TALER_MERCHANT_product_lock2 (
+ TALER_TESTING_interpreter_get_context (is),
+ pis->merchant_url,
+ pis->product_id,
+ pis->uuid,
+ pis->duration,
+ pis->quantity,
+ pis->quantity_frac,
+ true,
+ &lock_product_cb,
+ pis);
+ }
+ else
+ {
+ pis->iph = TALER_MERCHANT_product_lock (
+ TALER_TESTING_interpreter_get_context (is),
+ pis->merchant_url,
+ pis->product_id,
+ pis->uuid,
+ pis->duration,
+ pis->quantity,
+ &lock_product_cb,
+ pis);
+ }
GNUNET_assert (NULL != pis->iph);
}
@@ -223,6 +251,8 @@ TALER_TESTING_cmd_merchant_lock_product (
sizeof (uuid));
pis->duration = duration;
pis->quantity = quantity;
+ pis->quantity_frac = 0;
+ pis->use_fractional_quantity = false;
{
struct TALER_TESTING_Command cmd = {
@@ -238,4 +268,31 @@ TALER_TESTING_cmd_merchant_lock_product (
}
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_lock_product2 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ struct GNUNET_TIME_Relative duration,
+ uint32_t quantity,
+ uint32_t quantity_frac,
+ bool use_fractional_quantity,
+ unsigned int http_status)
+{
+ struct LockProductState *pis;
+ struct TALER_TESTING_Command cmd;
+
+ cmd = TALER_TESTING_cmd_merchant_lock_product (label,
+ merchant_url,
+ product_id,
+ duration,
+ quantity,
+ http_status);
+ pis = cmd.cls;
+ pis->quantity_frac = quantity_frac;
+ pis->use_fractional_quantity = use_fractional_quantity;
+ return cmd;
+}
+
+
/* end of testing_api_cmd_lock_product.c */
diff --git a/src/testing/testing_api_cmd_patch_product.c b/src/testing/testing_api_cmd_patch_product.c
@@ -26,6 +26,7 @@
#include <taler/taler_testing_lib.h>
#include "taler_merchant_service.h"
#include "taler_merchant_testing_lib.h"
+#include "merchant_api_common.h"
/**
@@ -90,6 +91,31 @@ struct PatchProductState
int64_t total_stock;
/**
+ * Fractional stock component when fractional quantities are enabled.
+ */
+ uint32_t total_stock_frac;
+
+ /**
+ * Fractional precision level associated with fractional quantities.
+ */
+ uint32_t unit_precision_level;
+
+ /**
+ * whether fractional quantities are allowed for this product.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Cached string representation of the stock level.
+ */
+ char unit_total_stock[64];
+
+ /**
+ * set to true if we should use the extended fractional API.
+ */
+ bool use_fractional;
+
+ /**
* in @e units.
*/
int64_t total_lost;
@@ -111,6 +137,85 @@ struct PatchProductState
};
+static void
+patch_product_update_unit_total_stock (struct PatchProductState *pps)
+{
+ uint64_t stock;
+ uint32_t frac;
+
+ if (-1 == pps->total_stock)
+ {
+ stock = (uint64_t) INT64_MAX;
+ frac = (uint32_t) INT32_MAX;
+ }
+ else
+ {
+ stock = (uint64_t) pps->total_stock;
+ frac = pps->unit_allow_fraction ? pps->total_stock_frac : 0;
+ }
+ TALER_MERCHANT_format_stock_string (stock,
+ frac,
+ pps->unit_total_stock,
+ sizeof (pps->unit_total_stock));
+}
+
+
+static uint32_t
+default_precision_from_unit (const char *unit)
+{
+ struct PrecisionRule
+ {
+ const char *unit;
+ uint32_t precision;
+ };
+ static const struct PrecisionRule rules[] = {
+ { "WeightUnitMg", 0 },
+ { "SizeUnitMm", 0 },
+ { "WeightUnitG", 1 },
+ { "SizeUnitCm", 1 },
+ { "SurfaceUnitMm2", 1 },
+ { "VolumeUnitMm3", 1 },
+ { "WeightUnitOunce", 2 },
+ { "SizeUnitInch", 2 },
+ { "SurfaceUnitCm2", 2 },
+ { "VolumeUnitOunce", 2 },
+ { "VolumeUnitInch3", 2 },
+ { "TimeUnitHour", 2 },
+ { "TimeUnitMonth", 2 },
+ { "WeightUnitTon", 3 },
+ { "WeightUnitKg", 3 },
+ { "WeightUnitPound", 3 },
+ { "SizeUnitM", 3 },
+ { "SizeUnitDm", 3 },
+ { "SizeUnitFoot", 3 },
+ { "SurfaceUnitDm2", 3 },
+ { "SurfaceUnitFoot2", 3 },
+ { "VolumeUnitCm3", 3 },
+ { "VolumeUnitLitre", 3 },
+ { "VolumeUnitGallon", 3 },
+ { "TimeUnitSecond", 3 },
+ { "TimeUnitMinute", 3 },
+ { "TimeUnitDay", 3 },
+ { "TimeUnitWeek", 3 },
+ { "SurfaceUnitM2", 4 },
+ { "SurfaceUnitInch2", 4 },
+ { "TimeUnitYear", 4 },
+ { "VolumeUnitDm3", 5 },
+ { "VolumeUnitFoot3", 5 },
+ { "VolumeUnitM3", 6 }
+ };
+
+ const size_t rules_len = sizeof (rules) / sizeof (rules[0]);
+ if (NULL == unit)
+ return 0;
+
+ for (size_t i = 0; i<rules_len; i++)
+ if (0 == strcmp (unit,
+ rules[i].unit))
+ return rules[i].precision;
+ return 0;
+}
+
/**
* Callback for a PATCH /products/$ID operation.
@@ -172,22 +277,50 @@ patch_product_run (void *cls,
struct PatchProductState *pis = cls;
pis->is = is;
- pis->iph = TALER_MERCHANT_product_patch (
- TALER_TESTING_interpreter_get_context (is),
- pis->merchant_url,
- pis->product_id,
- pis->description,
- pis->description_i18n,
- pis->unit,
- &pis->price,
- pis->image,
- pis->taxes,
- pis->total_stock,
- pis->total_lost,
- pis->address,
- pis->next_restock,
- &patch_product_cb,
- pis);
+ if (pis->use_fractional)
+ {
+ pis->iph = TALER_MERCHANT_product_patch2 (
+ TALER_TESTING_interpreter_get_context (is),
+ pis->merchant_url,
+ pis->product_id,
+ pis->description,
+ pis->description_i18n,
+ pis->unit,
+ &pis->price,
+ 1,
+ pis->image,
+ pis->taxes,
+ pis->total_stock,
+ pis->total_stock_frac,
+ pis->unit_allow_fraction,
+ pis->use_fractional
+ ? &pis->unit_precision_level
+ : NULL,
+ pis->total_lost,
+ pis->address,
+ pis->next_restock,
+ &patch_product_cb,
+ pis);
+ }
+ else
+ {
+ pis->iph = TALER_MERCHANT_product_patch (
+ TALER_TESTING_interpreter_get_context (is),
+ pis->merchant_url,
+ pis->product_id,
+ pis->description,
+ pis->description_i18n,
+ pis->unit,
+ &pis->price,
+ pis->image,
+ pis->taxes,
+ pis->total_stock,
+ pis->total_lost,
+ pis->address,
+ pis->next_restock,
+ &patch_product_cb,
+ pis);
+ }
GNUNET_assert (NULL != pis->iph);
}
@@ -217,6 +350,12 @@ patch_product_traits (void *cls,
TALER_TESTING_make_trait_product_image (pps->image),
TALER_TESTING_make_trait_taxes (pps->taxes),
TALER_TESTING_make_trait_product_stock (&pps->total_stock),
+ TALER_TESTING_make_trait_product_unit_total_stock (
+ pps->unit_total_stock),
+ TALER_TESTING_make_trait_product_unit_precision_level (
+ &pps->unit_precision_level),
+ TALER_TESTING_make_trait_product_unit_allow_fraction (
+ &pps->unit_allow_fraction),
TALER_TESTING_make_trait_address (pps->address),
TALER_TESTING_make_trait_timestamp (0,
&pps->next_restock),
@@ -286,15 +425,20 @@ TALER_TESTING_cmd_merchant_patch_product (
pis->description = description;
pis->description_i18n = description_i18n; /* ownership taken */
pis->unit = unit;
+ pis->unit_precision_level = default_precision_from_unit (unit);
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (price,
&pis->price));
pis->image = GNUNET_strdup (image);
pis->taxes = taxes; /* ownership taken */
pis->total_stock = total_stock;
+ pis->total_stock_frac = 0;
+ pis->unit_allow_fraction = false;
+ pis->use_fractional = false;
pis->total_lost = total_lost;
pis->address = address; /* ownership taken */
pis->next_restock = next_restock;
+ patch_product_update_unit_total_stock (pis);
{
struct TALER_TESTING_Command cmd = {
.cls = pis,
@@ -309,4 +453,51 @@ TALER_TESTING_cmd_merchant_patch_product (
}
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_patch_product2 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ const char *description,
+ json_t *description_i18n,
+ const char *unit,
+ const char *price,
+ const char *image,
+ json_t *taxes,
+ int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ uint64_t total_lost,
+ json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ unsigned int http_status)
+{
+ struct TALER_TESTING_Command cmd;
+
+ cmd = TALER_TESTING_cmd_merchant_patch_product (label,
+ merchant_url,
+ product_id,
+ description,
+ description_i18n,
+ unit,
+ price,
+ image,
+ taxes,
+ total_stock,
+ total_lost,
+ address,
+ next_restock,
+ http_status);
+ {
+ struct PatchProductState *pps = cmd.cls;
+
+ pps->total_stock_frac = total_stock_frac;
+ pps->unit_allow_fraction = unit_allow_fraction;
+ pps->use_fractional = true;
+ patch_product_update_unit_total_stock (pps);
+ }
+ return cmd;
+}
+
+
/* end of testing_api_cmd_patch_product.c */
diff --git a/src/testing/testing_api_cmd_patch_unit.c b/src/testing/testing_api_cmd_patch_unit.c
@@ -0,0 +1,278 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+*/
+/**
+ * @file testing_api_cmd_patch_unit.c
+ * @brief command to test PATCH /private/units/$ID
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_testing_lib.h>
+#include "taler_merchant_service.h"
+#include "taler_merchant_testing_lib.h"
+
+
+/**
+ * State for a PATCH /private/units command.
+ */
+struct PatchUnitState
+{
+ /**
+ * In-flight request handle.
+ */
+ struct TALER_MERCHANT_UnitPatchHandle *uph;
+
+ /**
+ * Interpreter context.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Merchant backend base URL.
+ */
+ const char *merchant_url;
+
+ /**
+ * Unit identifier.
+ */
+ const char *unit_id;
+
+ /**
+ * Optional new long label.
+ */
+ const char *unit_name_long;
+
+ /**
+ * Optional new short label.
+ */
+ const char *unit_name_short;
+
+ /**
+ * Optional long label translations.
+ */
+ json_t *unit_name_long_i18n;
+
+ /**
+ * Optional short label translations.
+ */
+ json_t *unit_name_short_i18n;
+
+ /**
+ * Whether a new fractional flag was provided.
+ */
+ bool have_unit_allow_fraction;
+
+ /**
+ * New fractional flag value.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Whether a new precision level was provided.
+ */
+ bool have_unit_precision_level;
+
+ /**
+ * New precision level.
+ */
+ uint32_t unit_precision_level;
+
+ /**
+ * Whether a new active flag was provided.
+ */
+ bool have_unit_active;
+
+ /**
+ * New active flag value.
+ */
+ bool unit_active;
+
+ /**
+ * Expected HTTP status.
+ */
+ unsigned int http_status;
+};
+
+
+/**
+ * Completion callback for PATCH /private/units.
+ */
+static void
+patch_unit_cb (void *cls,
+ const struct TALER_MERCHANT_HttpResponse *hr)
+{
+ struct PatchUnitState *pus = cls;
+
+ pus->uph = NULL;
+ if (pus->http_status != hr->http_status)
+ {
+ TALER_TESTING_unexpected_status_with_body (pus->is,
+ hr->http_status,
+ pus->http_status,
+ hr->reply);
+ return;
+ }
+ TALER_TESTING_interpreter_next (pus->is);
+}
+
+
+/**
+ * Issue the PATCH request.
+ */
+static void
+patch_unit_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct PatchUnitState *pus = cls;
+ const bool *allow_ptr = pus->have_unit_allow_fraction
+ ? &pus->unit_allow_fraction
+ : NULL;
+ const uint32_t *precision_ptr = pus->have_unit_precision_level
+ ? &pus->unit_precision_level
+ : NULL;
+ const bool *active_ptr = pus->have_unit_active
+ ? &pus->unit_active
+ : NULL;
+
+ pus->is = is;
+ pus->uph = TALER_MERCHANT_unit_patch (
+ TALER_TESTING_interpreter_get_context (is),
+ pus->merchant_url,
+ pus->unit_id,
+ pus->unit_name_long,
+ pus->unit_name_short,
+ pus->unit_name_long_i18n,
+ pus->unit_name_short_i18n,
+ allow_ptr,
+ precision_ptr,
+ active_ptr,
+ &patch_unit_cb,
+ pus);
+ if (NULL == pus->uph)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ }
+}
+
+
+/**
+ * Provide traits to other commands.
+ */
+static enum GNUNET_GenericReturnValue
+patch_unit_traits (void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct PatchUnitState *pus = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_unit_id (pus->unit_id),
+ TALER_TESTING_make_trait_unit_name_long (pus->unit_name_long),
+ TALER_TESTING_make_trait_unit_name_short (pus->unit_name_short),
+ TALER_TESTING_make_trait_unit_name_long_i18n (pus->unit_name_long_i18n),
+ TALER_TESTING_make_trait_unit_name_short_i18n (pus->unit_name_short_i18n),
+ TALER_TESTING_make_trait_unit_allow_fraction (
+ pus->have_unit_allow_fraction
+ ? &pus->unit_allow_fraction
+ : NULL),
+ TALER_TESTING_make_trait_unit_precision_level (
+ pus->have_unit_precision_level
+ ? &pus->unit_precision_level
+ : NULL),
+ TALER_TESTING_make_trait_unit_active (
+ pus->have_unit_active
+ ? &pus->unit_active
+ : NULL),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+}
+
+
+/**
+ * Cleanup.
+ */
+static void
+patch_unit_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct PatchUnitState *pus = cls;
+
+ if (NULL != pus->uph)
+ {
+ TALER_MERCHANT_unit_patch_cancel (pus->uph);
+ pus->uph = NULL;
+ }
+ if (NULL != pus->unit_name_long_i18n)
+ json_decref (pus->unit_name_long_i18n);
+ if (NULL != pus->unit_name_short_i18n)
+ json_decref (pus->unit_name_short_i18n);
+ GNUNET_free (pus);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_patch_unit (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ json_t *unit_name_long_i18n,
+ json_t *unit_name_short_i18n,
+ const bool unit_allow_fraction,
+ const uint32_t unit_precision_level,
+ const bool unit_active,
+ unsigned int http_status)
+{
+ struct PatchUnitState *pus;
+
+ pus = GNUNET_new (struct PatchUnitState);
+ pus->merchant_url = merchant_url;
+ pus->unit_id = unit_id;
+ pus->unit_name_long = unit_name_long;
+ pus->unit_name_short = unit_name_short;
+ pus->unit_name_long_i18n = unit_name_long_i18n;
+ pus->unit_name_short_i18n = unit_name_short_i18n;
+ pus->unit_allow_fraction = unit_allow_fraction;
+ pus->have_unit_allow_fraction = true;
+ pus->unit_precision_level = unit_precision_level;
+ pus->have_unit_precision_level = true;
+ pus->unit_active = unit_active;
+ pus->have_unit_active = true;
+ pus->http_status = http_status;
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = pus,
+ .label = label,
+ .run = &patch_unit_run,
+ .cleanup = &patch_unit_cleanup,
+ .traits = &patch_unit_traits
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_patch_unit.c */
diff --git a/src/testing/testing_api_cmd_post_orders.c b/src/testing/testing_api_cmd_post_orders.c
@@ -28,6 +28,9 @@
#include <gnunet/gnunet_time_lib.h>
#include <jansson.h>
#include <stdint.h>
+#include "taler_merchant_util.h"
+#include <stdlib.h>
+#include <math.h>
#include <taler/taler_exchange_service.h>
#include <taler/taler_testing_lib.h>
#include "taler_merchant_service.h"
@@ -490,6 +493,7 @@ orders_run2 (void *cls,
{
char *ctok;
struct TALER_MERCHANT_InventoryProduct pd;
+ double quantity_double = 0.0;
/* Token syntax is "[product_id]/[quantity]" */
ctok = strchr (token, '/');
@@ -497,17 +501,59 @@ orders_run2 (void *cls,
{
*ctok = '\0';
ctok++;
- if (1 != sscanf (ctok,
- "%u",
- &pd.quantity))
{
- GNUNET_break (0);
- break;
+ char *endptr;
+
+ quantity_double = strtod (ctok,
+ &endptr);
+ if ( (endptr == ctok) || ('\0' != *endptr) ||
+ (! isfinite (quantity_double)) || (quantity_double < 0.0))
+ {
+ GNUNET_break (0);
+ break;
+ }
}
}
else
{
- pd.quantity = 1;
+ quantity_double = 1.0;
+ }
+ if (quantity_double <= 0.0)
+ {
+ GNUNET_break (0);
+ break;
+ }
+
+ {
+ double quantity_floor;
+ double frac;
+ uint64_t quantity_int;
+ uint32_t quantity_frac_local = 0;
+ long long scaled;
+
+ quantity_floor = floor (quantity_double);
+ frac = quantity_double - quantity_floor;
+ quantity_int = (uint64_t) quantity_floor;
+ if (frac < 0.0)
+ {
+ GNUNET_break (0);
+ break;
+ }
+ scaled = llround (frac * (double) MERCHANT_UNIT_FRAC_BASE);
+ if (scaled < 0)
+ {
+ GNUNET_break (0);
+ break;
+ }
+ if (scaled >= (long long) MERCHANT_UNIT_FRAC_BASE)
+ {
+ quantity_int++;
+ scaled -= MERCHANT_UNIT_FRAC_BASE;
+ }
+ quantity_frac_local = (uint32_t) scaled;
+ pd.quantity = quantity_int;
+ pd.quantity_frac = quantity_frac_local;
+ pd.use_fractional_quantity = (0 != quantity_frac_local);
}
pd.product_id = token;
diff --git a/src/testing/testing_api_cmd_post_products.c b/src/testing/testing_api_cmd_post_products.c
@@ -26,6 +26,7 @@
#include <taler/taler_testing_lib.h>
#include "taler_merchant_service.h"
#include "taler_merchant_testing_lib.h"
+#include "merchant_api_common.h"
/**
@@ -90,6 +91,31 @@ struct PostProductsState
int64_t total_stock;
/**
+ * Fractional stock component when fractional quantities are enabled.
+ */
+ uint32_t total_stock_frac;
+
+ /**
+ * Fractional precision level associated with fractional quantities.
+ */
+ uint32_t unit_precision_level;
+
+ /**
+ * whether fractional quantities are allowed for this product.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Cached string representation of the stock level.
+ */
+ char unit_total_stock[64];
+
+ /**
+ * set to true if we should use the extended fractional API.
+ */
+ bool use_fractional;
+
+ /**
* where the product is in stock
*/
json_t *address;
@@ -111,6 +137,85 @@ struct PostProductsState
};
+static void
+post_products_update_unit_total_stock (struct PostProductsState *pps)
+{
+ uint64_t stock;
+ uint32_t frac;
+
+ if (-1 == pps->total_stock)
+ {
+ stock = (uint64_t) INT64_MAX;
+ frac = (uint32_t) INT32_MAX;
+ }
+ else
+ {
+ stock = (uint64_t) pps->total_stock;
+ frac = pps->unit_allow_fraction ? pps->total_stock_frac : 0;
+ }
+ TALER_MERCHANT_format_stock_string (stock,
+ frac,
+ pps->unit_total_stock,
+ sizeof (pps->unit_total_stock));
+}
+
+
+static uint32_t
+default_precision_from_unit (const char *unit)
+{
+ struct PrecisionRule
+ {
+ const char *unit;
+ uint32_t precision;
+ };
+ static const struct PrecisionRule rules[] = {
+ { "WeightUnitMg", 0 },
+ { "SizeUnitMm", 0 },
+ { "WeightUnitG", 1 },
+ { "SizeUnitCm", 1 },
+ { "SurfaceUnitMm2", 1 },
+ { "VolumeUnitMm3", 1 },
+ { "WeightUnitOunce", 2 },
+ { "SizeUnitInch", 2 },
+ { "SurfaceUnitCm2", 2 },
+ { "VolumeUnitOunce", 2 },
+ { "VolumeUnitInch3", 2 },
+ { "TimeUnitHour", 2 },
+ { "TimeUnitMonth", 2 },
+ { "WeightUnitTon", 3 },
+ { "WeightUnitKg", 3 },
+ { "WeightUnitPound", 3 },
+ { "SizeUnitM", 3 },
+ { "SizeUnitDm", 3 },
+ { "SizeUnitFoot", 3 },
+ { "SurfaceUnitDm2", 3 },
+ { "SurfaceUnitFoot2", 3 },
+ { "VolumeUnitCm3", 3 },
+ { "VolumeUnitLitre", 3 },
+ { "VolumeUnitGallon", 3 },
+ { "TimeUnitSecond", 3 },
+ { "TimeUnitMinute", 3 },
+ { "TimeUnitDay", 3 },
+ { "TimeUnitWeek", 3 },
+ { "SurfaceUnitM2", 4 },
+ { "SurfaceUnitInch2", 4 },
+ { "TimeUnitYear", 4 },
+ { "VolumeUnitDm3", 5 },
+ { "VolumeUnitFoot3", 5 },
+ { "VolumeUnitM3", 6 }
+ };
+
+ const size_t rules_len = sizeof (rules) / sizeof (rules[0]);
+ if (NULL == unit)
+ return 0;
+
+ for (size_t i = 0; i<rules_len; i++)
+ if (0 == strcmp (unit,
+ rules[i].unit))
+ return rules[i].precision;
+ return 0;
+}
+
/**
* Callback for a POST /products operation.
@@ -173,22 +278,52 @@ post_products_run (void *cls,
struct PostProductsState *pis = cls;
pis->is = is;
- pis->iph = TALER_MERCHANT_products_post2 (
- TALER_TESTING_interpreter_get_context (is),
- pis->merchant_url,
- pis->product_id,
- pis->description,
- pis->description_i18n,
- pis->unit,
- &pis->price,
- pis->image,
- pis->taxes,
- pis->total_stock,
- pis->address,
- pis->next_restock,
- pis->minimum_age,
- &post_products_cb,
- pis);
+ if (pis->use_fractional)
+ {
+ pis->iph = TALER_MERCHANT_products_post4 (
+ TALER_TESTING_interpreter_get_context (is),
+ pis->merchant_url,
+ pis->product_id,
+ pis->description,
+ pis->description_i18n,
+ pis->unit,
+ &pis->price,
+ 1,
+ pis->image,
+ pis->taxes,
+ pis->total_stock,
+ pis->total_stock_frac,
+ pis->unit_allow_fraction,
+ pis->use_fractional
+ ? &pis->unit_precision_level
+ : NULL,
+ pis->address,
+ pis->next_restock,
+ pis->minimum_age,
+ 0,
+ NULL,
+ &post_products_cb,
+ pis);
+ }
+ else
+ {
+ pis->iph = TALER_MERCHANT_products_post2 (
+ TALER_TESTING_interpreter_get_context (is),
+ pis->merchant_url,
+ pis->product_id,
+ pis->description,
+ pis->description_i18n,
+ pis->unit,
+ &pis->price,
+ pis->image,
+ pis->taxes,
+ pis->total_stock,
+ pis->address,
+ pis->next_restock,
+ pis->minimum_age,
+ &post_products_cb,
+ pis);
+ }
GNUNET_assert (NULL != pis->iph);
}
@@ -218,6 +353,12 @@ post_products_traits (void *cls,
TALER_TESTING_make_trait_product_image (pps->image),
TALER_TESTING_make_trait_taxes (pps->taxes),
TALER_TESTING_make_trait_product_stock (&pps->total_stock),
+ TALER_TESTING_make_trait_product_unit_total_stock (
+ pps->unit_total_stock),
+ TALER_TESTING_make_trait_product_unit_precision_level (
+ &pps->unit_precision_level),
+ TALER_TESTING_make_trait_product_unit_allow_fraction (
+ &pps->unit_allow_fraction),
TALER_TESTING_make_trait_address (pps->address),
TALER_TESTING_make_trait_timestamp (0,
&pps->next_restock),
@@ -289,15 +430,20 @@ TALER_TESTING_cmd_merchant_post_products2 (
pis->description = description;
pis->description_i18n = description_i18n; /* ownership taken */
pis->unit = unit;
+ pis->unit_precision_level = default_precision_from_unit (unit);
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (price,
&pis->price));
pis->image = GNUNET_strdup (image);
pis->taxes = taxes; /* ownership taken */
pis->total_stock = total_stock;
+ pis->total_stock_frac = 0;
+ pis->unit_allow_fraction = false;
+ pis->use_fractional = false;
pis->minimum_age = minimum_age;
pis->address = address; /* ownership taken */
pis->next_restock = next_restock;
+ post_products_update_unit_total_stock (pis);
{
struct TALER_TESTING_Command cmd = {
.cls = pis,
@@ -313,6 +459,53 @@ TALER_TESTING_cmd_merchant_post_products2 (
struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_post_products3 (
+ const char *label,
+ const char *merchant_url,
+ const char *product_id,
+ const char *description,
+ json_t *description_i18n,
+ const char *unit,
+ const char *price,
+ const char *image,
+ json_t *taxes,
+ int64_t total_stock,
+ uint32_t total_stock_frac,
+ bool unit_allow_fraction,
+ uint32_t minimum_age,
+ json_t *address,
+ struct GNUNET_TIME_Timestamp next_restock,
+ unsigned int http_status)
+{
+ struct TALER_TESTING_Command cmd;
+
+ cmd = TALER_TESTING_cmd_merchant_post_products2 (label,
+ merchant_url,
+ product_id,
+ description,
+ description_i18n,
+ unit,
+ price,
+ image,
+ taxes,
+ total_stock,
+ minimum_age,
+ address,
+ next_restock,
+ http_status);
+ {
+ struct PostProductsState *pis = cmd.cls;
+
+ pis->total_stock_frac = total_stock_frac;
+ pis->unit_allow_fraction = unit_allow_fraction;
+ pis->use_fractional = true;
+ post_products_update_unit_total_stock (pis);
+ }
+ return cmd;
+}
+
+
+struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_post_products (
const char *label,
const char *merchant_url,
diff --git a/src/testing/testing_api_cmd_post_units.c b/src/testing/testing_api_cmd_post_units.c
@@ -0,0 +1,242 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 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/>
+*/
+/**
+ * @file testing_api_cmd_post_units.c
+ * @brief command to test POST /private/units
+ * @author Bohdan Potuzhnyi
+ */
+#include "platform.h"
+#include <taler/taler_testing_lib.h>
+#include "taler_merchant_service.h"
+#include "taler_merchant_testing_lib.h"
+
+
+/**
+ * State for a POST /private/units command.
+ */
+struct PostUnitState
+{
+ /**
+ * In-flight request handle.
+ */
+ struct TALER_MERCHANT_UnitsPostHandle *uph;
+
+ /**
+ * Interpreter context.
+ */
+ struct TALER_TESTING_Interpreter *is;
+
+ /**
+ * Merchant backend base URL.
+ */
+ const char *merchant_url;
+
+ /**
+ * Unit identifier.
+ */
+ const char *unit_id;
+
+ /**
+ * Long label.
+ */
+ const char *unit_name_long;
+
+ /**
+ * Short label.
+ */
+ const char *unit_name_short;
+
+ /**
+ * Optional translations (reference counted).
+ */
+ json_t *unit_name_long_i18n;
+
+ /**
+ * Optional translations (reference counted).
+ */
+ json_t *unit_name_short_i18n;
+
+ /**
+ * Whether fractional quantities are allowed.
+ */
+ bool unit_allow_fraction;
+
+ /**
+ * Precision level for fractional quantities.
+ */
+ uint32_t unit_precision_level;
+
+ /**
+ * Whether the unit should be active.
+ */
+ bool unit_active;
+
+ /**
+ * Expected HTTP status.
+ */
+ unsigned int http_status;
+};
+
+
+/**
+ * Completion callback for POST /private/units.
+ */
+static void
+post_unit_cb (void *cls,
+ const struct TALER_MERCHANT_HttpResponse *hr)
+{
+ struct PostUnitState *pus = cls;
+
+ pus->uph = NULL;
+ if (pus->http_status != hr->http_status)
+ {
+ TALER_TESTING_unexpected_status_with_body (pus->is,
+ hr->http_status,
+ pus->http_status,
+ hr->reply);
+ return;
+ }
+ TALER_TESTING_interpreter_next (pus->is);
+}
+
+
+/**
+ * Issue the POST /private/units request.
+ */
+static void
+post_unit_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct PostUnitState *pus = cls;
+
+ pus->is = is;
+ pus->uph = TALER_MERCHANT_units_post (
+ TALER_TESTING_interpreter_get_context (is),
+ pus->merchant_url,
+ pus->unit_id,
+ pus->unit_name_long,
+ pus->unit_name_short,
+ pus->unit_allow_fraction,
+ pus->unit_precision_level,
+ pus->unit_active,
+ pus->unit_name_long_i18n,
+ pus->unit_name_short_i18n,
+ &post_unit_cb,
+ pus);
+ if (NULL == pus->uph)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ }
+}
+
+
+/**
+ * Provide traits for other commands.
+ */
+static enum GNUNET_GenericReturnValue
+post_unit_traits (void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct PostUnitState *pus = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_unit_id (pus->unit_id),
+ TALER_TESTING_make_trait_unit_name_long (pus->unit_name_long),
+ TALER_TESTING_make_trait_unit_name_short (pus->unit_name_short),
+ TALER_TESTING_make_trait_unit_allow_fraction (&pus->unit_allow_fraction),
+ TALER_TESTING_make_trait_unit_precision_level (&pus->unit_precision_level),
+ TALER_TESTING_make_trait_unit_active (&pus->unit_active),
+ TALER_TESTING_make_trait_unit_name_long_i18n (pus->unit_name_long_i18n),
+ TALER_TESTING_make_trait_unit_name_short_i18n (pus->unit_name_short_i18n),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+}
+
+
+/**
+ * Cleanup / cancel pending request.
+ */
+static void
+post_unit_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct PostUnitState *pus = cls;
+
+ if (NULL != pus->uph)
+ {
+ TALER_MERCHANT_units_post_cancel (pus->uph);
+ pus->uph = NULL;
+ }
+ if (NULL != pus->unit_name_long_i18n)
+ json_decref (pus->unit_name_long_i18n);
+ if (NULL != pus->unit_name_short_i18n)
+ json_decref (pus->unit_name_short_i18n);
+ GNUNET_free (pus);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_post_units (const char *label,
+ const char *merchant_url,
+ const char *unit_id,
+ const char *unit_name_long,
+ const char *unit_name_short,
+ bool unit_allow_fraction,
+ uint32_t unit_precision_level,
+ bool unit_active,
+ json_t *unit_name_long_i18n,
+ json_t *unit_name_short_i18n,
+ unsigned int http_status)
+{
+ struct PostUnitState *pus;
+
+ pus = GNUNET_new (struct PostUnitState);
+ pus->merchant_url = merchant_url;
+ pus->unit_id = unit_id;
+ pus->unit_name_long = unit_name_long;
+ pus->unit_name_short = unit_name_short;
+ pus->unit_allow_fraction = unit_allow_fraction;
+ pus->unit_precision_level = unit_precision_level;
+ pus->unit_active = unit_active;
+ pus->unit_name_long_i18n = unit_name_long_i18n;
+ pus->unit_name_short_i18n = unit_name_short_i18n;
+ pus->http_status = http_status;
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = pus,
+ .label = label,
+ .run = &post_unit_run,
+ .cleanup = &post_unit_cleanup,
+ .traits = &post_unit_traits
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_post_units.c */