aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Grothoff <christian@grothoff.org>2024-05-26 15:21:50 +0200
committerChristian Grothoff <christian@grothoff.org>2024-05-26 15:21:50 +0200
commitc4936ee74300fd2a8503530a3aa8c43cf04b455c (patch)
tree51831f4f21c597603cf13bae267e622250305e16
parentffdfeb863a03cc2cf9a92731f7b44b0c058d6a2d (diff)
-finish implementation of protocol v16, still undertested
-rw-r--r--src/backend/taler-merchant-httpd_config.c2
-rw-r--r--src/backend/taler-merchant-httpd_private-delete-products-ID.c40
-rw-r--r--src/backend/taler-merchant-httpd_private-get-pos.c15
-rw-r--r--src/backend/taler-merchant-httpd_private-get-products-ID.c19
-rw-r--r--src/backend/taler-merchant-httpd_private-patch-products-ID.c306
-rw-r--r--src/backend/taler-merchant-httpd_private-post-orders.c156
-rw-r--r--src/backend/taler-merchant-httpd_private-post-products-ID-lock.c72
-rw-r--r--src/backenddb/Makefile.am2
-rw-r--r--src/backenddb/pg_insert_product.c1
-rw-r--r--src/backenddb/pg_lookup_all_products.c32
-rw-r--r--src/backenddb/pg_lookup_product.c57
-rw-r--r--src/backenddb/pg_lookup_product.h7
-rw-r--r--src/backenddb/pg_update_product.c76
-rw-r--r--src/backenddb/pg_update_product.h26
-rw-r--r--src/backenddb/pg_update_product.sql139
-rw-r--r--src/backenddb/procedures.sql.in1
-rw-r--r--src/backenddb/test_merchantdb.c171
-rw-r--r--src/include/taler_merchantdb_plugin.h44
-rw-r--r--src/lib/Makefile.am2
-rw-r--r--src/lib/merchant_api_get_config.c4
20 files changed, 805 insertions, 367 deletions
diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c
index 97dd1714..10f0cd39 100644
--- a/src/backend/taler-merchant-httpd_config.c
+++ b/src/backend/taler-merchant-httpd_config.c
@@ -42,7 +42,7 @@
* #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in
* merchant_api_config.c!
*/
-#define MERCHANT_PROTOCOL_VERSION "15:0:11"
+#define MERCHANT_PROTOCOL_VERSION "16:0:12"
/**
diff --git a/src/backend/taler-merchant-httpd_private-delete-products-ID.c b/src/backend/taler-merchant-httpd_private-delete-products-ID.c
index 7d314785..23b6de3c 100644
--- a/src/backend/taler-merchant-httpd_private-delete-products-ID.c
+++ b/src/backend/taler-merchant-httpd_private-delete-products-ID.c
@@ -59,22 +59,30 @@ TMH_private_delete_products_ID (const struct TMH_RequestHandler *rh,
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
"delete_product (soft)");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- /* check if deletion must have failed because of locks by
- checking if the product exists */
- qs = TMH_db->lookup_product (TMH_db->cls,
- mi->settings.id,
- hc->infix,
- NULL);
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_STORE_FAILED,
- "lookup_product");
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
- hc->infix);
+ {
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
+
+ /* check if deletion must have failed because of locks by
+ checking if the product exists */
+ qs = TMH_db->lookup_product (TMH_db->cls,
+ mi->settings.id,
+ hc->infix,
+ NULL,
+ &num_categories,
+ &categories);
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "lookup_product");
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
+ hc->infix);
+ GNUNET_free (categories);
+ }
return TALER_MHD_reply_with_error (
connection,
MHD_HTTP_CONFLICT,
diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c
index 681b90c4..4c14b6ae 100644
--- a/src/backend/taler-merchant-httpd_private-get-pos.c
+++ b/src/backend/taler-merchant-httpd_private-get-pos.c
@@ -50,18 +50,21 @@ static void
add_product (void *cls,
uint64_t product_serial,
const char *product_id,
- const struct TALER_MERCHANTDB_ProductDetails *pd)
+ const struct TALER_MERCHANTDB_ProductDetails *pd,
+ size_t num_categories,
+ const uint64_t *categories)
{
struct Context *ctx = cls;
json_t *pa = ctx->pa;
json_t *cata;
- /* FIXME-8839: add proper category support! */
cata = json_array ();
- GNUNET_assert (
- 0 == json_array_append_new (
- cata,
- json_integer (0)));
+ GNUNET_assert (NULL != cata);
+ for (size_t i = 0; i<num_categories; i++)
+ GNUNET_assert (
+ 0 == json_array_append_new (
+ cata,
+ json_integer (categories[i])));
GNUNET_assert (
0 ==
json_array_append_new (
diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c
index 107081f7..0729b1df 100644
--- a/src/backend/taler-merchant-httpd_private-get-products-ID.c
+++ b/src/backend/taler-merchant-httpd_private-get-products-ID.c
@@ -40,12 +40,17 @@ TMH_private_get_products_ID (
struct TMH_MerchantInstance *mi = hc->instance;
struct TALER_MERCHANTDB_ProductDetails pd = { 0 };
enum GNUNET_DB_QueryStatus qs;
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
+ json_t *jcategories;
GNUNET_assert (NULL != mi);
qs = TMH_db->lookup_product (TMH_db->cls,
mi->settings.id,
hc->infix,
- &pd);
+ &pd,
+ &num_categories,
+ &categories);
if (0 > qs)
{
GNUNET_break (0);
@@ -61,6 +66,16 @@ TMH_private_get_products_ID (
TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
hc->infix);
}
+ jcategories = json_array ();
+ GNUNET_assert (NULL != jcategories);
+ for (size_t i = 0; i<num_categories; i++)
+ {
+ GNUNET_assert (0 ==
+ json_array_append_new (jcategories,
+ json_integer (categories[i])));
+ }
+ GNUNET_free (categories);
+
{
MHD_RESULT ret;
@@ -73,6 +88,8 @@ TMH_private_get_products_ID (
pd.description_i18n),
GNUNET_JSON_pack_string ("unit",
pd.unit),
+ GNUNET_JSON_pack_array_steal ("categories",
+ jcategories),
TALER_JSON_pack_amount ("price",
&pd.price),
GNUNET_JSON_pack_allow_null (
diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c
index 7bc327cd..6e50cced 100644
--- a/src/backend/taler-merchant-httpd_private-patch-products-ID.c
+++ b/src/backend/taler-merchant-httpd_private-patch-products-ID.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
@@ -29,76 +29,6 @@
/**
- * How often do we retry the simple INSERT database transaction?
- */
-#define MAX_RETRIES 3
-
-
-/**
- * Determine the cause of the PATCH failure in more detail and report.
- *
- * @param connection connection to report on
- * @param instance_id instance we are processing
- * @param product_id ID of the product to patch
- * @param pd product details we failed to set
- */
-static MHD_RESULT
-determine_cause (struct MHD_Connection *connection,
- const char *instance_id,
- const char *product_id,
- const struct TALER_MERCHANTDB_ProductDetails *pd)
-{
- struct TALER_MERCHANTDB_ProductDetails pdx;
- enum GNUNET_DB_QueryStatus qs;
-
- qs = TMH_db->lookup_product (TMH_db->cls,
- instance_id,
- product_id,
- &pdx);
- switch (qs)
- {
- case GNUNET_DB_STATUS_HARD_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_FETCH_FAILED,
- NULL);
- case GNUNET_DB_STATUS_SOFT_ERROR:
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected serialization problem");
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
- product_id);
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- break; /* do below */
- }
-
- {
- enum TALER_ErrorCode ec;
-
- ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
- if (pdx.total_lost > pd->total_lost)
- ec = TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED;
- if (pdx.total_sold > pd->total_sold)
- ec = TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED;
- if (pdx.total_stock > pd->total_stock)
- ec = TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED;
- TALER_MERCHANTDB_product_details_free (&pdx);
- GNUNET_break (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE != ec);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_CONFLICT,
- ec,
- NULL);
- }
-}
-
-
-/**
* PATCH configuration of an existing instance, given its configuration.
*
* @param rh context of the handler
@@ -107,13 +37,15 @@ determine_cause (struct MHD_Connection *connection,
* @return MHD result code
*/
MHD_RESULT
-TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh,
- struct MHD_Connection *connection,
- struct TMH_HandlerContext *hc)
+TMH_private_patch_products_ID (
+ const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
{
struct TMH_MerchantInstance *mi = hc->instance;
const char *product_id = hc->infix;
struct TALER_MERCHANTDB_ProductDetails pd = {0};
+ const json_t *categories = NULL;
int64_t total_stock;
enum GNUNET_DB_QueryStatus qs;
struct GNUNET_JSON_Specification spec[] = {
@@ -135,6 +67,10 @@ TMH_private_patch_products_ID (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 (
@@ -155,6 +91,15 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh,
NULL),
GNUNET_JSON_spec_end ()
};
+ MHD_RESULT ret;
+ size_t num_cats = 0;
+ uint64_t *cats = NULL;
+ bool no_instance;
+ ssize_t no_cat;
+ bool no_product;
+ bool lost_reduced;
+ bool sold_reduced;
+ bool stock_reduced;
pd.total_sold = 0; /* will be ignored anyway */
GNUNET_assert (NULL != mi);
@@ -173,11 +118,11 @@ TMH_private_patch_products_ID (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;
}
if (-1 == total_stock)
pd.total_stock = INT64_MAX;
@@ -189,23 +134,45 @@ TMH_private_patch_products_ID (const struct TMH_RequestHandler *rh,
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;
+ }
+ 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 (NULL == pd.description_i18n)
pd.description_i18n = json_object ();
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.taxes)
@@ -214,74 +181,143 @@ TMH_private_patch_products_ID (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 (NULL == pd.image)
pd.image = "";
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;
}
+
if ( (pd.total_stock < pd.total_sold + pd.total_lost) ||
(pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */)
{
GNUNET_break_op (0);
- GNUNET_JSON_parse_free (spec);
- return TALER_MHD_reply_with_error (
+ ret = TALER_MHD_reply_with_error (
connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS,
NULL);
+ goto cleanup;
}
+
qs = TMH_db->update_product (TMH_db->cls,
mi->settings.id,
product_id,
- &pd);
+ &pd,
+ num_cats,
+ cats,
+ &no_instance,
+ &no_cat,
+ &no_product,
+ &lost_reduced,
+ &sold_reduced,
+ &stock_reduced);
+ switch (qs)
{
- MHD_RESULT ret = MHD_NO;
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ NULL);
+ goto cleanup;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+ "unexpected serialization problem");
+ goto cleanup;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+ "unexpected problem in stored procedure");
+ goto cleanup;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
- switch (qs)
- {
- case GNUNET_DB_STATUS_HARD_ERROR:
- GNUNET_break (0);
- ret = TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_STORE_FAILED,
- NULL);
- break;
- case GNUNET_DB_STATUS_SOFT_ERROR:
- GNUNET_break (0);
- ret = TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "unexpected serialization problem");
- break;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- ret = determine_cause (connection,
- mi->settings.id,
- product_id,
- &pd);
- break;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- ret = TALER_MHD_reply_static (connection,
- MHD_HTTP_NO_CONTENT,
- NULL,
- NULL,
- 0);
- break;
- }
- GNUNET_JSON_parse_free (spec);
- return ret;
+ 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 (-1 != no_cat)
+ {
+ char cats[24];
+
+ GNUNET_snprintf (cats,
+ sizeof (cats),
+ "%llu",
+ (unsigned long long) no_cat);
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN,
+ cats);
+ goto cleanup;
+ }
+ if (no_product)
+ {
+ ret = TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
+ product_id);
+ goto cleanup;
+ }
+ if (lost_reduced)
+ {
+ ret = TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED,
+ NULL);
+ goto cleanup;
+ }
+ if (sold_reduced)
+ {
+ ret = TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED,
+ NULL);
+ goto cleanup;
+ }
+ if (stock_reduced)
+ {
+ ret = TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED,
+ NULL);
+ goto cleanup;
}
+ /* success! */
+ ret = TALER_MHD_reply_static (connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+cleanup:
+ GNUNET_free (cats);
+ GNUNET_JSON_parse_free (spec);
+ return ret;
}
diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c
index eedece55..8bdae3f1 100644
--- a/src/backend/taler-merchant-httpd_private-post-orders.c
+++ b/src/backend/taler-merchant-httpd_private-post-orders.c
@@ -940,6 +940,8 @@ execute_order (struct OrderContext *oc)
struct TALER_MERCHANTDB_ProductDetails pd;
MHD_RESULT ret;
const struct InventoryProduct *ip;
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
ip = &oc->parse_request.inventory_products[
oc->execute_order.out_of_stock_index];
@@ -950,10 +952,13 @@ execute_order (struct OrderContext *oc)
TMH_db->cls,
oc->hc->instance->settings.id,
ip->product_id,
- &pd);
+ &pd,
+ &num_categories,
+ &categories);
switch (qs)
{
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ GNUNET_free (categories);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order creation failed: product out of stock\n");
ret = TALER_MHD_REPLY_JSON_PACK (
@@ -1317,6 +1322,7 @@ get_exchange_keys (void *cls,
rx);
}
+
/**
* Fetch details about the token family with the given @a slug
* and add them to the list of token authorities. Check if the
@@ -1340,12 +1346,13 @@ set_token_authority (struct OrderContext *oc,
// TODO: make this configurable. This is the granularity of token
// expiration dates.
GNUNET_TIME_UNIT_DAYS
- );
+ );
qs = TMH_db->lookup_token_family_key (TMH_db->cls,
oc->hc->instance->settings.id,
slug,
- GNUNET_TIME_absolute_to_timestamp (min_start_date),
+ GNUNET_TIME_absolute_to_timestamp (
+ min_start_date),
start_date,
&key_details);
@@ -1390,9 +1397,10 @@ set_token_authority (struct OrderContext *oc,
struct GNUNET_CRYPTO_BlindSignPrivateKey *priv;
struct GNUNET_CRYPTO_BlindSignPublicKey *pub;
struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
- struct GNUNET_TIME_Timestamp valid_before = GNUNET_TIME_absolute_to_timestamp(
- GNUNET_TIME_absolute_add (now,
- key_details.token_family.duration));
+ struct GNUNET_TIME_Timestamp valid_before =
+ GNUNET_TIME_absolute_to_timestamp (
+ GNUNET_TIME_absolute_add (now,
+ key_details.token_family.duration));
GNUNET_CRYPTO_blind_sign_keys_create (&priv,
&pub,
@@ -1411,13 +1419,16 @@ set_token_authority (struct OrderContext *oc,
slug,
&token_pub,
&token_priv,
- GNUNET_TIME_absolute_to_timestamp (now),
+ GNUNET_TIME_absolute_to_timestamp (now
+ ),
valid_before);
authority.token_expiration = valid_before;
authority.pub = &token_pub;
// GNUNET_CRYPTO_blind_sign_priv_decref (&token_priv.private_key);
- } else {
+ }
+ else
+ {
authority.token_expiration = key_details.valid_before;
authority.pub = key_details.pub;
}
@@ -1428,11 +1439,13 @@ set_token_authority (struct OrderContext *oc,
GNUNET_free (key_details.token_family.slug);
GNUNET_free (key_details.token_family.name);
- if (NULL != key_details.priv) {
+ if (NULL != key_details.priv)
+ {
GNUNET_CRYPTO_blind_sign_priv_decref (&key_details.priv->private_key);
}
- switch (key_details.token_family.kind) {
+ switch (key_details.token_family.kind)
+ {
case TALER_MERCHANTDB_TFK_Subscription:
authority.kind = TALER_MCTK_SUBSCRIPTION;
authority.details.subscription.start_date = key_details.valid_after;
@@ -1454,6 +1467,7 @@ set_token_authority (struct OrderContext *oc,
return MHD_YES;
}
+
/**
* Serialize order into @a oc->serialize_order.contract,
* ready to be stored in the database. Upon success, continue
@@ -1472,16 +1486,16 @@ serialize_order (struct OrderContext *oc)
merchant = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("name",
- settings->name),
+ settings->name),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("website",
- settings->website)),
+ settings->website)),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("email",
- settings->email)),
+ settings->email)),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("logo",
- settings->logo)));
+ settings->logo)));
GNUNET_assert (NULL != merchant);
{
json_t *loca;
@@ -1493,7 +1507,7 @@ serialize_order (struct OrderContext *oc)
loca = json_deep_copy (loca);
GNUNET_assert (NULL != loca);
GNUNET_assert (0 ==
- json_object_set_new (merchant,
+ json_object_set_new (merchant,
"address",
loca));
}
@@ -1508,7 +1522,7 @@ serialize_order (struct OrderContext *oc)
juri = json_deep_copy (juri);
GNUNET_assert (NULL != juri);
GNUNET_assert (0 ==
- json_object_set_new (merchant,
+ json_object_set_new (merchant,
"jurisdiction",
juri));
}
@@ -1516,7 +1530,8 @@ serialize_order (struct OrderContext *oc)
for (unsigned int i = 0; i<oc->parse_choices.authorities_len; i++)
{
- struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices.authorities[i];
+ struct TALER_MerchantContractTokenAuthority *authority = &oc->parse_choices.
+ authorities[i];
// TODO: Finish spec to clearly define how token families are stored in
// ContractTerms.
@@ -1529,7 +1544,7 @@ serialize_order (struct OrderContext *oc)
// so it's clear with key is referenced.
json_t *jauthority = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("description",
- authority->description),
+ authority->description),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("description_i18n",
authority->description_i18n)),
@@ -1539,10 +1554,10 @@ serialize_order (struct OrderContext *oc)
&authority->pub->public_key.pub_key_hash),
GNUNET_JSON_pack_timestamp ("token_expiration",
authority->token_expiration)
- );
+ );
GNUNET_assert (0 ==
- json_object_set_new (token_types,
+ json_object_set_new (token_types,
authority->label,
jauthority));
}
@@ -1561,20 +1576,21 @@ serialize_order (struct OrderContext *oc)
json_t *jinput = GNUNET_JSON_PACK (
GNUNET_JSON_pack_int64 ("type",
input->type)
- );
+ );
if (TALER_MCIT_TOKEN == input->type)
{
- GNUNET_assert(0 ==
- json_object_set_new(jinput,
- "number",
- json_integer (
- input->details.token.count)));
- GNUNET_assert(0 ==
- json_object_set_new(jinput,
- "token_family_slug",
- json_string (
- input->details.token.token_family_slug)));
+ GNUNET_assert (0 ==
+ json_object_set_new (jinput,
+ "number",
+ json_integer (
+ input->details.token.count)));
+ GNUNET_assert (0 ==
+ json_object_set_new (jinput,
+ "token_family_slug",
+ json_string (
+ input->details.token.
+ token_family_slug)));
}
GNUNET_assert (0 == json_array_append_new (inputs, jinput));
@@ -1587,21 +1603,22 @@ serialize_order (struct OrderContext *oc)
json_t *joutput = GNUNET_JSON_PACK (
GNUNET_JSON_pack_int64 ("type",
output->type)
- );
+ );
if (TALER_MCOT_TOKEN == output->type)
{
- GNUNET_assert(0 ==
- json_object_set_new(joutput,
- "number",
- json_integer (
- output->details.token.count)));
-
- GNUNET_assert(0 ==
- json_object_set_new(joutput,
- "token_family_slug",
- json_string (
- output->details.token.token_family_slug)));
+ GNUNET_assert (0 ==
+ json_object_set_new (joutput,
+ "number",
+ json_integer (
+ output->details.token.count)));
+
+ GNUNET_assert (0 ==
+ json_object_set_new (joutput,
+ "token_family_slug",
+ json_string (
+ output->details.token.
+ token_family_slug)));
}
GNUNET_assert (0 == json_array_append (outputs, joutput));
@@ -1609,10 +1626,10 @@ serialize_order (struct OrderContext *oc)
json_t *jchoice = GNUNET_JSON_PACK (
GNUNET_JSON_pack_array_incref ("inputs",
- inputs),
+ inputs),
GNUNET_JSON_pack_array_incref ("outputs",
- outputs)
- );
+ outputs)
+ );
GNUNET_assert (0 == json_array_append (choices, jchoice));
}
@@ -1633,7 +1650,8 @@ serialize_order (struct OrderContext *oc)
oc->parse_order.fulfillment_message)),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("fulfillment_message_i18n",
- oc->parse_order.fulfillment_message_i18n)),
+ oc->parse_order.fulfillment_message_i18n))
+ ,
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("fulfillment_url",
oc->parse_order.fulfillment_url)),
@@ -1674,12 +1692,12 @@ serialize_order (struct OrderContext *oc)
&oc->parse_order.brutto),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_array_incref ("choices",
- choices)
- ),
+ choices)
+ ),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("token_types",
token_types)
- ),
+ ),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("extra",
(json_t *) oc->parse_order.extra))
@@ -1715,6 +1733,7 @@ serialize_order (struct OrderContext *oc)
oc->phase++;
}
+
/**
* Set max_fee in @a oc based on STEFAN value if
* not yet present. Upon success, continue
@@ -1750,6 +1769,7 @@ set_max_fee (struct OrderContext *oc)
oc->phase++;
}
+
/**
* Set list of acceptable exchanges in @a oc. Upon success, continue
* processing with set_max_fee().
@@ -1871,7 +1891,7 @@ parse_order (struct OrderContext *oc)
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_array_const ("choices",
- &oc->parse_order.choices),
+ &oc->parse_order.choices),
NULL),
GNUNET_JSON_spec_mark_optional (
TALER_JSON_spec_web_url ("merchant_base_url",
@@ -1937,7 +1957,7 @@ parse_order (struct OrderContext *oc)
ret);
return;
}
- if (NULL == version || 0 == strcmp("0", version))
+ if (NULL == version || 0 == strcmp ("0", version))
{
oc->parse_order.version = TALER_MCV_V0;
@@ -1952,11 +1972,11 @@ parse_order (struct OrderContext *oc)
return;
}
}
- else if (0 == strcmp("1", version))
+ else if (0 == strcmp ("1", version))
{
oc->parse_order.version = TALER_MCV_V1;
- if (! json_is_array(oc->parse_order.choices))
+ if (! json_is_array (oc->parse_order.choices))
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
@@ -1989,9 +2009,9 @@ parse_order (struct OrderContext *oc)
return;
}
if ( (! no_fee) &&
- (GNUNET_OK !=
- TALER_amount_cmp_currency (&oc->parse_order.brutto,
- &oc->parse_order.max_fee)) )
+ (GNUNET_OK !=
+ TALER_amount_cmp_currency (&oc->parse_order.brutto,
+ &oc->parse_order.max_fee)) )
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
@@ -2268,6 +2288,7 @@ parse_order (struct OrderContext *oc)
oc->phase++;
}
+
/**
* Parse contract choices. Upon success, continue
* processing with merge_inventory().
@@ -2355,7 +2376,7 @@ parse_choices (struct OrderContext *oc)
GNUNET_JSON_spec_uint32 ("count",
&input.details.token.count),
NULL),
- GNUNET_JSON_spec_end()
+ GNUNET_JSON_spec_end ()
};
if (GNUNET_OK !=
@@ -2431,7 +2452,8 @@ parse_choices (struct OrderContext *oc)
size_t idx;
json_array_foreach ((json_t *) joutputs, idx, joutput)
{
- struct TALER_MerchantContractOutput output = { .details.token.count = 1 };
+ struct TALER_MerchantContractOutput output = { .details.token.count = 1}
+ ;
const char *kind;
const char *ierror_name;
unsigned int ierror_line;
@@ -2447,14 +2469,14 @@ parse_choices (struct OrderContext *oc)
GNUNET_JSON_spec_uint32 ("count",
&output.details.token.count),
NULL),
- GNUNET_JSON_spec_end()
+ GNUNET_JSON_spec_end ()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (joutput,
- ispec,
- &ierror_name,
- &ierror_line))
+ ispec,
+ &ierror_name,
+ &ierror_line))
{
GNUNET_JSON_parse_free (spec);
GNUNET_JSON_parse_free (ispec);
@@ -2526,6 +2548,7 @@ parse_choices (struct OrderContext *oc)
oc->phase++;
}
+
/**
* Process the @a payment_target and add the details of how the
* order could be paid to @a order. On success, continue
@@ -2591,11 +2614,15 @@ merge_inventory (struct OrderContext *oc)
= &oc->parse_request.inventory_products[i];
struct TALER_MERCHANTDB_ProductDetails pd;
enum GNUNET_DB_QueryStatus qs;
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
qs = TMH_db->lookup_product (TMH_db->cls,
oc->hc->instance->settings.id,
ip->product_id,
- &pd);
+ &pd,
+ &num_categories,
+ &categories);
if (qs <= 0)
{
enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
@@ -2631,6 +2658,7 @@ merge_inventory (struct OrderContext *oc)
ip->product_id);
return;
}
+ GNUNET_free (categories);
oc->parse_order.minimum_age
= GNUNET_MAX (oc->parse_order.minimum_age,
pd.minimum_age);
diff --git a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c
index 184f1d28..844b2ec8 100644
--- a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c
+++ b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c
@@ -29,9 +29,10 @@
MHD_RESULT
-TMH_private_post_products_ID_lock (const struct TMH_RequestHandler *rh,
- struct MHD_Connection *connection,
- struct TMH_HandlerContext *hc)
+TMH_private_post_products_ID_lock (
+ const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
{
struct TMH_MerchantInstance *mi = hc->instance;
const char *product_id = hc->infix;
@@ -75,35 +76,48 @@ TMH_private_post_products_ID_lock (const struct TMH_RequestHandler *rh,
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_STORE_FAILED,
- NULL);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ NULL);
case GNUNET_DB_STATUS_SOFT_ERROR:
GNUNET_break (0);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
- "Serialization error for single-statment request");
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+ "Serialization error for single-statment request");
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- qs = TMH_db->lookup_product (TMH_db->cls,
- mi->settings.id,
- product_id,
- NULL);
- if (GNUNET_DB_STATUS_HARD_ERROR == qs)
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_DB_STORE_FAILED,
- "lookup_product");
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
- product_id);
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_GONE,
- TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS,
- product_id);
+ {
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
+
+ qs = TMH_db->lookup_product (TMH_db->cls,
+ mi->settings.id,
+ product_id,
+ NULL,
+ &num_categories,
+ &categories);
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "lookup_product");
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN,
+ product_id);
+ GNUNET_free (categories);
+ }
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_GONE,
+ TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS,
+ product_id);
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
return TALER_MHD_reply_static (connection,
MHD_HTTP_NO_CONTENT,
diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am
index 99ef1e77..4cacd3b4 100644
--- a/src/backenddb/Makefile.am
+++ b/src/backenddb/Makefile.am
@@ -63,7 +63,7 @@ libtalermerchantdb_la_LIBADD = \
libtalermerchantdb_la_LDFLAGS = \
$(POSTGRESQL_LDFLAGS) \
- -version-info 3:1:1 \
+ -version-info 4:0:2 \
-no-undefined
libtaler_plugin_merchantdb_postgres_la_SOURCES = \
diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c
index c46255c9..0d0a8380 100644
--- a/src/backenddb/pg_insert_product.c
+++ b/src/backenddb/pg_insert_product.c
@@ -72,7 +72,6 @@ TMH_PG_insert_product (void *cls,
};
enum GNUNET_DB_QueryStatus qs;
- *no_cat = -1;
check_connection (pg);
PREPARE (pg,
"insert_product",
diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c
index 29751a45..31320d59 100644
--- a/src/backenddb/pg_lookup_all_products.c
+++ b/src/backenddb/pg_lookup_all_products.c
@@ -41,6 +41,11 @@ struct LookupProductsContext
void *cb_cls;
/**
+ * Postgres context.
+ */
+ struct PostgresClosure *pg;
+
+ /**
* Did database result extraction fail?
*/
bool extract_failed;
@@ -61,12 +66,15 @@ lookup_products_cb (void *cls,
unsigned int num_results)
{
struct LookupProductsContext *plc = cls;
+ struct PostgresClosure *pg = plc->pg;
for (unsigned int i = 0; i < num_results; i++)
{
char *product_id;
uint64_t product_serial;
struct TALER_MERCHANTDB_ProductDetails pd;
+ size_t num_categories;
+ uint64_t *categories;
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_string ("product_id",
&product_id),
@@ -79,7 +87,7 @@ lookup_products_cb (void *cls,
GNUNET_PQ_result_spec_string ("unit",
&pd.unit),
TALER_PQ_result_spec_amount_with_currency ("price",
- &pd.price),
+ &pd.price),
TALER_PQ_result_spec_json ("taxes",
&pd.taxes),
GNUNET_PQ_result_spec_uint64 ("total_stock",
@@ -96,6 +104,10 @@ lookup_products_cb (void *cls,
&pd.next_restock),
GNUNET_PQ_result_spec_uint32 ("minimum_age",
&pd.minimum_age),
+ GNUNET_PQ_result_spec_array_uint64 (pg->conn,
+ "categories",
+ &num_categories,
+ &categories),
GNUNET_PQ_result_spec_end
};
@@ -111,7 +123,9 @@ lookup_products_cb (void *cls,
plc->cb (plc->cb_cls,
product_serial,
product_id,
- &pd);
+ &pd,
+ num_categories,
+ categories);
GNUNET_PQ_cleanup_result (rs);
}
}
@@ -119,14 +133,15 @@ lookup_products_cb (void *cls,
enum GNUNET_DB_QueryStatus
TMH_PG_lookup_all_products (void *cls,
- const char *instance_id,
- TALER_MERCHANTDB_ProductCallback cb,
- void *cb_cls)
+ const char *instance_id,
+ TALER_MERCHANTDB_ProductCallback cb,
+ void *cb_cls)
{
struct PostgresClosure *pg = cls;
struct LookupProductsContext plc = {
.cb = cb,
.cb_cls = cb_cls,
+ .pg = pg,
/* Can be overwritten by the lookup_products_cb */
.extract_failed = false,
};
@@ -157,6 +172,13 @@ TMH_PG_lookup_all_products (void *cls,
" FROM merchant_inventory"
" JOIN merchant_instances"
" USING (merchant_serial)"
+ ",LATERAL ("
+ " SELECT ARRAY ("
+ " SELECT mpc.category_serial"
+ " FROM merchant_product_categories mpc"
+ " WHERE mpc.product_serial = mi.product_serial"
+ " ) AS category_array"
+ " ) t"
" WHERE merchant_instances.merchant_id=$1");
qs = GNUNET_PQ_eval_prepared_multi_select (
pg->conn,
diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c
index a078cf8e..2948e6ca 100644
--- a/src/backenddb/pg_lookup_product.c
+++ b/src/backenddb/pg_lookup_product.c
@@ -29,7 +29,9 @@ enum GNUNET_DB_QueryStatus
TMH_PG_lookup_product (void *cls,
const char *instance_id,
const char *product_id,
- struct TALER_MERCHANTDB_ProductDetails *pd)
+ struct TALER_MERCHANTDB_ProductDetails *pd,
+ size_t *num_categories,
+ uint64_t **categories)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
@@ -38,6 +40,35 @@ TMH_PG_lookup_product (void *cls,
GNUNET_PQ_query_param_end
};
+ PREPARE (pg,
+ "lookup_product",
+ "SELECT"
+ " mi.description"
+ ",mi.description_i18n"
+ ",mi.unit"
+ ",mi.price"
+ ",mi.taxes"
+ ",mi.total_stock"
+ ",mi.total_sold"
+ ",mi.total_lost"
+ ",mi.image"
+ ",mi.address"
+ ",mi.next_restock"
+ ",mi.minimum_age"
+ ",t.category_array AS categories"
+ " FROM merchant_inventory mi"
+ " JOIN merchant_instances inst"
+ " USING (merchant_serial)"
+ ",LATERAL ("
+ " SELECT ARRAY ("
+ " SELECT mpc.category_serial"
+ " FROM merchant_product_categories mpc"
+ " WHERE mpc.product_serial = mi.product_serial"
+ " ) AS category_array"
+ " ) t"
+ " WHERE inst.merchant_id=$1"
+ " AND mi.product_id=$2"
+ );
if (NULL == pd)
{
struct GNUNET_PQ_ResultSpec rs_null[] = {
@@ -77,30 +108,14 @@ TMH_PG_lookup_product (void *cls,
&pd->next_restock),
GNUNET_PQ_result_spec_uint32 ("minimum_age",
&pd->minimum_age),
+ GNUNET_PQ_result_spec_array_uint64 (pg->conn,
+ "categories",
+ num_categories,
+ categories),
GNUNET_PQ_result_spec_end
};
check_connection (pg);
- PREPARE (pg,
- "lookup_product",
- "SELECT"
- " description"
- ",description_i18n"
- ",unit"
- ",price"
- ",taxes"
- ",total_stock"
- ",total_sold"
- ",total_lost"
- ",image"
- ",merchant_inventory.address"
- ",next_restock"
- ",minimum_age"
- " FROM merchant_inventory"
- " JOIN merchant_instances"
- " USING (merchant_serial)"
- " WHERE merchant_instances.merchant_id=$1"
- " AND merchant_inventory.product_id=$2");
return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"lookup_product",
params,
diff --git a/src/backenddb/pg_lookup_product.h b/src/backenddb/pg_lookup_product.h
index a6add4cb..7346cc0d 100644
--- a/src/backenddb/pg_lookup_product.h
+++ b/src/backenddb/pg_lookup_product.h
@@ -33,12 +33,17 @@
* @param product_id product to lookup
* @param[out] pd set to the product details on success, can be NULL
* (in that case we only want to check if the product exists)
+ * @param[out] num_categories set to length of @a categories array
+ * @param[out] categories set to array of categories the
+ * product is in, caller must free() it.
* @return database result code
*/
enum GNUNET_DB_QueryStatus
TMH_PG_lookup_product (void *cls,
const char *instance_id,
const char *product_id,
- struct TALER_MERCHANTDB_ProductDetails *pd);
+ struct TALER_MERCHANTDB_ProductDetails *pd,
+ size_t *num_categories,
+ uint64_t **categories);
#endif
diff --git a/src/backenddb/pg_update_product.c b/src/backenddb/pg_update_product.c
index cd7c1857..d7b63b82 100644
--- a/src/backenddb/pg_update_product.c
+++ b/src/backenddb/pg_update_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
@@ -26,11 +26,20 @@
#include "pg_update_product.h"
#include "pg_helper.h"
+
enum GNUNET_DB_QueryStatus
TMH_PG_update_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,
+ ssize_t *no_cat,
+ bool *no_product,
+ bool *lost_reduced,
+ bool *sold_reduced,
+ bool *stocked_reduced)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
@@ -42,14 +51,37 @@ TMH_PG_update_product (void *cls,
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 */
+ &pd->price), /* $8 */
GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */
GNUNET_PQ_query_param_uint64 (&pd->total_lost),
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 ("no_instance",
+ no_instance),
+ GNUNET_PQ_result_spec_bool ("no_product",
+ no_product),
+ GNUNET_PQ_result_spec_bool ("lost_reduced",
+ lost_reduced),
+ GNUNET_PQ_result_spec_bool ("sold_reduced",
+ sold_reduced),
+ GNUNET_PQ_result_spec_bool ("stocked_reduced",
+ stocked_reduced),
+ 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;
if ( (pd->total_stock < pd->total_lost + pd->total_sold) ||
(pd->total_lost < pd->total_lost
@@ -61,26 +93,20 @@ TMH_PG_update_product (void *cls,
check_connection (pg);
PREPARE (pg,
"update_product",
- "UPDATE merchant_inventory SET"
- " description=$3"
- ",description_i18n=$4"
- ",unit=$5"
- ",image=$6"
- ",taxes=$7"
- ",price=$8"
- ",total_stock=$9"
- ",total_lost=$10"
- ",address=$11"
- ",next_restock=$12"
- ",minimum_age=$13"
- " WHERE merchant_serial="
- " (SELECT merchant_serial"
- " FROM merchant_instances"
- " WHERE merchant_id=$1)"
- " AND product_id=$2"
- " AND total_stock <= $9"
- " AND total_lost <= $10");
- return GNUNET_PQ_eval_prepared_non_select (pg->conn,
- "update_product",
- params);
+ "SELECT"
+ " out_lost_reduced AS lost_reduced"
+ ",out_sold_reduced AS sold_reduced"
+ ",out_stocked_reduced AS stocked_reduced"
+ ",out_no_product AS no_product"
+ ",out_no_cat AS no_cat"
+ ",out_no_instance AS no_instance"
+ " FROM merchant_do_update_product"
+ "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);");
+ qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "update_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_update_product.h b/src/backenddb/pg_update_product.h
index 3ad280ef..ffba1073 100644
--- a/src/backenddb/pg_update_product.h
+++ b/src/backenddb/pg_update_product.h
@@ -28,26 +28,40 @@
/**
* Update details about a particular product. Note that the
* transaction must enforce that the sold/stocked/lost counters
- * are not reduced (i.e. by expanding the WHERE clause on the existing
- * values).
+ * are not reduced.
*
* @param cls closure
* @param instance_id instance to lookup products for
* @param product_id product to lookup
+ * @param num_cats length of @a cats array
+ * @param cats number of categories the product is in
+ * @param[out] no_instance the update failed as the instance is unknown
+ * @param[out] no_cat set to -1 on success, otherwise the update failed and this is set
+ * to the index of a category in @a cats that is unknown
+ * @param[out] no_product the @a product_id is unknown
+ * @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] pd set to the product details on success, can be NULL
* (in that case we only want to check if the product exists)
* total_sold in @a pd is ignored, total_lost must not
* exceed total_stock minus the existing total_sold;
* total_sold and total_stock must be larger or equal to
* the existing value;
- * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the
- * non-decreasing constraints are not met *or* if the product
- * does not yet exist.
+ * @return database result code
*/
enum GNUNET_DB_QueryStatus
TMH_PG_update_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,
+ ssize_t *no_cat,
+ bool *no_product,
+ bool *lost_reduced,
+ bool *sold_reduced,
+ bool *stocked_reduced);
#endif
diff --git a/src/backenddb/pg_update_product.sql b/src/backenddb/pg_update_product.sql
new file mode 100644
index 00000000..6b5a416b
--- /dev/null
+++ b/src/backenddb/pg_update_product.sql
@@ -0,0 +1,139 @@
+--
+-- 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_update_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_total_lost 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_no_product BOOL,
+ OUT out_lost_reduced BOOL,
+ OUT out_sold_reduced BOOL,
+ OUT out_stocked_reduced BOOL,
+ OUT out_no_cat INT8)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_merchant_id INT8;
+ my_product_serial INT8;
+ i INT8;
+ ini_cat INT8;
+ rec RECORD;
+BEGIN
+
+out_no_instance=FALSE;
+out_no_product=FALSE;
+out_lost_reduced=FALSE;
+out_sold_reduced=FALSE; -- We currently don't allow updating 'sold', hence always FALSE
+out_stocked_reduced=FALSE;
+out_no_cat=NULL;
+
+-- 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;
+ RETURN;
+END IF;
+
+-- Check existing entry satisfies constraints
+SELECT total_stock
+ ,total_lost
+ ,product_serial
+ INTO rec
+ FROM merchant_inventory
+ WHERE merchant_serial=my_merchant_id
+ AND product_id=in_product_id;
+
+IF NOT FOUND
+THEN
+ out_no_product=TRUE;
+ RETURN;
+END IF;
+
+my_product_serial = rec.product_serial;
+
+IF rec.total_stock > in_total_stock
+THEN
+ out_stocked_reduced=TRUE;
+ RETURN;
+END IF;
+
+IF rec.total_lost > in_total_lost
+THEN
+ out_lost_reduced=TRUE;
+ RETURN;
+END IF;
+
+-- Remove old categories
+DELETE FROM merchant_product_categories
+ WHERE product_serial=my_product_serial;
+
+-- Add new 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;
+
+UPDATE merchant_inventory SET
+ description=in_description
+ ,description_i18n=in_description_i18n
+ ,unit=in_unit
+ ,image=in_image
+ ,taxes=in_taxes
+ ,price=in_price
+ ,total_stock=in_total_stock
+ ,total_lost=in_total_lost
+ ,address=in_address
+ ,next_restock=in_next_restock
+ ,minimum_age=in_minimum_age
+ WHERE merchant_serial=my_merchant_id
+ AND product_serial=my_product_serial; -- could also match on product_id
+
+ASSERT FOUND,'SELECTED it earlier, should UPDATE it now';
+
+-- Success!
+END $$;
diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in
index 3588c2e9..9afa246b 100644
--- a/src/backenddb/procedures.sql.in
+++ b/src/backenddb/procedures.sql.in
@@ -21,5 +21,6 @@ SET search_path TO merchant;
#include "pg_insert_deposit_to_transfer.sql"
#include "pg_insert_product.sql"
#include "pg_insert_transfer_details.sql"
+#include "pg_update_product.sql"
COMMIT;
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
index de8b2b9a..83fd8dcd 100644
--- a/src/backenddb/test_merchantdb.c
+++ b/src/backenddb/test_merchantdb.c
@@ -833,9 +833,9 @@ test_insert_product (const struct InstanceData *instance,
if (expected_result > 0)
{
TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance,
- "Conflict returned");
+ "No instance wrong");
TEST_COND_RET_ON_FAIL (conflict == expect_conflict,
- "Conflict returned");
+ "Conflict wrong");
TEST_COND_RET_ON_FAIL (no_cat == expected_no_cat,
"Wrong category missing returned");
}
@@ -854,14 +854,54 @@ test_insert_product (const struct InstanceData *instance,
static int
test_update_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_no_instance,
+ bool expect_no_product,
+ bool expect_lost_reduced,
+ bool expect_sold_reduced,
+ bool expect_stocked_reduced,
+ ssize_t expected_no_cat)
+
{
- TEST_COND_RET_ON_FAIL (expected_result ==
- plugin->update_product (plugin->cls,
- instance->instance.id,
- product->id,
- &product->product),
- "Update product failed\n");
+ bool no_instance;
+ ssize_t no_cat;
+ bool no_product;
+ bool lost_reduced;
+ bool sold_reduced;
+ bool stocked_reduced;
+
+ TEST_COND_RET_ON_FAIL (
+ expected_result ==
+ plugin->update_product (plugin->cls,
+ instance->instance.id,
+ product->id,
+ &product->product,
+ num_cats,
+ cats,
+ &no_instance,
+ &no_cat,
+ &no_product,
+ &lost_reduced,
+ &sold_reduced,
+ &stocked_reduced),
+ "Update product failed\n");
+ if (expected_result > 0)
+ {
+ TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance,
+ "No instance wrong");
+ TEST_COND_RET_ON_FAIL (no_product == expect_no_product,
+ "No product wrong");
+ TEST_COND_RET_ON_FAIL (lost_reduced == expect_lost_reduced,
+ "No product wrong");
+ TEST_COND_RET_ON_FAIL (stocked_reduced == expect_stocked_reduced,
+ "Stocked reduced wrong");
+ TEST_COND_RET_ON_FAIL (sold_reduced == expect_sold_reduced,
+ "Sold reduced wrong");
+ TEST_COND_RET_ON_FAIL (no_cat == expected_no_cat,
+ "Wrong category missing returned");
+ }
return 0;
}
@@ -878,25 +918,34 @@ test_lookup_product (const struct InstanceData *instance,
const struct ProductData *product)
{
struct TALER_MERCHANTDB_ProductDetails lookup_result;
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
+
if (0 > plugin->lookup_product (plugin->cls,
instance->instance.id,
product->id,
- &lookup_result))
+ &lookup_result,
+ &num_categories,
+ &categories))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Lookup product failed\n");
TALER_MERCHANTDB_product_details_free (&lookup_result);
return 1;
}
- const struct TALER_MERCHANTDB_ProductDetails *to_cmp = &product->product;
- if (0 != check_products_equal (&lookup_result,
- to_cmp))
+ GNUNET_free (categories);
{
- GNUNET_break (0);
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Lookup product failed: incorrect product returned\n");
- TALER_MERCHANTDB_product_details_free (&lookup_result);
- return 1;
+ const struct TALER_MERCHANTDB_ProductDetails *to_cmp = &product->product;
+
+ if (0 != check_products_equal (&lookup_result,
+ to_cmp))
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Lookup product failed: incorrect product returned\n");
+ TALER_MERCHANTDB_product_details_free (&lookup_result);
+ return 1;
+ }
}
TALER_MERCHANTDB_product_details_free (&lookup_result);
return 0;
@@ -1148,15 +1197,23 @@ run_test_products (struct TestProducts_Closure *cls)
TEST_RET_ON_FAIL (test_lookup_product (&cls->instance,
&cls->products[0]));
/* Make sure it fails correctly for products that don't exist */
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
- plugin->lookup_product (plugin->cls,
- cls->instance.instance.id,
- "nonexistent_product",
- NULL))
{
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Lookup product failed\n");
- return 1;
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
+
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ plugin->lookup_product (plugin->cls,
+ cls->instance.instance.id,
+ "nonexistent_product",
+ NULL,
+ &num_categories,
+ &categories))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Lookup product failed\n");
+ return 1;
+ }
+ GNUNET_free (categories);
}
/* Test product update */
cls->products[0].product.description =
@@ -1182,33 +1239,67 @@ run_test_products (struct TestProducts_Closure *cls)
json_array_append_new (cls->products[0].product.address,
json_string ("444 Some Street")));
cls->products[0].product.next_restock = GNUNET_TIME_timestamp_get ();
- TEST_RET_ON_FAIL (test_update_product (&cls->instance,
- &cls->products[0],
- GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_update_product (
+ &cls->instance,
+ &cls->products[0],
+ 0,
+ NULL,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false,
+ false,
+ false,
+ false,
+ false,
+ -1));
{
struct ProductData stock_dec = cls->products[0];
stock_dec.product.total_stock = 40;
- TEST_RET_ON_FAIL (test_update_product (&cls->instance,
- &stock_dec,
- GNUNET_DB_STATUS_SUCCESS_NO_RESULTS))
- ;
+ TEST_RET_ON_FAIL (test_update_product (
+ &cls->instance,
+ &stock_dec,
+ 0,
+ NULL,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false,
+ false,
+ false,
+ false,
+ true,
+ -1));
}
{
struct ProductData lost_dec = cls->products[0];
lost_dec.product.total_lost = 1;
- TEST_RET_ON_FAIL (test_update_product (&cls->instance,
- &lost_dec,
- GNUNET_DB_STATUS_SUCCESS_NO_RESULTS))
- ;
+ TEST_RET_ON_FAIL (test_update_product (
+ &cls->instance,
+ &lost_dec,
+ 0,
+ NULL,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false,
+ false,
+ true,
+ false,
+ false,
+ -1));
}
TEST_RET_ON_FAIL (test_lookup_product (&cls->instance,
&cls->products[0]));
- TEST_RET_ON_FAIL (test_update_product (&cls->instance,
- &cls->products[1],
- GNUNET_DB_STATUS_SUCCESS_NO_RESULTS));
+ TEST_RET_ON_FAIL (test_update_product (
+ &cls->instance,
+ &cls->products[1],
+ 0,
+ NULL,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false,
+ true,
+ false,
+ false,
+ false,
+ -1));
/* Test collective product lookup */
TEST_RET_ON_FAIL (test_insert_product (&cls->instance,
&cls->products[1],
diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h
index 12c00833..faccc55a 100644
--- a/src/include/taler_merchantdb_plugin.h
+++ b/src/include/taler_merchantdb_plugin.h
@@ -341,13 +341,18 @@ struct TALER_MERCHANTDB_ProductDetails
* @param product_serial row ID of the product
* @param product_id ID of the product
* @param pd full product details
+ * @param num_categories length of @a categories array
+ * @param categories array of categories the
+ * product is in
*/
typedef void
(*TALER_MERCHANTDB_ProductCallback)(
void *cls,
uint64_t product_serial,
const char *product_id,
- const struct TALER_MERCHANTDB_ProductDetails *pd);
+ const struct TALER_MERCHANTDB_ProductDetails *pd,
+ size_t num_categories,
+ const uint64_t *categories);
/**
@@ -1728,13 +1733,18 @@ struct TALER_MERCHANTDB_Plugin
* @param product_id product to lookup
* @param[out] pd set to the product details on success, can be NULL
* (in that case we only want to check if the product exists)
+ * @param[out] num_categories set to length of @a categories array
+ * @param[out] categories set to array of categories the
+ * product is in, caller must free() it.
* @return database result code
*/
enum GNUNET_DB_QueryStatus
(*lookup_product)(void *cls,
const char *instance_id,
const char *product_id,
- struct TALER_MERCHANTDB_ProductDetails *pd);
+ struct TALER_MERCHANTDB_ProductDetails *pd,
+ size_t *num_categories,
+ uint64_t **categories);
/**
* Delete information about a product. Note that the transaction must
@@ -1786,21 +1796,31 @@ struct TALER_MERCHANTDB_Plugin
* @param cls closure
* @param instance_id instance to lookup products for
* @param product_id product to lookup
- * @param pd set to the product details on success, can be NULL
- * (in that case we only want to check if the product exists);
- * total_sold in @a pd is ignored (!), total_lost must not
- * exceed total_stock minus the existing total_sold;
- * total_sold and total_stock must be larger or equal to
- * the existing value;
- * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the
- * non-decreasing constraints are not met *or* if the product
- * does not yet exist.
+ * @param pd product details with updated values
+ * @param num_cats length of @a cats array
+ * @param cats number of categories the product is in
+ * @param[out] no_instance the update failed as the instance is unknown
+ * @param[out] no_cat set to -1 on success, otherwise the update failed and this is set
+ * to the index of a category in @a cats that is unknown
+ * @param[out] no_product the @a product_id is unknown
+ * @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
+ * @return database result code
*/
enum GNUNET_DB_QueryStatus
(*update_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,
+ ssize_t *no_cat,
+ bool *no_product,
+ bool *lost_reduced,
+ bool *sold_reduced,
+ bool *stocked_reduced);
/**
* Lock stocks of a particular product. Note that the transaction must
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
index 1e7430d4..d997b767 100644
--- a/src/lib/Makefile.am
+++ b/src/lib/Makefile.am
@@ -10,7 +10,7 @@ lib_LTLIBRARIES = \
libtalermerchant.la
libtalermerchant_la_LDFLAGS = \
- -version-info 5:2:0 \
+ -version-info 5:3:0 \
-no-undefined
libtalermerchant_la_SOURCES = \
diff --git a/src/lib/merchant_api_get_config.c b/src/lib/merchant_api_get_config.c
index cf5147fe..9b501342 100644
--- a/src/lib/merchant_api_get_config.c
+++ b/src/lib/merchant_api_get_config.c
@@ -34,12 +34,12 @@
* Which version of the Taler protocol is implemented
* by this library? Used to determine compatibility.
*/
-#define MERCHANT_PROTOCOL_CURRENT 15
+#define MERCHANT_PROTOCOL_CURRENT 16
/**
* How many configs are we backwards-compatible with?
*/
-#define MERCHANT_PROTOCOL_AGE 3
+#define MERCHANT_PROTOCOL_AGE 4
/**
* How many exchanges do we allow at most per merchant?