merchant

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

commit 7d3cd38106c7bc3d0ac6c3455d9b6758ce05ae34
parent a853c303e488e6da1b65b7e6b42c1d1d5d81035e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 28 Dec 2025 14:43:28 +0100

add product groups, pots and net-v-gross status to products in REST API

Diffstat:
Msrc/backend/taler-merchant-httpd_private-get-products-ID.c | 6++++++
Msrc/backend/taler-merchant-httpd_private-patch-products-ID.c | 24+++++++++++++++++++++++-
Msrc/backend/taler-merchant-httpd_private-post-products.c | 36+++++++++++++++++++++++++++++++++++-
Msrc/backenddb/pg_insert_product.c | 20++++++++++++++++++--
Msrc/backenddb/pg_insert_product.h | 6+++++-
Msrc/backenddb/pg_insert_product.sql | 54++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/backenddb/pg_lookup_all_products.c | 15+++++++++++++++
Msrc/backenddb/pg_lookup_product.c | 21++++++++++++++++++---
Msrc/backenddb/pg_update_product.c | 20++++++++++++++++++--
Msrc/backenddb/pg_update_product.h | 6+++++-
Msrc/backenddb/pg_update_product.sql | 36+++++++++++++++++++++++++++++++++++-
Msrc/backenddb/test_merchantdb.c | 42+++++++++++++++++++++++++-----------------
Msrc/include/taler_merchantdb_plugin.h | 32++++++++++++++++++++++++++++++--
13 files changed, 283 insertions(+), 35 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c @@ -135,6 +135,12 @@ TMH_private_get_products_ID ( GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_timestamp ("next_restock", (pd.next_restock))), + GNUNET_JSON_pack_uint64 ("product_group_id", + pd.product_group_id), + GNUNET_JSON_pack_uint64 ("money_pot_id", + pd.money_pot_id), + GNUNET_JSON_pack_bool ("price_is_net", + pd.price_is_net), GNUNET_JSON_pack_uint64 ("minimum_age", pd.minimum_age)); TALER_MERCHANTDB_product_details_free (&pd); diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -135,6 +135,8 @@ TMH_private_patch_products_ID ( bool lost_reduced; bool sold_reduced; bool stock_reduced; + bool no_group; + bool no_pot; pd.total_sold = 0; /* will be ignored anyway */ GNUNET_assert (NULL != mi); @@ -343,7 +345,9 @@ TMH_private_patch_products_ID ( &no_product, &lost_reduced, &sold_reduced, - &stock_reduced); + &stock_reduced, + &no_group, + &no_pot); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -401,6 +405,24 @@ TMH_private_patch_products_ID ( product_id); goto cleanup; } + if (no_group) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, + NULL); + goto cleanup; + } + if (no_pot) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + NULL); + goto cleanup; + } if (lost_reduced) { ret = TALER_MHD_reply_with_error ( diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c @@ -111,6 +111,18 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, GNUNET_JSON_spec_uint32 ("minimum_age", &pd.minimum_age), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("money_pot_id", + &pd.money_pot_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("product_group_id", + &pd.product_group_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("price_is_net", + &pd.price_is_net), + NULL), GNUNET_JSON_spec_end () }; size_t num_cats = 0; @@ -118,6 +130,8 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, bool conflict; bool no_instance; ssize_t no_cat; + bool no_group; + bool no_pot; enum GNUNET_DB_QueryStatus qs; MHD_RESULT ret; @@ -314,7 +328,9 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, cats, &no_instance, &conflict, - &no_cat); + &no_cat, + &no_group, + &no_pot); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -346,6 +362,24 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, mi->settings.id); goto cleanup; } + if (no_group) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, + NULL); + goto cleanup; + } + if (no_pot) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + NULL); + goto cleanup; + } if (conflict) { ret = TALER_MHD_reply_with_error ( diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c @@ -36,7 +36,9 @@ TMH_PG_insert_product (void *cls, const uint64_t *cats, bool *no_instance, bool *conflict, - ssize_t *no_cat) + ssize_t *no_cat, + bool *no_group, + bool *no_pot) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -64,6 +66,13 @@ TMH_PG_insert_product (void *cls, cats, pg->conn), 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), + (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_end }; uint64_t ncat; @@ -77,6 +86,10 @@ TMH_PG_insert_product (void *cls, GNUNET_PQ_result_spec_uint64 ("no_cat", &ncat), &cats_found), + GNUNET_PQ_result_spec_bool ("no_group", + no_group), + GNUNET_PQ_result_spec_bool ("no_pot", + no_pot), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; @@ -88,9 +101,12 @@ TMH_PG_insert_product (void *cls, " out_conflict AS conflict" ",out_no_instance AS no_instance" ",out_no_cat AS no_cat" + ",out_no_group AS no_group" + ",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);"); + ",$9, $10, $11, $12, $13, $14::TEXT::JSONB, $15, $16" + ",$17, $18, $19, $20, $21);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "insert_product", params, diff --git a/src/backenddb/pg_insert_product.h b/src/backenddb/pg_insert_product.h @@ -38,6 +38,8 @@ * @param[out] conflict set to true if a conflicting * product already exists in the database * @param[out] no_cat set to index of non-existing category from @a cats, or -1 if all @a cats were found + * @param[out] no_group set to true if the product group in @a pd is unknown + * @param[out] no_pot set to true if the money pot in @a pd is unknown * @return database result code */ enum GNUNET_DB_QueryStatus @@ -49,7 +51,9 @@ TMH_PG_insert_product (void *cls, const uint64_t *cats, bool *no_instance, bool *conflict, - ssize_t *no_cat); + ssize_t *no_cat, + bool *no_group, + bool *no_pot); #endif diff --git a/src/backenddb/pg_insert_product.sql b/src/backenddb/pg_insert_product.sql @@ -34,9 +34,14 @@ CREATE FUNCTION merchant_do_insert_product ( IN in_minimum_age INT4, IN ina_categories INT8[], 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, OUT out_no_instance BOOL, OUT out_conflict BOOL, - OUT out_no_cat INT8) + OUT out_no_cat INT8, + OUT out_no_group BOOL, + OUT out_no_pot BOOL) LANGUAGE plpgsql AS $$ DECLARE @@ -48,6 +53,9 @@ DECLARE my_price_array taler_amount_currency[]; BEGIN +out_no_group = FALSE; +out_no_pot = FALSE; + -- Which instance are we using? SELECT merchant_serial INTO my_merchant_id @@ -63,6 +71,34 @@ THEN END IF; out_no_instance=FALSE; +IF in_product_group_id IS NOT NULL +THEN + PERFORM FROM merchant_product_groups + WHERE product_group_serial=in_product_group_id + AND merchant_serial=my_merchant_id; + IF NOT FOUND + THEN + out_no_group=TRUE; + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; + END IF; +END IF; + +IF in_money_pot_id IS NOT NULL +THEN + PERFORM FROM merchant_money_pots + WHERE money_pot_serial=in_money_pot_id + AND merchant_serial=my_merchant_id; + IF NOT FOUND + THEN + out_no_pot=TRUE; + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; + END IF; +END IF; + IF COALESCE (array_length(ina_price_list,1),0) = 0 THEN my_price_array := ARRAY[in_price]::taler_amount_currency[]; @@ -89,7 +125,10 @@ INSERT INTO merchant_inventory ,fractional_precision_level ,address ,next_restock - ,minimum_age + ,minimum_age + ,product_group_serial + ,money_pot_serial + ,price_is_net ) VALUES ( my_merchant_id ,in_product_id @@ -114,7 +153,11 @@ INSERT INTO merchant_inventory ,in_fractional_precision_level ,in_address ,in_next_restock - ,in_minimum_age) + ,in_minimum_age + ,in_product_group_id + ,in_money_pot_id + ,in_price_is_net + ) ON CONFLICT (merchant_serial, product_id) DO NOTHING RETURNING product_serial INTO my_product_serial; @@ -143,7 +186,10 @@ THEN AND fractional_precision_level=in_fractional_precision_level AND address=in_address AND next_restock=in_next_restock - AND minimum_age=in_minimum_age; + AND minimum_age=in_minimum_age + AND product_group_serial IS NOT DISTINCT FROM in_product_group_id + AND money_pot_serial IS NOT DISTINCT FROM in_money_pot_id + AND price_is_net=in_price_is_net; IF NOT FOUND THEN out_conflict=TRUE; diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c @@ -124,9 +124,21 @@ lookup_products_cb (void *cls, "categories", &num_categories, &categories), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("product_group_serial", + &pd.product_group_id), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("money_pot_serial", + &pd.money_pot_id), + NULL), + GNUNET_PQ_result_spec_bool ("price_is_net", + &pd.price_is_net), GNUNET_PQ_result_spec_end }; + pd.product_group_id = 0; + pd.money_pot_id = 0; if (GNUNET_OK != GNUNET_PQ_extract_result (result, rs, @@ -193,6 +205,9 @@ TMH_PG_lookup_all_products (void *cls, ",product_id" ",product_serial" ",t.category_array AS categories" + ",product_group_serial" + ",money_pot_serial" + ",price_is_net" " FROM merchant_inventory minv" " JOIN merchant_instances inst" " USING (merchant_serial)" diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c @@ -51,9 +51,9 @@ TMH_PG_lookup_product (void *cls, ",mi.price" ",mi.price_array" ",mi.taxes::TEXT" - ",mi.total_stock" - ",mi.total_stock_frac" - ",mi.allow_fractional_quantity" + ",mi.total_stock" + ",mi.total_stock_frac" + ",mi.allow_fractional_quantity" ",mi.fractional_precision_level" ",mi.total_sold" ",mi.total_sold_frac" @@ -63,6 +63,9 @@ TMH_PG_lookup_product (void *cls, ",mi.address::TEXT" ",mi.next_restock" ",mi.minimum_age" + ",mi.product_group_serial" + ",mi.money_pot_serial" + ",mi.price_is_net" ",t.category_array AS categories" " FROM merchant_inventory mi" " JOIN merchant_instances inst" @@ -146,11 +149,23 @@ TMH_PG_lookup_product (void *cls, "categories", num_categories, &my_categories), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("product_group_serial", + &pd->product_group_id), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("money_pot_serial", + &pd->money_pot_id), + NULL), + GNUNET_PQ_result_spec_bool ("price_is_net", + &pd->price_is_net), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; check_connection (pg); + pd->product_group_id = 0; + pd->money_pot_id = 0; qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "lookup_product", params, diff --git a/src/backenddb/pg_update_product.c b/src/backenddb/pg_update_product.c @@ -39,7 +39,9 @@ TMH_PG_update_product (void *cls, bool *no_product, bool *lost_reduced, bool *sold_reduced, - bool *stocked_reduced) + bool *stocked_reduced, + bool *no_group, + bool *no_pot) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -68,6 +70,13 @@ TMH_PG_update_product (void *cls, cats, pg->conn), 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), + (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_end }; uint64_t ncat; @@ -87,6 +96,10 @@ TMH_PG_update_product (void *cls, GNUNET_PQ_result_spec_uint64 ("no_cat", &ncat), &cats_found), + GNUNET_PQ_result_spec_bool ("no_group", + no_group), + GNUNET_PQ_result_spec_bool ("no_pot", + no_pot), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; @@ -108,9 +121,12 @@ TMH_PG_update_product (void *cls, ",out_no_product AS no_product" ",out_no_cat AS no_cat" ",out_no_instance AS no_instance" + ",out_no_group AS no_group" + ",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);"); + ",$10,$11,$12,$13,$14,$15::TEXT::JSONB,$16,$17,$18" + ",$19,$20,$21,$22);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "update_product", params, diff --git a/src/backenddb/pg_update_product.h b/src/backenddb/pg_update_product.h @@ -48,6 +48,8 @@ * exceed total_stock minus the existing total_sold; * total_sold and total_stock must be larger or equal to * the existing value; + * @param[out] no_group set to true if the product group in @a pd is unknown + * @param[out] no_pot set to true if the money pot in @a pd is unknown * @return database result code */ enum GNUNET_DB_QueryStatus @@ -62,6 +64,8 @@ TMH_PG_update_product (void *cls, bool *no_product, bool *lost_reduced, bool *sold_reduced, - bool *stocked_reduced); + bool *stocked_reduced, + bool *no_group, + bool *no_pot); #endif diff --git a/src/backenddb/pg_update_product.sql b/src/backenddb/pg_update_product.sql @@ -36,12 +36,17 @@ CREATE FUNCTION merchant_do_update_product ( IN in_minimum_age INT4, IN ina_categories INT8[], 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, OUT out_no_instance BOOL, OUT out_no_product BOOL, OUT out_lost_reduced BOOL, OUT out_sold_reduced BOOL, OUT out_stocked_reduced BOOL, - OUT out_no_cat INT8) + OUT out_no_cat INT8, + OUT out_no_group BOOL, + OUT out_no_pot BOOL) LANGUAGE plpgsql AS $$ DECLARE @@ -54,6 +59,8 @@ DECLARE my_price_array taler_amount_currency[]; BEGIN +out_no_group = FALSE; +out_no_pot = FALSE; out_no_instance=FALSE; out_no_product=FALSE; out_lost_reduced=FALSE; @@ -73,6 +80,30 @@ THEN RETURN; END IF; +IF in_product_group_id IS NOT NULL +THEN + PERFORM FROM merchant_product_groups + WHERE product_group_serial=in_product_group_id + AND merchant_serial=my_merchant_id; + IF NOT FOUND + THEN + out_no_group=TRUE; + RETURN; + END IF; +END IF; + +IF in_money_pot_id IS NOT NULL +THEN + PERFORM FROM merchant_money_pots + WHERE money_pot_serial=in_money_pot_id + AND merchant_serial=my_merchant_id; + IF NOT FOUND + THEN + out_no_pot=TRUE; + RETURN; + END IF; +END IF; + -- Check existing entry satisfies constraints SELECT total_stock ,total_stock_frac @@ -168,6 +199,9 @@ UPDATE merchant_inventory SET ,address=in_address ,next_restock=in_next_restock ,minimum_age=in_minimum_age + ,product_group_serial=in_product_group_id + ,money_pot_serial=in_money_pot_id + ,price_is_net=in_price_is_net WHERE merchant_serial=my_merchant_id AND product_serial=my_product_serial; -- could also match on product_id 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 ********** */ @@ -824,6 +824,8 @@ test_insert_product (const struct InstanceData *instance, bool conflict; bool no_instance; ssize_t no_cat; + bool no_group; + bool no_pot; TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_product (plugin->cls, @@ -834,7 +836,9 @@ test_insert_product (const struct InstanceData *instance, cats, &no_instance, &conflict, - &no_cat), + &no_cat, + &no_group, + &no_pot), "Insert product failed\n"); if (expected_result > 0) { @@ -877,6 +881,8 @@ test_update_product (const struct InstanceData *instance, bool lost_reduced; bool sold_reduced; bool stocked_reduced; + bool no_group; + bool no_pot; TEST_COND_RET_ON_FAIL ( expected_result == @@ -891,7 +897,9 @@ test_update_product (const struct InstanceData *instance, &no_product, &lost_reduced, &sold_reduced, - &stocked_reduced), + &stocked_reduced, + &no_group, + &no_pot), "Update product failed\n"); if (expected_result > 0) { diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -431,6 +431,23 @@ struct TALER_MERCHANTDB_ProductDetails * restrictions. */ uint32_t minimum_age; + + /** + * Group in which the product is in. 0 for default group. + */ + uint64_t product_group_id; + + /** + * Money pot into which sales of this product should go into by default. + */ + uint64_t money_pot_id; + + /** + * True if the price for this product is given in net, + * False if its the gross price. + */ + bool price_is_net; + }; @@ -2404,6 +2421,7 @@ struct TALER_MERCHANTDB_Plugin struct TALER_MERCHANTDB_ProductDetails *pd, size_t *num_categories, uint64_t **categories); + /** * Lookup product image by its hash. * @@ -2447,6 +2465,8 @@ struct TALER_MERCHANTDB_Plugin * @param[out] conflict set to true if a conflicting * product already exists in the database * @param[out] no_cat set to index of non-existing category from @a cats, or -1 if all @a cats were found + * @param[out] no_group set to true if the product group in @a pd is unknown + * @param[out] no_pot set to true if the money pot in @a pd is unknown * @return database result code */ enum GNUNET_DB_QueryStatus @@ -2458,7 +2478,10 @@ struct TALER_MERCHANTDB_Plugin const uint64_t *cats, bool *no_instance, bool *conflict, - ssize_t *no_cat); + ssize_t *no_cat, + bool *no_group, + bool *no_pot); + /** * Update details about a particular product. Note that the @@ -2479,6 +2502,8 @@ struct TALER_MERCHANTDB_Plugin * @param[out] lost_reduced the update failed as the counter of units lost would have been lowered * @param[out] sold_reduced the update failed as the counter of units sold would have been lowered * @param[out] stocked_reduced the update failed as the counter of units stocked would have been lowered + * @param[out] no_group set to true if the product group in @a pd is unknown + * @param[out] no_pot set to true if the money pot in @a pd is unknown * @return database result code */ enum GNUNET_DB_QueryStatus @@ -2493,7 +2518,10 @@ struct TALER_MERCHANTDB_Plugin bool *no_product, bool *lost_reduced, bool *sold_reduced, - bool *stocked_reduced); + bool *stocked_reduced, + bool *no_group, + bool *no_pot); + /** * Lock stocks of a particular product. Note that the transaction must