commit cd7ee096333262ace0d8117b4846c171752f4dfa parent 31d407c0843a719bfc25ea6964bab9f466f17808 Author: Christian Grothoff <christian@grothoff.org> Date: Wed, 31 Dec 2025 21:17:41 +0100 fix price array handling in products Diffstat:
24 files changed, 616 insertions(+), 238 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -452,7 +452,7 @@ TMH_products_array_valid (const json_t *products) valid = false; } if ( (NULL != taxes) && - (! TMH_taxes_array_valid (taxes)) ) + (! TALER_MERCHANT_taxes_array_valid (taxes)) ) { GNUNET_break_op (0); valid = false; @@ -511,40 +511,6 @@ TMH_template_contract_valid (const json_t *template_contract) } -bool -TMH_taxes_array_valid (const json_t *taxes) -{ - json_t *tax; - size_t idx; - - if (! json_is_array (taxes)) - return false; - json_array_foreach (taxes, idx, tax) - { - struct TALER_Amount amount; - const char *name; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("name", - &name), - TALER_JSON_spec_amount_any ("tax", - &amount), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (NULL, - tax, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - return false; - } - } - return true; -} - - struct TMH_WireMethod * TMH_setup_wire_account ( struct TALER_FullPayto payto_uri, diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h @@ -39,18 +39,6 @@ TMH_accounts_array_valid (const json_t *accounts); /** - * Check if @a taxes is an array of valid Taxes in the sense of - * Taler's API definition. - * - * @param taxes array to check - * @return true if @a taxes is an array and all - * entries are valid Taxes. - */ -bool -TMH_taxes_array_valid (const json_t *taxes); - - -/** * Check if @a location is a valid Location object in the sense of Taler's API * definition. * diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c @@ -139,8 +139,12 @@ add_product (void *cls, (json_t *) pd->description_i18n), GNUNET_JSON_pack_string ("unit", pd->unit), - TALER_JSON_pack_amount ("price", - &pd->price), + // Note: deprecated field + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("price", + (0 == pd->price_array_length) + ? NULL + : &pd->price_array[0])), TALER_JSON_pack_amount_array ("unit_price", pd->price_array_length, pd->price_array), diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c @@ -107,8 +107,15 @@ TMH_private_get_products_ID ( pd.unit), GNUNET_JSON_pack_array_steal ("categories", jcategories), - TALER_JSON_pack_amount ("price", - &pd.price), + // Note: deprecated field + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("price", + (0 == pd.price_array_length) + ? NULL + : &pd.price_array[0])), + 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)), diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -50,6 +50,7 @@ TMH_private_patch_products_ID ( const char *unit_total_stock = NULL; bool unit_total_stock_missing; bool total_stock_missing; + struct TALER_Amount price; bool price_missing; bool unit_price_missing; bool unit_allow_fraction; @@ -71,11 +72,17 @@ TMH_private_patch_products_ID ( NULL), GNUNET_JSON_spec_string ("unit", (const char **) &pd.unit), + // FIXME: deprecated API GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount_any ("price", - &pd.price), + &price), &price_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_string ("image", (const char **) &pd.image), NULL), @@ -104,11 +111,6 @@ TMH_private_patch_products_ID ( &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), NULL), @@ -160,7 +162,7 @@ TMH_private_patch_products_ID ( { if (! price_missing) { - if (0 != TALER_amount_cmp (&pd.price, + if (0 != TALER_amount_cmp (&price, &pd.price_array[0])) { ret = TALER_MHD_reply_with_error (connection, @@ -170,11 +172,6 @@ TMH_private_patch_products_ID ( goto cleanup; } } - else - { - pd.price = pd.price_array[0]; - price_missing = false; - } } else { @@ -188,7 +185,7 @@ TMH_private_patch_products_ID ( } pd.price_array = GNUNET_new_array (1, struct TALER_Amount); - pd.price_array[0] = pd.price; + pd.price_array[0] = price; pd.price_array_length = 1; } if (! unit_precision_missing) @@ -301,7 +298,7 @@ TMH_private_patch_products_ID ( if (NULL == pd.taxes) pd.taxes = json_array (); /* check taxes is well-formed */ - if (! TMH_taxes_array_valid (pd.taxes)) + if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) { GNUNET_break_op (0); ret = TALER_MHD_reply_with_error (connection, diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -3500,9 +3500,44 @@ phase_merge_inventory (struct OrderContext *oc) } } { + struct TALER_MERCHANT_ProductSold ps = { + .product_id = (char *) ip->product_id, + .product_name = pd.product_name, + .description = pd.description, + .description_i18n = pd.description_i18n, + .unit_quantity.integer = ip->quantity, + .unit_quantity.fractional = ip->quantity_frac, + .prices_length = pd.price_array_length, + .prices = GNUNET_new_array (pd.price_array_length, + struct TALER_Amount), + .prices_are_net = pd.price_is_net, + .image = pd.image, + .taxes = pd.taxes, + .delivery_date = oc->parse_order.delivery_date, + .product_money_pot = pd.money_pot_id, + .unit = pd.unit, + + }; json_t *p; char unit_quantity_buf[64]; + for (size_t j = 0; j<pd.price_array_length; j++) + { + struct TALER_Amount atomic_amount; + + TALER_amount_set_zero (pd.price_array[j].currency, + &atomic_amount); + atomic_amount.fraction = 1; + GNUNET_assert ( + GNUNET_OK == + TALER_MERCHANT_amount_multiply_by_quantity ( + &ps.prices[j], + &pd.price_array[j], + &ps.unit_quantity, + TALER_MERCHANT_ROUND_UP, + &atomic_amount)); + } + TALER_MERCHANT_vk_format_fractional_string ( TALER_MERCHANT_VK_QUANTITY, ip->quantity, @@ -3511,31 +3546,9 @@ phase_merge_inventory (struct OrderContext *oc) unit_quantity_buf); if (0 != pd.money_pot_id) pots[pots_off++] = pd.money_pot_id; - // FIXME: reuse TALER_MERCHANT_product_sold_serialize() here!? - p = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("product_name", - pd.product_name), - GNUNET_JSON_pack_string ("description", - pd.description), - GNUNET_JSON_pack_object_incref ("description_i18n", - pd.description_i18n), - GNUNET_JSON_pack_string ("unit", - pd.unit), - TALER_JSON_pack_amount ("price", - &pd.price), - GNUNET_JSON_pack_array_incref ("taxes", - pd.taxes), - GNUNET_JSON_pack_string ("image", - pd.image), - GNUNET_JSON_pack_uint64 ( - "quantity", - ip->quantity), - GNUNET_JSON_pack_uint64 ( - "product_money_pot", - pd.money_pot_id), - GNUNET_JSON_pack_string ("unit_quantity", - unit_quantity_buf)); + p = TALER_MERCHANT_product_sold_serialize (&ps); GNUNET_assert (NULL != p); + GNUNET_free (ps.prices); GNUNET_assert (0 == json_array_append_new (oc->merge_inventory.products, p)); @@ -4403,20 +4416,6 @@ phase_parse_order (struct OrderContext *oc) oc->parse_order.merchant_base_url = url; } - // FIXME: integrate remaining validation logic with - // TALER_MERCHANT_parse_product() and then remove this! - if ( (NULL != products) && - (! TMH_products_array_valid (products)) ) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order.products"); - return; - } - /* Merchant information must not already be present */ if (NULL != jmerchant) { diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c @@ -45,6 +45,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, bool unit_allow_fraction_missing; uint32_t unit_precision_level; bool unit_precision_missing; + struct TALER_Amount price; bool price_missing; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("product_id", @@ -64,7 +65,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, (const char **) &pd.unit), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount_any ("price", - &pd.price), + &price), &price_missing), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("image", @@ -158,7 +159,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, { if (! price_missing) { - if (0 != TALER_amount_cmp (&pd.price, + if (0 != TALER_amount_cmp (&price, &pd.price_array[0])) { ret = TALER_MHD_reply_with_error (connection, @@ -168,11 +169,6 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, goto cleanup; } } - else - { - pd.price = pd.price_array[0]; - price_missing = false; - } } else { @@ -186,7 +182,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, } pd.price_array = GNUNET_new_array (1, struct TALER_Amount); - pd.price_array[0] = pd.price; + pd.price_array[0] = price; pd.price_array_length = 1; } } @@ -279,7 +275,7 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, pd.taxes = json_array (); /* check taxes is well-formed */ - if (! TMH_taxes_array_valid (pd.taxes)) + if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) { GNUNET_break_op (0); ret = TALER_MHD_reply_with_error (connection, diff --git a/src/backenddb/merchant-0027.sql b/src/backenddb/merchant-0027.sql @@ -27,38 +27,29 @@ SET search_path TO merchant; ALTER TABLE merchant_inventory ADD COLUMN price_array taler_amount_currency[] NOT NULL - DEFAULT ARRAY[]::taler_amount_currency[]; + DEFAULT ARRAY[]::taler_amount_currency[], + ADD COLUMN total_stock_frac INT4 NOT NULL DEFAULT 0, + ADD COLUMN total_sold_frac INT4 NOT NULL DEFAULT 0, + ADD COLUMN total_lost_frac INT4 NOT NULL DEFAULT 0, + ADD COLUMN allow_fractional_quantity BOOL NOT NULL DEFAULT FALSE, + ADD COLUMN fractional_precision_level INT4 NOT NULL DEFAULT 0; 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 + 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 +-- Note: price column is dropped in merchant-0028.sql -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/1000000 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/1000000 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/1000000 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'; diff --git a/src/backenddb/merchant-0028.sql b/src/backenddb/merchant-0028.sql @@ -119,6 +119,7 @@ ALTER TABLE merchant_inventory ADD COLUMN money_pot_serial INT8 DEFAULT (NULL) REFERENCES merchant_money_pots (money_pot_serial) ON DELETE SET NULL, + DROP COLUMN price, -- forgotten to drop in v27 ADD COLUMN price_is_net BOOL DEFAULT (FALSE); COMMENT ON COLUMN merchant_inventory.product_group_serial diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c @@ -45,34 +45,32 @@ TMH_PG_insert_product (void *cls, GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_string (pd->description), - TALER_PQ_query_param_json (pd->description_i18n), + TALER_PQ_query_param_json (pd->description_i18n), /* $4 */ GNUNET_PQ_query_param_string (pd->unit), GNUNET_PQ_query_param_string (pd->image), - TALER_PQ_query_param_json (pd->taxes), - TALER_PQ_query_param_amount_with_currency (pg->conn, - &pd->price), + TALER_PQ_query_param_json (pd->taxes), /* $7 */ 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), + pg->conn), /* $8 */ + GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */ + GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $10 */ 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), + TALER_PQ_query_param_json (pd->address), /* $13 */ GNUNET_PQ_query_param_timestamp (&pd->next_restock), GNUNET_PQ_query_param_uint32 (&pd->minimum_age), GNUNET_PQ_query_param_array_uint64 (num_cats, cats, - pg->conn), + pg->conn), /* $16 */ GNUNET_PQ_query_param_string (pd->product_name), (0 == pd->product_group_id) ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&pd->product_group_id), + : GNUNET_PQ_query_param_uint64 (&pd->product_group_id), /* $18 */ (0 == pd->money_pot_id) ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), - GNUNET_PQ_query_param_bool (pd->price_is_net), + : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), /* $19 */ + GNUNET_PQ_query_param_bool (pd->price_is_net), /* $20 */ GNUNET_PQ_query_param_end }; uint64_t ncat; @@ -105,8 +103,8 @@ TMH_PG_insert_product (void *cls, ",out_no_pot AS no_pot" " FROM merchant_do_insert_product" "($1, $2, $3, $4::TEXT::JSONB, $5, $6, $7::TEXT::JSONB, $8" - ",$9, $10, $11, $12, $13, $14::TEXT::JSONB, $15, $16" - ",$17, $18, $19, $20, $21);"); + ",$9, $10, $11, $12, $13::TEXT::JSONB, $14, $15, $16" + ",$17, $18, $19, $20);"); 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 @@ -19,24 +19,23 @@ CREATE FUNCTION merchant_do_insert_product ( IN in_instance_id TEXT, IN in_product_id TEXT, IN in_description TEXT, - IN in_description_i18n JSONB, + IN in_description_i18n JSONB, -- $4 IN in_unit TEXT, IN in_image TEXT, - IN in_taxes JSONB, - IN in_price taler_amount_currency, + IN in_taxes JSONB, -- $7 IN ina_price_list taler_amount_currency[], - IN in_total_stock INT8, - IN in_total_stock_frac INT4, + IN in_total_stock INT8, -- $9 + IN in_total_stock_frac INT4, --$10 IN in_allow_fractional_quantity BOOL, IN in_fractional_precision_level INT4, - IN in_address JSONB, + IN in_address JSONB, -- $13 IN in_next_restock INT8, IN in_minimum_age INT4, - IN ina_categories INT8[], + IN ina_categories INT8[], -- $16 IN in_product_name TEXT, IN in_product_group_id INT8, -- NULL for default IN in_money_pot_id INT8, -- NULL for none - IN in_price_is_net BOOL, + IN in_price_is_net BOOL, -- $20 OUT out_no_instance BOOL, OUT out_conflict BOOL, OUT out_no_cat INT8, @@ -49,8 +48,6 @@ DECLARE my_product_serial INT8; i INT8; ini_cat INT8; - my_price taler_amount_currency; - my_price_array taler_amount_currency[]; BEGIN out_no_group = FALSE; @@ -99,14 +96,6 @@ THEN END IF; END IF; -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 @@ -117,7 +106,6 @@ INSERT INTO merchant_inventory ,image ,image_hash ,taxes - ,price ,price_array ,total_stock ,total_stock_frac @@ -145,8 +133,7 @@ INSERT INTO merchant_inventory 'hex') END ,in_taxes - ,my_price - ,my_price_array + ,ina_price_list ,in_total_stock ,in_total_stock_frac ,in_allow_fractional_quantity @@ -177,9 +164,8 @@ THEN AND unit=in_unit AND image=in_image AND taxes=in_taxes - AND price=my_price AND to_jsonb(COALESCE(price_array, ARRAY[]::taler_amount_currency[])) - = to_jsonb(COALESCE(my_price_array, ARRAY[]::taler_amount_currency[])) + = to_jsonb(COALESCE(ina_price_list, ARRAY[]::taler_amount_currency[])) -- FIXME: wild. Why so complicated? AND total_stock=in_total_stock AND total_stock_frac=in_total_stock_frac AND allow_fractional_quantity=in_allow_fractional_quantity diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c @@ -88,8 +88,6 @@ lookup_products_cb (void *cls, &pd.description_i18n), GNUNET_PQ_result_spec_string ("unit", &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, @@ -187,7 +185,6 @@ TMH_PG_lookup_all_products (void *cls, ",description_i18n::TEXT" ",product_name" ",unit" - ",price" ",price_array" ",taxes::TEXT" ",total_stock" diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c @@ -48,7 +48,6 @@ TMH_PG_lookup_product (void *cls, ",mi.description_i18n::TEXT" ",mi.product_name" ",mi.unit" - ",mi.price" ",mi.price_array" ",mi.taxes::TEXT" ",mi.total_stock" @@ -113,8 +112,6 @@ TMH_PG_lookup_product (void *cls, &my_name), GNUNET_PQ_result_spec_string ("unit", &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, diff --git a/src/backenddb/pg_update_product.c b/src/backenddb/pg_update_product.c @@ -48,34 +48,32 @@ TMH_PG_update_product (void *cls, GNUNET_PQ_query_param_string (instance_id), /* $1 */ GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_string (pd->description), - TALER_PQ_query_param_json (pd->description_i18n), + TALER_PQ_query_param_json (pd->description_i18n), /* $4 */ GNUNET_PQ_query_param_string (pd->unit), GNUNET_PQ_query_param_string (pd->image), /* $6 */ - TALER_PQ_query_param_json (pd->taxes), - TALER_PQ_query_param_amount_with_currency (pg->conn, - &pd->price), /* $8 */ + TALER_PQ_query_param_json (pd->taxes), /* $7 */ 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 */ + pg->conn), /* $8 */ + GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */ + GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $10 */ + GNUNET_PQ_query_param_bool (pd->allow_fractional_quantity), /* $11 */ GNUNET_PQ_query_param_uint32 (&pd->fractional_precision_level), GNUNET_PQ_query_param_uint64 (&pd->total_lost), - TALER_PQ_query_param_json (pd->address), + TALER_PQ_query_param_json (pd->address), /* $14 */ GNUNET_PQ_query_param_timestamp (&pd->next_restock), GNUNET_PQ_query_param_uint32 (&pd->minimum_age), GNUNET_PQ_query_param_array_uint64 (num_cats, cats, - pg->conn), - GNUNET_PQ_query_param_string (pd->product_name), + pg->conn), /* $17 */ + GNUNET_PQ_query_param_string (pd->product_name), /* $18 */ (0 == pd->product_group_id) ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_uint64 (&pd->product_group_id), (0 == pd->money_pot_id) ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), + : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), /* $20 */ GNUNET_PQ_query_param_bool (pd->price_is_net), GNUNET_PQ_query_param_end }; @@ -125,8 +123,8 @@ TMH_PG_update_product (void *cls, ",out_no_pot AS no_pot" " FROM merchant_do_update_product" "($1,$2,$3,$4::TEXT::JSONB,$5,$6,$7::TEXT::JSONB,$8,$9" - ",$10,$11,$12,$13,$14,$15::TEXT::JSONB,$16,$17,$18" - ",$19,$20,$21,$22);"); + ",$10,$11,$12,$13,$14::TEXT::JSONB,$15,$16,$17,$18" + ",$19,$20,$21);"); 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 @@ -20,25 +20,24 @@ CREATE FUNCTION merchant_do_update_product ( IN in_instance_id TEXT, IN in_product_id TEXT, IN in_description TEXT, - IN in_description_i18n JSONB, + IN in_description_i18n JSONB, -- $4 IN in_unit TEXT, IN in_image TEXT, - IN in_taxes JSONB, - IN in_price taler_amount_currency, + IN in_taxes JSONB, -- $7 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_total_lost INT8, -- NOTE: not in insert_product + IN in_address JSONB, -- $14 IN in_next_restock INT8, IN in_minimum_age INT4, - IN ina_categories INT8[], + IN ina_categories INT8[], -- $17 IN in_product_name TEXT, IN in_product_group_id INT8, -- NULL for default IN in_money_pot_id INT8, -- NULL for none - IN in_price_is_net BOOL, + IN in_price_is_net BOOL, -- $21 OUT out_no_instance BOOL, OUT out_no_product BOOL, OUT out_lost_reduced BOOL, @@ -55,8 +54,6 @@ DECLARE i INT8; ini_cat INT8; rec RECORD; - my_price taler_amount_currency; - my_price_array taler_amount_currency[]; BEGIN out_no_group = FALSE; @@ -123,14 +120,6 @@ 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; @@ -189,8 +178,7 @@ UPDATE merchant_inventory SET 'hex') END ,taxes=in_taxes - ,price=my_price - ,price_array=my_price_array + ,price_array=ina_price_list ,total_stock=in_total_stock ,total_stock_frac=in_total_stock_frac ,allow_fractional_quantity=in_allow_fractional_quantity diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -717,6 +717,12 @@ static void make_product (const char *id, struct ProductData *product) { + static struct TALER_Amount htwenty40; + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:120.40", + &htwenty40)); + memset (product, 0, sizeof (*product)); @@ -727,9 +733,8 @@ make_product (const char *id, GNUNET_assert (NULL != product->product.description_i18n); product->product.unit = "boxes"; product->product.minimum_age = 0; - GNUNET_assert (GNUNET_OK == - TALER_string_to_amount ("EUR:120.40", - &product->product.price)); + product->product.price_array = &htwenty40; + product->product.price_array_length = 1; product->product.taxes = json_array (); GNUNET_assert (NULL != product->product.taxes); product->product.total_stock = 55; @@ -775,11 +780,7 @@ check_products_equal (const struct TALER_MERCHANTDB_ProductDetails *a, b->description_i18n)) || (0 != strcmp (a->unit, b->unit)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&a->price, - &b->price)) || - (0 != TALER_amount_cmp (&a->price, - &b->price)) || + (a->price_array_length != b->price_array_length) || (1 != json_equal (a->taxes, b->taxes)) || (a->total_stock != b->total_stock) || @@ -794,6 +795,13 @@ check_products_equal (const struct TALER_MERCHANTDB_ProductDetails *a, b->next_restock))) return 1; + for (size_t i = 0; i<a->price_array_length; i++) + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&a->price_array[i], + &b->price_array[i])) || + (0 != TALER_amount_cmp (&a->price_array[i], + &b->price_array[i])) ) + return 1; return 0; } @@ -1121,6 +1129,8 @@ struct TestProducts_Closure static void pre_test_products (struct TestProducts_Closure *cls) { + static struct TALER_Amount four95; + /* Instance */ make_instance ("test_inst_products", &cls->instance); @@ -1134,9 +1144,12 @@ pre_test_products (struct TestProducts_Closure *cls) cls->products[1].product.description = "This is a another test product"; cls->products[1].product.unit = "cans"; cls->products[1].product.minimum_age = 0; + GNUNET_assert (GNUNET_OK == TALER_string_to_amount ("EUR:4.95", - &cls->products[1].product.price)); + &four95)); + cls->products[1].product.price_array = &four95; + cls->products[1].product.price_array_length = 1; cls->products[1].product.total_stock = 5001; } @@ -1242,9 +1255,15 @@ run_test_products (struct TestProducts_Closure *cls) json_string ( "description in another language"))); cls->products[0].product.unit = "barrels"; - GNUNET_assert (GNUNET_OK == - TALER_string_to_amount ("EUR:7.68", - &cls->products[0].product.price)); + { + static struct TALER_Amount seven68; + + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount ("EUR:7.68", + &seven68)); + cls->products[0].product.price_array = &seven68; + cls->products[0].product.price_array_length = 1; + } GNUNET_assert (0 == json_array_append_new (cls->products[0].product.taxes, json_string ("2% sales tax"))); diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -33,7 +33,7 @@ * Six decimal digits are supported to match the maximum unit precision. */ #define TALER_MERCHANT_UNIT_FRAC_BASE 1000000U -#define TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS 6U +#define TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS 6U /** * Return default project data used by Taler merchant. @@ -732,6 +732,41 @@ struct TALER_MERCHANT_ProductQuantity /** + * How to round when computing with amounts? + */ +enum TALER_MERCHANT_RoundMode +{ + TALER_MERCHANT_ROUND_NEAREST, + TALER_MERCHANT_ROUND_UP, + TALER_MERCHANT_ROUND_DOWN +}; + + +/** + * Multiply the @a unit_price by the quantity given in @a factor. + * Round the result using the given rounding mode @a rm to a + * multiple of the @a atomic_amount. + * + * @param[out] result where to store the result + * @param unit_price price for one item + * @param factor quantity to purchase, can be factional + * @param rm rounding mode to apply + * @param atomic_amount granularity to round to + * @return #GNUNET_OK on success + * #GNUNET_NO on integer overflow (resulting amount cannot be represented) + * #GNUNET_SYSERR on internal failure (e.g. currency + * @a unit_price and @a atomic_amount do not match) + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_amount_multiply_by_quantity ( + struct TALER_Amount *result, + const struct TALER_Amount *unit_price, + const struct TALER_MERCHANT_ProductQuantity *factor, + enum TALER_MERCHANT_RoundMode rm, + const struct TALER_Amount *atomic_amount); + + +/** * Details about a product (to be) sold. */ struct TALER_MERCHANT_ProductSold @@ -1106,6 +1141,18 @@ TALER_MERCHANT_parse_choice_input ( /** + * Check if @a taxes is an array of valid Taxes in the sense of + * Taler's API definition. + * + * @param taxes array to check + * @return true if @a taxes is an array and all + * entries are valid Taxes. + */ +bool +TALER_MERCHANT_taxes_array_valid (const json_t *taxes); + + +/** * Parse JSON product given in @a p, returning the result in * @a r. * diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -346,12 +346,6 @@ struct TALER_MERCHANTDB_ProductDetails char *unit; /** - * Price per unit of the product. Zero to imply that the - * product is not sold separately or that the price is not fixed. - */ - struct TALER_Amount price; - - /** * Optional list of per-unit prices. When NULL or empty, @e price * must be used as the canonical single price. */ diff --git a/src/lib/merchant_api_common.c b/src/lib/merchant_api_common.c @@ -49,13 +49,13 @@ TALER_MERCHANT_format_fractional_string (uint64_t integer, return; } { - char frac_buf[TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; + char frac_buf[TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; size_t idx; GNUNET_snprintf (frac_buf, sizeof (frac_buf), "%0*u", - TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS, + TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS, (unsigned int) fractional); for (idx = strlen (frac_buf); idx > 0; idx--) { diff --git a/src/util/Makefile.am b/src/util/Makefile.am @@ -39,6 +39,7 @@ lib_LTLIBRARIES = \ libtalermerchantutil.la libtalermerchantutil_la_SOURCES = \ + amount_quantity.c \ contract_parse.c \ contract_serialize.c \ json.c \ diff --git a/src/util/amount_quantity.c b/src/util/amount_quantity.c @@ -0,0 +1,355 @@ +/* + 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 Lesser 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 amount_quantity.c + * @brief Parsing quantities and other decimal fractions + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_db_lib.h> +#include <taler/taler_json_lib.h> +#include "taler_merchant_util.h" + + +/** + * Multiply two 64-bit values and store result as high/low 64-bit parts. + */ +static void +mul64_overflow (uint64_t a, + uint64_t b, + uint64_t *hi, + uint64_t *lo) +{ + uint64_t a_lo = a & 0xFFFFFFFF; + uint64_t a_hi = a >> 32; + uint64_t b_lo = b & 0xFFFFFFFF; + uint64_t b_hi = b >> 32; + + uint64_t p0 = a_lo * b_lo; + uint64_t p1 = a_lo * b_hi; + uint64_t p2 = a_hi * b_lo; + uint64_t p3 = a_hi * b_hi; + + uint64_t carry = ((p0 >> 32) + (p1 & 0xFFFFFFFF) + (p2 & 0xFFFFFFFF)) >> 32; + + *lo = p0 + (p1 << 32) + (p2 << 32); + *hi = p3 + (p1 >> 32) + (p2 >> 32) + carry; +} + + +/** + * Add two 128-bit numbers represented as hi/lo pairs. + * Returns 1 on overflow, 0 otherwise. + */ +static int +add128 (uint64_t a_hi, + uint64_t a_lo, + uint64_t b_hi, + uint64_t b_lo, + uint64_t *r_hi, + uint64_t *r_lo) +{ + uint64_t carry; + + *r_lo = a_lo + b_lo; + carry = (*r_lo < a_lo) ? 1 : 0; + *r_hi = a_hi + b_hi + carry; + + return (*r_hi < a_hi) || ((*r_hi == a_hi) && carry && (b_hi == UINT64_MAX)); +} + + +/** + * Subtract two 128-bit numbers represented as hi/lo pairs. + * Returns 1 on underflow, 0 otherwise. + */ +static int +sub128 (uint64_t a_hi, + uint64_t a_lo, + uint64_t b_hi, + uint64_t b_lo, + uint64_t *r_hi, + uint64_t *r_lo) +{ + uint64_t carry; + + carry = (a_lo < b_lo) ? 1 : 0; + *r_lo = a_lo - b_lo; + *r_hi = a_hi - b_hi - carry; + + return (a_hi < b_hi) || ((a_hi == b_hi) && carry); +} + + +/** + * Divide a 128-bit number by a 64-bit number. + * Returns quotient in q_hi/q_lo and remainder in r. + */ +static void +div128_64 (uint64_t n_hi, + uint64_t n_lo, + uint64_t d, + uint64_t *q_hi, + uint64_t *q_lo, + uint64_t *r) +{ + uint64_t remainder; + + if (0 == n_hi) + { + *q_hi = 0; + *q_lo = n_lo / d; + *r = n_lo % d; + return; + } + + /* Note: very slow method, could be done faster, but + in practice we expect the above short-cut to apply + in virtually all cases, so we keep it simple here; + also, if it mattered, we should use __uint128_t on + systems that support it. */ + remainder = 0; + *q_hi = 0; + *q_lo = 0; + for (int i = 127; i >= 0; i--) + { + remainder <<= 1; + if (i >= 64) + remainder |= (n_hi >> (i - 64)) & 1; + else + remainder |= (n_lo >> i) & 1; + + if (remainder >= d) + { + remainder -= d; + if (i >= 64) + *q_hi |= (1ULL << (i - 64)); + else + *q_lo |= (1ULL << i); + } + } + *r = remainder; +} + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_amount_multiply_by_quantity ( + struct TALER_Amount *result, + const struct TALER_Amount *unit_price, + const struct TALER_MERCHANT_ProductQuantity *factor, + enum TALER_MERCHANT_RoundMode rm, + const struct TALER_Amount *atomic_amount) +{ + uint64_t price_hi; + uint64_t price_lo; + uint64_t factor_hi; + uint64_t factor_lo; + uint64_t prod_hi; + uint64_t prod_lo; + uint64_t raw_hi; + uint64_t raw_lo; + uint64_t rem; + uint64_t atomic_hi; + uint64_t atomic_lo; + uint64_t rounded_hi; + uint64_t rounded_lo; + uint64_t remainder; + + if (GNUNET_OK != + TALER_amount_cmp_currency (unit_price, + atomic_amount)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + GNUNET_assert (factor->fractional < TALER_MERCHANT_UNIT_FRAC_BASE); + GNUNET_assert (unit_price->fraction < TALER_AMOUNT_FRAC_BASE); + GNUNET_assert (atomic_amount->fraction < TALER_AMOUNT_FRAC_BASE); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (unit_price->currency, + result)); + + if (TALER_amount_is_zero (atomic_amount)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Convert unit_price to fractional units */ + mul64_overflow (unit_price->value, + TALER_AMOUNT_FRAC_BASE, + &price_hi, + &price_lo); + if (add128 (price_hi, + price_lo, + 0, + unit_price->fraction, + &price_hi, + &price_lo)) + return GNUNET_NO; + + /* Convert factor to fractional units */ + mul64_overflow (factor->integer, + TALER_MERCHANT_UNIT_FRAC_BASE, + &factor_hi, + &factor_lo); + if (add128 (factor_hi, + factor_lo, + 0, + factor->fractional, + &factor_hi, + &factor_lo)) + return GNUNET_NO; + + /* Multiply price by factor: (price_hi:price_lo) * (factor_hi:factor_lo) */ + { + uint64_t p0_hi, p0_lo, p1_hi, p1_lo, p2_hi, p2_lo, p3_hi, p3_lo; + + mul64_overflow (price_lo, + factor_lo, + &p0_hi, + &p0_lo); + mul64_overflow (price_lo, + factor_hi, + &p1_hi, + &p1_lo); + mul64_overflow (price_hi, + factor_lo, + &p2_hi, + &p2_lo); + mul64_overflow (price_hi, + factor_hi, + &p3_hi, + &p3_lo); + /* Check for overflow in 128-bit result */ + if ( (0 != p3_hi) || + (0 != p3_lo) || + (0 != p2_hi) || + (0 != p1_hi) ) + return GNUNET_NO; + + /* Add all fractions together */ + prod_hi = p0_hi; + prod_lo = p0_lo; + if (add128 (prod_hi, + prod_lo, + p1_lo, + 0, + &prod_hi, + &prod_lo)) + return GNUNET_NO; + if (add128 (prod_hi, + prod_lo, + p2_lo, + 0, + &prod_hi, + &prod_lo)) + return GNUNET_NO; + } + + /* Divide by MERCHANT_UNIT_FRAC_BASE */ + div128_64 (prod_hi, + prod_lo, + TALER_MERCHANT_UNIT_FRAC_BASE, + &raw_hi, + &raw_lo, + &rem); + + /* Convert atomic_amount to fractional units */ + mul64_overflow (atomic_amount->value, + TALER_AMOUNT_FRAC_BASE, + &atomic_hi, + &atomic_lo); + if (add128 (atomic_hi, + atomic_lo, + 0, + atomic_amount->fraction, + &atomic_hi, + &atomic_lo)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (atomic_hi > 0) + { + /* outside of supported range */ + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Compute remainder when dividing by atomic_amount and round down */ + { + uint64_t q_hi, q_lo; + uint64_t half_atomic = atomic_lo >> 1; + bool round_up = false; + + div128_64 (raw_hi, + raw_lo, + atomic_lo, + &q_hi, + &q_lo, + &remainder); + sub128 (raw_hi, + raw_lo, + 0, + remainder, + &rounded_hi, + &rounded_lo); + switch (rm) + { + case TALER_MERCHANT_ROUND_NEAREST: + round_up = (remainder > half_atomic) || + (remainder == half_atomic && (q_lo & 1)); + break; + case TALER_MERCHANT_ROUND_UP: + round_up = (remainder > 0); + break; + case TALER_MERCHANT_ROUND_DOWN: + break; + } + if ( (round_up) && + (add128 (rounded_hi, + rounded_lo, + atomic_hi, + atomic_lo, + &rounded_hi, + &rounded_lo)) ) + return GNUNET_NO; + } + + /* Convert back to value and fraction */ + { + uint64_t final_value; + uint64_t final_fraction; + uint64_t q_hi; + + div128_64 (rounded_hi, + rounded_lo, + TALER_AMOUNT_FRAC_BASE, + &q_hi, + &final_value, + &final_fraction); + + /* Check for overflow */ + if (0 != q_hi) + return GNUNET_NO; + + result->value = final_value; + result->fraction = (uint32_t) final_fraction; + } + return GNUNET_OK; +} diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c @@ -1170,7 +1170,7 @@ parse_unit_quantity (const char *s, { unsigned int digit = (unsigned int) (*ptr - '0'); - if (digits >= TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + if (digits >= TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) { GNUNET_break_op (0); return GNUNET_SYSERR; @@ -1179,7 +1179,7 @@ parse_unit_quantity (const char *s, digits++; ptr++; } - while (digits < TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + while (digits < TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) { frac *= 10; digits++; @@ -1196,6 +1196,43 @@ parse_unit_quantity (const char *s, } +bool +TALER_MERCHANT_taxes_array_valid (const json_t *taxes) +{ + json_t *tax; + size_t idx; + + if (! json_is_array (taxes)) + return false; + json_array_foreach (taxes, idx, tax) + { + struct TALER_Amount amount; + const char *name; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + TALER_JSON_spec_amount_any ("tax", + &amount), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (tax, + spec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return false; + } + } + return true; +} + + enum GNUNET_GenericReturnValue TALER_MERCHANT_parse_product_sold (const json_t *pj, struct TALER_MERCHANT_ProductSold *r) @@ -1299,6 +1336,18 @@ TALER_MERCHANT_parse_product_sold (const json_t *pj, GNUNET_break ( (0 == r->unit_quantity.fractional) && (legacy_quantity == r->unit_quantity.integer) ); } + if ( (NULL != r->image) && + (! TALER_MERCHANT_image_data_url_valid (r->image)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (NULL != r->taxes) && + (! TALER_MERCHANT_taxes_array_valid (r->taxes)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } if (NULL != prices) { size_t len = json_array_size (prices); diff --git a/src/util/contract_serialize.c b/src/util/contract_serialize.c @@ -429,8 +429,8 @@ TALER_MERCHANT_product_sold_serialize ( GNUNET_JSON_pack_string ("description", p->description)), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("description_i18n", - p->description_i18n)), + GNUNET_JSON_pack_object_incref ("description_i18n", + (json_t *) p->description_i18n)), GNUNET_JSON_pack_allow_null ( ( (0 != p->unit_quantity.integer) || (0 != p->unit_quantity.fractional) ) @@ -451,7 +451,7 @@ TALER_MERCHANT_product_sold_serialize ( /* Deprecated, use prices! */ GNUNET_JSON_pack_allow_null ( TALER_JSON_pack_amount ("price", - TALER_amount_is_valid (&p->prices[0]) + 0 < p->prices_length ? &p->prices[0] : NULL)), GNUNET_JSON_pack_array_steal ("prices", @@ -460,8 +460,8 @@ TALER_MERCHANT_product_sold_serialize ( GNUNET_JSON_pack_string ("image", p->image)), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ("taxes", - p->taxes)), + GNUNET_JSON_pack_array_incref ("taxes", + (json_t *) p->taxes)), GNUNET_JSON_pack_allow_null ( GNUNET_TIME_absolute_is_never (p->delivery_date.abs_time) ? GNUNET_JSON_pack_string ("dummy", diff --git a/src/util/value_kinds.c b/src/util/value_kinds.c @@ -85,7 +85,7 @@ TALER_MERCHANT_vk_parse_fractional_string ( { unsigned int digit = (unsigned int) (*ptr - '0'); - if (digits >= TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + if (digits >= TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) { GNUNET_break_op (0); return GNUNET_SYSERR; @@ -94,7 +94,7 @@ TALER_MERCHANT_vk_parse_fractional_string ( digits++; ptr++; } - while (digits < TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + while (digits < TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) { frac *= 10; digits++; @@ -294,13 +294,13 @@ TALER_MERCHANT_vk_format_fractional_string ( return; } { - char frac_buf[TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; + char frac_buf[TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS + 1]; size_t idx; GNUNET_snprintf (frac_buf, sizeof (frac_buf), "%0*u", - TALER_TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS, + TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS, (unsigned int) fractional); for (idx = strlen (frac_buf); idx > 0; idx--) {