merchant

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

commit f799df31e066a23a0df8f4d062470526710741dd
parent fa640a10c6b9bf2e9ec688bb5ab59b9e8a4d599a
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 26 Apr 2020 14:01:59 +0200

implement logic to complete POSTed /orders using inventory data

Diffstat:
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 180++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/backenddb/merchant-0001.sql | 6+++---
Msrc/backenddb/plugin_merchantdb_postgres.c | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/include/taler_merchantdb_plugin.h | 37++++++++++++++++++++++++++++++++++++-
4 files changed, 309 insertions(+), 18 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -74,9 +74,6 @@ check_products (json_t *products) int res; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("description", &description), - /* FIXME: there are other fields in the product specification - that are currently not labeled as optional. Maybe check - those as well, or make them truly optional. */ GNUNET_JSON_spec_end () }; @@ -89,7 +86,7 @@ check_products (json_t *products) { GNUNET_break (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Product description parsing failed at #%u: %s:%u\n", + "Product parsing failed at #%u: %s:%u\n", (unsigned int) index, error_name, error_line); @@ -188,7 +185,7 @@ struct InventoryProduct * @param inventory_products array of products to add to @a order from our inventory * @param uuids_length length of the @a uuids array * @param uuids array of UUIDs used to reserve products from @a inventory_products - * @return transaction status + * @return transaction status, 0 if @a uuids were insufficient to reserve required inventory */ static enum GNUNET_DB_QueryStatus execute_transaction (struct TMH_HandlerContext *hc, @@ -209,7 +206,7 @@ execute_transaction (struct TMH_HandlerContext *hc, GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } - // FIXME: migrate locks from UUIDs to ORDER here! + /* Setup order */ qs = TMH_db->insert_order (TMH_db->cls, hc->instance->settings.id, order_id, @@ -220,7 +217,44 @@ execute_transaction (struct TMH_HandlerContext *hc, TMH_db->rollback (TMH_db->cls); return qs; } - return TMH_db->commit (TMH_db->cls); + GNUNET_assert (qs > 0); + /* Migrate locks from UUIDs to new order: first release old locks */ + for (unsigned int i = 0; i<uuids_length; i++) + { + qs = TMH_db->unlock_inventory (TMH_db->cls, + &uuids[i]); + if (qs < 0) + { + TMH_db->rollback (TMH_db->cls); + return qs; + } + /* qs == 0 is OK here, that just means we did not HAVE any lock under this + UUID */ + } + /* Migrate locks from UUIDs to new order: acquire new locks + (note: this can basically ONLY fail on serializability OR + because the UUID locks were insufficient for the desired + quantities). */ + for (unsigned int i = 0; i<inventory_products_length; i++) + { + qs = TMH_db->insert_order_lock (TMH_db->cls, + hc->instance->settings.id, + order_id, + inventory_products[i].product_id, + inventory_products[i].quantity); + if (qs <= 0) + { + /* qs == 0: lock acquisition failed due to insufficient stocks */ + TMH_db->rollback (TMH_db->cls); + return qs; + } + } + /* finally, commit transaction (note: if it fails, we ALSO re-acquire + the UUID locks, which is exactly what we want) */ + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* 1 == success! */ + return qs; } @@ -790,13 +824,137 @@ merge_inventory (struct MHD_Connection *connection, if (NULL == json_object_get (order, "products")) { - json_object_set_new (order, - "products", - json_array ()); + GNUNET_assert (0 == + json_object_set_new (order, + "products", + json_array ())); } + { + bool have_total = false; + bool want_total; + struct TALER_Amount total; + json_t *np = json_array (); + + want_total = (NULL == json_object_get (order, + "total")); + + for (unsigned int i = 0; i<inventory_products_length; i++) + { + struct TALER_MERCHANTDB_ProductDetails pd; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_product (TMH_db->cls, + hc->instance->settings.id, + inventory_products[i].product_id, + &pd); + if (qs <= 0) + { + enum TALER_ErrorCode ec; + unsigned int http_status; - // FIXME: merge inventory products into order here! + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_ORDERS_LOOKUP_PRODUCT_DB_HARD_FAILURE; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_ORDERS_LOOKUP_PRODUCT_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_ORDERS_LOOKUP_PRODUCT_NOT_FOUND; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); + } + json_decref (np); + return TALER_MHD_reply_with_error (connection, + http_status, + ec, + inventory_products[i].product_id); + } + { + json_t *p; + + p = json_pack ("{s:s, s:o, s:s, s:o, s:o, s:o}", + "description", + pd.description, + "description_i18n", + pd.description_i18n, + "unit", + pd.unit, + "price", + TALER_JSON_from_amount (&pd.price), + "taxes", + pd.taxes, + "image", + pd.image); + GNUNET_assert (NULL != p); + GNUNET_assert (0 == + json_array_append_new (np, + p)); + if (have_total) + { + if (0 < + TALER_amount_add (&total, + &total, + &pd.price)) + { + GNUNET_break (0); + json_decref (np); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ORDERS_TOTAL_SUM_FAILED, + "failed to add up product prices"); + } + } + else + { + have_total = true; + total = pd.price; + } + + } + GNUNET_free (pd.description); + GNUNET_free (pd.unit); + json_decref (pd.address); + } + if ( (have_total) && + (want_total) ) + { + GNUNET_assert (0 == + json_object_set_new (order, + "total", + TALER_JSON_from_amount (&total))); + } + if ( (! have_total) && + (want_total) ) + { + GNUNET_break_op (0); + json_decref (np); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ORDERS_TOTAL_MISSING, + "total missing in order, and we could not calculate it"); + } + + /* merge into existing products list */ + { + json_t *xp; + + xp = json_object_get (order, + "products"); + GNUNET_assert (NULL != xp); + json_array_extend (xp, np); + json_decref (np); + } + } return add_payment_details (connection, hc, order, diff --git a/src/backenddb/merchant-0001.sql b/src/backenddb/merchant-0001.sql @@ -167,12 +167,12 @@ CREATE TABLE IF NOT EXISTS merchant_inventory_locks ,total_locked BIGINT NOT NULL ,expiration TIMESTAMP NOT NULL ); -CREATE INDEX IF NOT EXISTS merchant_inventory_locks_by_product_and_lock - ON merchant_inventory_locks - (product_serial, lock_uuid); CREATE INDEX IF NOT EXISTS merchant_inventory_locks_by_expiration ON merchant_inventory_locks (expiration); +CREATE INDEX IF NOT EXISTS merchant_inventory_locks_by_uuid + ON merchant_inventory_locks + (lock_uuid); COMMENT ON TABLE merchant_inventory_locks IS 'locks on inventory helt by shopping carts; note that locks MAY not be honored if merchants increase total_lost for inventory'; COMMENT ON COLUMN merchant_inventory_locks.total_locked diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -1151,6 +1151,68 @@ postgres_insert_order (void *cls, } +/** + * Release an inventory lock by UUID. Releases ALL stocks locked under + * the given UUID. + * + * @param cls closure + * @param uuid the UUID to release locks for + * @return transaction status, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS means there are no locks under @a uuid + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success + */ +static enum GNUNET_DB_QueryStatus +postgres_unlock_inventory (void *cls, + const struct GNUNET_Uuid *uuid) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (uuid), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "unlock_inventory", + params); +} + + +/** + * Lock inventory stock to a particular order. + * + * @param cls closure + * @param instance_id identifies the instance responsible for the order + * @param order_id alphanumeric string that uniquely identifies the order + * @param product_id uniquely identifies the product to be locked + * @param quantity how many units should be locked to the @a order_id + * @return transaction status, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS means there are insufficient stocks + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success + */ +static enum GNUNET_DB_QueryStatus +postgres_insert_order_lock (void *cls, + const char *instance_id, + const char *order_id, + const char *product_id, + uint32_t quantity) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_string (order_id), + GNUNET_PQ_query_param_string (product_id), + GNUNET_PQ_query_param_uint32 (&quantity), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "insert_order_lock", + params); +} + + /* ********************* OLD API ************************** */ /** @@ -4163,7 +4225,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) " FROM merchant_inventory" " JOIN ps USING (product_serial)" " WHERE " - " total_stock - total_sold - total_lost > " + " total_stock - total_sold - total_lost - $4 >= " " (SELECT SUM(total_locked)" " FROM merchant_inventory_locks" " WHERE product_serial=ps.product_serial) + " @@ -4204,6 +4266,41 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) " FROM merchant_instances" " WHERE merchant_id=$1", 4), + GNUNET_PQ_make_prepare ("unlock_inventory", + "DELETE" + " FROM merchant_inventory_locks" + " WHERE lock_uuid=$1", + 1), + GNUNET_PQ_make_prepare ("insert_order_lock", + "WITH tmp AS" + " (SELECT " + " product_serial" + " ,merchant_serial" + " ,total_stock" + " ,total_sold" + " ,total_lost" + " FROM merchant_inventory" + " WHERE product_id=$3" + " AND merchant_serial=" + " (SELECT merchant_serial" + " FROM merchant_instances" + " WHERE merchant_id=$1))" + " INSERT INTO merchant_order_locks" + " (product_serial" + " ,total_locked" + " ,order_serial)" + " SELECT tmp.product_serial, $4, order_serial" + " FROM merchant_orders" + " JOIN tmp USING(merchant_serial)" + " WHERE order_id=$2 AND" + " tmp.total_stock - tmp.total_sold - tmp.total_lost - $4 >= " + " (SELECT SUM(total_locked)" + " FROM merchant_inventory_locks" + " WHERE product_serial=tmp.product_serial) + " + " (SELECT SUM(total_locked)" + " FROM merchant_order_locks" + " WHERE product_serial=tmp.product_serial)", + 4), /* OLD API: */ #if 0 GNUNET_PQ_make_prepare ("insert_deposit", @@ -4698,7 +4795,9 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) plugin->lock_product = &postgres_lock_product; plugin->delete_order = &postgres_delete_order; plugin->lookup_order = &postgres_lookup_order; - + plugin->insert_order = &postgres_insert_order; + plugin->unlock_inventory = &postgres_unlock_inventory; + plugin->insert_order_lock = &postgres_insert_order_lock; /* old API: */ plugin->store_deposit = &postgres_store_deposit; plugin->store_coin_to_transfer = &postgres_store_coin_to_transfer; @@ -4711,7 +4810,6 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) plugin->find_deposits_by_wtid = &postgres_find_deposits_by_wtid; plugin->find_proof_by_wtid = &postgres_find_proof_by_wtid; plugin->insert_contract_terms = &postgres_insert_contract_terms; - plugin->insert_order = &postgres_insert_order; plugin->find_contract_terms = &postgres_find_contract_terms; plugin->find_contract_terms_history = &postgres_find_contract_terms_history; plugin->find_contract_terms_by_date = &postgres_find_contract_terms_by_date; diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -651,7 +651,7 @@ struct TALER_MERCHANTDB_Plugin * * @param cls closure * @param instance_id identifies the instance responsible for the order - * @param order_id alphanumeric string that uniquely identifies the proposal + * @param order_id alphanumeric string that uniquely identifies the order * @param pay_deadline how long does the customer have to pay for the order * @param contract_terms proposal data to store * @return transaction status @@ -664,6 +664,41 @@ struct TALER_MERCHANTDB_Plugin const json_t *contract_terms); + /** + * Release an inventory lock by UUID. Releases ALL stocks locked under + * the given UUID. + * + * @param cls closure + * @param uuid the UUID to release locks for + * @return transaction status, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS means there are no locks under @a uuid + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success + */ + enum GNUNET_DB_QueryStatus + (*unlock_inventory)(void *cls, + const struct GNUNET_Uuid *uuid); + + + /** + * Lock inventory stock to a particular order. + * + * @param cls closure + * @param instance_id identifies the instance responsible for the order + * @param order_id alphanumeric string that uniquely identifies the order + * @param product_id uniquely identifies the product to be locked + * @param quantity how many units should be locked to the @a order_id + * @return transaction status, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS means there are insufficient stocks + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT indicates success + */ + enum GNUNET_DB_QueryStatus + (*insert_order_lock)(void *cls, + const char *instance_id, + const char *order_id, + const char *product_id, + uint32_t quantity); + + /* ****************** OLD API ******************** */ /**