diff options
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? |