diff options
-rw-r--r-- | src/backend/taler-merchant-httpd_private-post-products.c | 285 | ||||
-rw-r--r-- | src/backenddb/merchant-0001.sql | 2 | ||||
-rw-r--r-- | src/backenddb/merchant-0006.sql | 3 | ||||
-rw-r--r-- | src/backenddb/pg_insert_product.c | 63 | ||||
-rw-r--r-- | src/backenddb/pg_insert_product.h | 14 | ||||
-rw-r--r-- | src/backenddb/pg_insert_product.sql | 175 | ||||
-rw-r--r-- | src/backenddb/procedures.sql.in | 1 | ||||
-rw-r--r-- | src/backenddb/test_merchantdb.c | 82 | ||||
-rw-r--r-- | src/include/taler_merchant_service.h | 48 | ||||
-rw-r--r-- | src/include/taler_merchantdb_plugin.h | 13 | ||||
-rw-r--r-- | src/lib/merchant_api_post_products.c | 59 |
11 files changed, 549 insertions, 196 deletions
diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c index 3cad91a9..3edc0c16 100644 --- a/src/backend/taler-merchant-httpd_private-post-products.c +++ b/src/backend/taler-merchant-httpd_private-post-products.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020 Taler Systems SA + (C) 2020-2024 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 @@ -28,50 +28,6 @@ #include <taler/taler_json_lib.h> -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Check if the two products are identical. - * - * @param p1 product to compare - * @param p2 other product to compare - * @return true if they are 'equal', false if not or of payto_uris is not an array - */ -static bool -products_equal (const struct TALER_MERCHANTDB_ProductDetails *p1, - const struct TALER_MERCHANTDB_ProductDetails *p2) -{ - return ( (0 == strcmp (p1->description, - p2->description)) && - (1 == json_equal (p1->description_i18n, - p2->description_i18n)) && - (0 == strcmp (p1->unit, - p2->unit)) && - (GNUNET_OK == - TALER_amount_cmp_currency (&p1->price, - &p2->price)) && - (0 == TALER_amount_cmp (&p1->price, - &p2->price)) && - (1 == json_equal (p1->taxes, - p2->taxes)) && - (p1->total_stock == p2->total_stock) && - (p1->total_sold == p2->total_sold) && - (p1->total_lost == p2->total_lost) && - (p1->minimum_age == p2->minimum_age) && - (0 == strcmp (p1->image, - p2->image)) && - (1 == json_equal (p1->address, - p2->address)) && - (GNUNET_TIME_timestamp_cmp (p1->next_restock, - ==, - p2->next_restock) ) ); -} - - MHD_RESULT TMH_private_post_products (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, @@ -79,9 +35,9 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, { struct TMH_MerchantInstance *mi = hc->instance; struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; + const json_t *categories = NULL; const char *product_id; int64_t total_stock; - enum GNUNET_DB_QueryStatus qs; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("product_id", &product_id), @@ -103,6 +59,10 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, GNUNET_JSON_spec_json ("taxes", &pd.taxes), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("categories", + &categories), + NULL), GNUNET_JSON_spec_int64 ("total_stock", &total_stock), GNUNET_JSON_spec_mark_optional ( @@ -119,6 +79,13 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, NULL), GNUNET_JSON_spec_end () }; + size_t num_cats = 0; + uint64_t *cats = NULL; + bool conflict; + bool no_instance; + ssize_t no_cat; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; GNUNET_assert (NULL != mi); { @@ -138,13 +105,33 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (total_stock < -1) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "total_stock"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "total_stock"); + goto cleanup; } + num_cats = json_array_size (categories); + cats = GNUNET_new_array (num_cats, + uint64_t); + { + size_t idx; + json_t *val; + json_array_foreach (categories, idx, val) + { + if (! json_is_integer (val)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "categories"); + goto cleanup; + } + cats[idx] = json_integer_value (val); + } + } if (-1 == total_stock) pd.total_stock = INT64_MAX; @@ -162,31 +149,31 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (! TMH_taxes_array_valid (pd.taxes)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "taxes"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "taxes"); + goto cleanup; } if (! TMH_location_object_valid (pd.address)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + goto cleanup; } if (! TALER_JSON_check_i18n (pd.description_i18n)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + goto cleanup; } if (NULL == pd.image) @@ -194,110 +181,88 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, if (! TMH_image_data_url_valid (pd.image)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image"); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image"); + goto cleanup; } - /* finally, interact with DB until no serialization error */ - for (unsigned int i = 0; i<MAX_RETRIES; i++) + qs = TMH_db->insert_product (TMH_db->cls, + mi->settings.id, + product_id, + &pd, + num_cats, + cats, + &no_instance, + &conflict, + &no_cat); + switch (qs) { - /* Test if an product of this id is known */ - struct TALER_MERCHANTDB_ProductDetails epd; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "/post products")) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - product_id, - &epd); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - /* restart transaction */ - goto retry; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Good, we can proceed! */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* idempotency check: is epd == pd? */ - { - bool eq; - - eq = products_equal (&pd, - &epd); - TALER_MERCHANTDB_product_details_free (&epd); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0) - : TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, - product_id); - } - } /* end switch (qs) */ - - qs = TMH_db->insert_product (TMH_db->cls, - mi->settings.id, - product_id, - &pd); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - break; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } -retry: - GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - } /* for RETRIES loop */ - GNUNET_JSON_parse_free (spec); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + ret = TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, (GNUNET_DB_STATUS_SOFT_ERROR == qs) ? TALER_EC_GENERIC_DB_SOFT_FAILURE : TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); + 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_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, + product_id); + goto cleanup; + } + if (-1 != no_cat) + { + char nocats[24]; + + GNUNET_break_op (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_snprintf (nocats, + sizeof (nocats), + "%llu", + (unsigned long long) no_cat); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + nocats); + goto cleanup; + } + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +cleanup: + GNUNET_JSON_parse_free (spec); + GNUNET_free (cats); + return ret; } diff --git a/src/backenddb/merchant-0001.sql b/src/backenddb/merchant-0001.sql index 2adb9996..ca753a04 100644 --- a/src/backenddb/merchant-0001.sql +++ b/src/backenddb/merchant-0001.sql @@ -194,7 +194,7 @@ CREATE TABLE IF NOT EXISTS merchant_inventory ,description TEXT NOT NULL ,description_i18n BYTEA NOT NULL ,unit TEXT NOT NULL - ,image BYTEA NOT NULL + ,image BYTEA NOT NULL -- NOTE: merchant-0006 changes this to TEXT! ,taxes BYTEA NOT NULL ,price taler_amount_currency NOT NULL ,total_stock BIGINT NOT NULL diff --git a/src/backenddb/merchant-0006.sql b/src/backenddb/merchant-0006.sql index 2b24e526..00b9afff 100644 --- a/src/backenddb/merchant-0006.sql +++ b/src/backenddb/merchant-0006.sql @@ -22,6 +22,9 @@ SELECT _v.register_patch('merchant-0006', NULL, NULL); SET search_path TO merchant; +ALTER TABLE merchant_inventory + ALTER COLUMN image SET DATA TYPE TEXT; + CREATE TABLE IF NOT EXISTS merchant_categories (category_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY ,merchant_serial BIGINT NOT NULL diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c index 55db57e9..c46255c9 100644 --- a/src/backenddb/pg_insert_product.c +++ b/src/backenddb/pg_insert_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-2024 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 @@ -30,7 +30,12 @@ enum GNUNET_DB_QueryStatus TMH_PG_insert_product (void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd) + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + bool *conflict, + ssize_t *no_cat) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -42,36 +47,46 @@ TMH_PG_insert_product (void *cls, 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), + &pd->price), GNUNET_PQ_query_param_uint64 (&pd->total_stock), TALER_PQ_query_param_json (pd->address), 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_end }; + uint64_t ncat; + bool cats_found = true; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("conflict", + conflict), + GNUNET_PQ_result_spec_bool ("no_instance", + no_instance), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint64 ("no_cat", + &ncat), + &cats_found), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + *no_cat = -1; check_connection (pg); PREPARE (pg, "insert_product", - "INSERT INTO merchant_inventory" - "(merchant_serial" - ",product_id" - ",description" - ",description_i18n" - ",unit" - ",image" - ",taxes" - ",price" - ",total_stock" - ",address" - ",next_restock" - ",minimum_age" - ")" - " SELECT merchant_serial," - " $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12" - " FROM merchant_instances" - " WHERE merchant_id=$1"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_product", - params); + "SELECT" + " out_conflict AS conflict" + ",out_no_cat AS no_cat" + ",out_no_instance AS no_instance" + " FROM merchant_do_insert_product" + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "insert_product", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + *no_cat = (cats_found) ? -1 : (ssize_t) ncat; + return qs; } diff --git a/src/backenddb/pg_insert_product.h b/src/backenddb/pg_insert_product.h index 169bd150..5e85228e 100644 --- a/src/backenddb/pg_insert_product.h +++ b/src/backenddb/pg_insert_product.h @@ -32,12 +32,24 @@ * @param instance_id instance to insert product for * @param product_id product identifier of product to insert * @param pd the product details to insert + * @param num_cats length of @a cats array + * @param cats array of categories the product is in + * @param[out] no_instance set to true if @a instance_id is unknown + * @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 * @return database result code */ enum GNUNET_DB_QueryStatus TMH_PG_insert_product (void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd); + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + bool *conflict, + ssize_t *no_cat); + #endif diff --git a/src/backenddb/pg_insert_product.sql b/src/backenddb/pg_insert_product.sql new file mode 100644 index 00000000..4abf4469 --- /dev/null +++ b/src/backenddb/pg_insert_product.sql @@ -0,0 +1,175 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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/> +-- + + +CREATE OR REPLACE FUNCTION merchant_do_insert_product ( + IN in_instance_id TEXT, + IN in_product_id TEXT, + IN in_description TEXT, + IN in_description_i18n BYTEA, + IN in_unit TEXT, + IN in_image TEXT, + IN in_taxes BYTEA, + IN in_price taler_amount_currency, + IN in_total_stock INT8, + IN in_address BYTEA, + IN in_next_restock INT8, + IN in_minimum_age INT4, + IN ina_categories INT8[], + OUT out_no_instance BOOL, + OUT out_conflict BOOL, + OUT out_no_cat INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + my_merchant_id INT8; + my_product_serial INT8; + i INT8; + ini_cat INT8; +BEGIN + +-- Which instance are we using? +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_no_cat=NULL; + RETURN; +END IF; +out_no_instance=FALSE; + +INSERT INTO merchant_inventory + (merchant_serial + ,product_id + ,description + ,description_i18n + ,unit + ,image + ,taxes + ,price + ,total_stock + ,address + ,next_restock + ,minimum_age +) VALUES ( + my_merchant_id + ,in_product_id + ,in_description + ,in_description_i18n + ,in_unit + ,in_image + ,in_taxes + ,in_price + ,in_total_stock + ,in_address + ,in_next_restock + ,in_minimum_age) +ON CONFLICT (merchant_serial, product_id) DO NOTHING + RETURNING product_serial + INTO my_product_serial; + + +IF NOT FOUND +THEN + -- Check for idempotency + SELECT product_serial + INTO my_product_serial + FROM merchant_inventory + WHERE merchant_serial=my_merchant_id + AND product_id=in_product_id + AND description=in_description + AND description_i18n=in_description_i18n + AND unit=in_unit + AND image=in_image + AND taxes=in_taxes + AND price=in_price + AND total_stock=in_total_stock + AND address=in_address + AND next_restock=in_next_restock + AND minimum_age=in_minimum_age; + IF NOT FOUND + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + + -- Check categories match as well + FOR i IN 1..COALESCE(array_length(ina_categories,1),0) + LOOP + ini_cat=ina_categories[i]; + + PERFORM + FROM merchant_product_categories + WHERE product_serial=my_product_serial + AND category_serial=ini_cat; + IF NOT FOUND + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + END LOOP; + + -- Also check there are no additional categories + -- in either set. + SELECT COUNT(*) + INTO i + FROM merchant_product_categories + WHERE product_serial=my_product_serial; + IF i != array_length(ina_categories,1) + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + + -- Is idempotent! + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; +END IF; +out_conflict=FALSE; + + +-- Add categories +FOR i IN 1..COALESCE(array_length(ina_categories,1),0) +LOOP + ini_cat=ina_categories[i]; + + INSERT INTO merchant_product_categories + (product_serial + ,category_serial) + VALUES + (my_product_serial + ,ini_cat) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_no_cat=i; + RETURN; + END IF; +END LOOP; + +-- Success! +out_no_cat=NULL; +END $$; diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in index 3ebf8b8b..3588c2e9 100644 --- a/src/backenddb/procedures.sql.in +++ b/src/backenddb/procedures.sql.in @@ -19,6 +19,7 @@ BEGIN; SET search_path TO merchant; #include "pg_insert_deposit_to_transfer.sql" +#include "pg_insert_product.sql" #include "pg_insert_transfer_details.sql" COMMIT; diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c index fbb662f8..de8b2b9a 100644 --- a/src/backenddb/test_merchantdb.c +++ b/src/backenddb/test_merchantdb.c @@ -797,20 +797,48 @@ check_products_equal (const struct TALER_MERCHANTDB_ProductDetails *a, * * @param instance the instance to insert the product for. * @param product the product data to insert. + * @param num_cats length of the @a cats array + * @param cats array of categories for the product * @param expected_result the result we expect the db to return. + * @param expect_conflict expected conflict status + * @param expect_no_instance expected instance missing status + * @param expected_no_cat expected category missing index * @return 0 when successful, 1 otherwise. */ static int test_insert_product (const struct InstanceData *instance, const struct ProductData *product, - enum GNUNET_DB_QueryStatus expected_result) + unsigned int num_cats, + const uint64_t *cats, + enum GNUNET_DB_QueryStatus expected_result, + bool expect_conflict, + bool expect_no_instance, + ssize_t expected_no_cat) { + bool conflict; + bool no_instance; + ssize_t no_cat; + TEST_COND_RET_ON_FAIL (expected_result == plugin->insert_product (plugin->cls, instance->instance.id, product->id, - &product->product), + &product->product, + num_cats, + cats, + &no_instance, + &conflict, + &no_cat), "Insert product failed\n"); + if (expected_result > 0) + { + TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance, + "Conflict returned"); + TEST_COND_RET_ON_FAIL (conflict == expect_conflict, + "Conflict returned"); + TEST_COND_RET_ON_FAIL (no_cat == expected_no_cat, + "Wrong category missing returned"); + } return 0; } @@ -1076,18 +1104,46 @@ run_test_products (struct TestProducts_Closure *cls) /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + true, + -1)); /* Insert the instance */ TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); /* Test inserting a product */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); - /* Test that double insert fails */ + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); + /* Test that double insert succeeds */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[0], - GNUNET_DB_STATUS_SUCCESS_NO_RESULTS)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); + /* Test that conflicting insert fails */ + { + uint64_t cat = 42; + + TEST_RET_ON_FAIL (test_insert_product (&cls->instance, + &cls->products[0], + 1, + &cat, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + true, + false, + -1)); + } /* Test lookup of individual products */ TEST_RET_ON_FAIL (test_lookup_product (&cls->instance, &cls->products[0])); @@ -1156,7 +1212,12 @@ run_test_products (struct TestProducts_Closure *cls) /* Test collective product lookup */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->products[1], - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); TEST_RET_ON_FAIL (test_lookup_products (&cls->instance, 2, cls->products)); @@ -2059,7 +2120,12 @@ run_test_orders (struct TestOrders_Closure *cls) /* Test order lock */ TEST_RET_ON_FAIL (test_insert_product (&cls->instance, &cls->product, - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + 0, + NULL, + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT, + false, + false, + -1)); if (1 != plugin->insert_order_lock (plugin->cls, cls->instance.instance.id, cls->orders[0].id, diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h index 537a2485..68ab80fa 100644 --- a/src/include/taler_merchant_service.h +++ b/src/include/taler_merchant_service.h @@ -1707,6 +1707,54 @@ TALER_MERCHANT_products_post2 ( /** + * 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 price the price for one @a unit of the product, zero is used to imply that + * this product is not sold separately or that the price is not fixed and + * must be supplied by the front-end. If non-zero, price must include + * applicable taxes. + * @param image base64-encoded product image + * @param taxes list of taxes paid by the merchant + * @param total_stock in @a units, -1 to indicate "infinite" (i.e. electronic books) + * @param address where the product is in stock + * @param next_restock when the next restocking is expected to happen, 0 for unknown, + * #GNUNET_TIME_UNIT_FOREVER_ABS for 'never'. + * @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_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); + + +/** * Cancel POST /products operation. * * @param pph operation to cancel diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h index 7c009b89..12c00833 100644 --- a/src/include/taler_merchantdb_plugin.h +++ b/src/include/taler_merchantdb_plugin.h @@ -1758,13 +1758,24 @@ struct TALER_MERCHANTDB_Plugin * @param instance_id instance to insert product for * @param product_id product identifier of product to insert * @param pd the product details to insert + * @param num_cats length of @a cats array + * @param cats array of categories the product is in + * @param[out] no_instance set to true if @a instance_id is unknown + * @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 * @return database result code */ enum GNUNET_DB_QueryStatus (*insert_product)(void *cls, const char *instance_id, const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd); + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *no_instance, + bool *conflict, + ssize_t *no_cat); /** * Update details about a particular product. Note that the diff --git a/src/lib/merchant_api_post_products.c b/src/lib/merchant_api_post_products.c index 0f09f397..5d0ad27e 100644 --- a/src/lib/merchant_api_post_products.c +++ b/src/lib/merchant_api_post_products.c @@ -159,7 +159,7 @@ handle_post_products_finished (void *cls, struct TALER_MERCHANT_ProductsPostHandle * -TALER_MERCHANT_products_post2 ( +TALER_MERCHANT_products_post3 ( struct GNUNET_CURL_Context *ctx, const char *backend_url, const char *product_id, @@ -173,12 +173,28 @@ TALER_MERCHANT_products_post2 ( 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) { struct TALER_MERCHANT_ProductsPostHandle *pph; json_t *req_obj; + json_t *categories; + if (0 == num_cats) + { + categories = NULL; + } + else + { + categories = json_array (); + GNUNET_assert (NULL != categories); + for (unsigned int i = 0; i<num_cats; i++) + GNUNET_assert (0 == + json_array_append_new (categories, + json_integer (cats[i]))); + } req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("product_id", product_id), @@ -187,6 +203,9 @@ TALER_MERCHANT_products_post2 ( 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", @@ -243,6 +262,44 @@ TALER_MERCHANT_products_post2 ( struct TALER_MERCHANT_ProductsPostHandle * +TALER_MERCHANT_products_post2 ( + 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, + TALER_MERCHANT_ProductsPostCallback cb, + void *cb_cls) +{ + return TALER_MERCHANT_products_post3 (ctx, + backend_url, + product_id, + description, + description_i18n, + unit, + price, + image, + taxes, + total_stock, + address, + next_restock, + minimum_age, + 0, + NULL, + cb, + cb_cls); +} + + +struct TALER_MERCHANT_ProductsPostHandle * TALER_MERCHANT_products_post ( struct GNUNET_CURL_Context *ctx, const char *backend_url, |