aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Grothoff <christian@grothoff.org>2024-05-26 12:30:26 +0200
committerChristian Grothoff <christian@grothoff.org>2024-05-26 12:30:26 +0200
commitffdfeb863a03cc2cf9a92731f7b44b0c058d6a2d (patch)
treeb63a5da281434b4cfe29276caccc5e00b6a91afc
parent79c0c75009f9d292949382db02e3a2331d005bd8 (diff)
expand POST /products to allow specifying categories the product is in
-rw-r--r--src/backend/taler-merchant-httpd_private-post-products.c285
-rw-r--r--src/backenddb/merchant-0001.sql2
-rw-r--r--src/backenddb/merchant-0006.sql3
-rw-r--r--src/backenddb/pg_insert_product.c63
-rw-r--r--src/backenddb/pg_insert_product.h14
-rw-r--r--src/backenddb/pg_insert_product.sql175
-rw-r--r--src/backenddb/procedures.sql.in1
-rw-r--r--src/backenddb/test_merchantdb.c82
-rw-r--r--src/include/taler_merchant_service.h48
-rw-r--r--src/include/taler_merchantdb_plugin.h13
-rw-r--r--src/lib/merchant_api_post_products.c59
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,