merchant

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

commit e8990e5b404a43ca612cd6c6e41e3d3fdac74221
parent 9291f59c6bc8c5c4502b2e732f8a7b9f22304b6e
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Wed, 29 Oct 2025 12:21:15 +0100

draft implementation of #9510

Diffstat:
Msrc/backend/taler-merchant-httpd_private-get-orders.c | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/backend/taler-merchant-httpd_private-get-products.c | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/backenddb/merchant-0025.sql | 35+++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_lookup_orders.c | 15+++++++++++++--
Msrc/backenddb/pg_lookup_products.c | 40++++++++++++++++++++++++++++++++++++++--
Msrc/backenddb/pg_lookup_products.h | 24++++++++++++++++++++++++
Msrc/backenddb/test_merchantdb.c | 3+++
Msrc/include/taler_merchantdb_plugin.h | 29+++++++++++++++++++++++++++++
8 files changed, 244 insertions(+), 6 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c b/src/backend/taler-merchant-httpd_private-get-orders.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2019-2021 Taler Systems SA + (C) 2019--2025 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 published by the Free Software @@ -85,6 +85,11 @@ struct TMH_PendingOrder const char *instance_id; /** + * Alias of @a of.summary_filter, but with memory to be released (owner). + */ + char *summary_filter; + + /** * The result after adding the orders (#TALER_EC_NONE for okay, anything else for an error). */ enum TALER_ErrorCode result; @@ -221,6 +226,7 @@ cleanup (void *ctx) GNUNET_assert (po == GNUNET_CONTAINER_heap_remove_node (po->hn)); json_decref (po->pa); + GNUNET_free (po->summary_filter); GNUNET_free (po); } @@ -682,6 +688,42 @@ TMH_notify_order_change (struct TMH_MerchantInstance *mi, /** + * Transforms an (untrusted) input filter into a Postgresql LIKE filter. + * Escapes "%" and "_" in the @a input and adds "%" at the beginning + * and the end to turn the @a input into a suitable Postgresql argument. + * + * @param input text to turn into a substring match expression, or NULL + * @return NULL if @a input was NULL, otherwise transformed @a input + */ +static char * +tr (const char *input) +{ + char *out; + size_t slen; + size_t wpos; + + if (NULL == input) + return NULL; + slen = strlen (input); + out = GNUNET_malloc (slen * 2 + 3); + wpos = 0; + out[wpos++] = '%'; + for (size_t i = 0; i<slen; i++) + { + char c = input[i]; + + if ( (c == '%') || + (c == '_') ) + out[wpos++] = '\\'; + out[wpos++] = c; + } + out[wpos++] = '%'; + GNUNET_assert (wpos < slen); + return out; +} + + +/** * Handle a GET "/orders" request. * * @param rh context of the handler @@ -838,6 +880,10 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_PARAMETER_MALFORMED, "offset"); } + pf->summary_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "summary_filter")); + pf->of.summary_filter = pf->summary_filter; /* just an alias! */ po->of.session_id = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, diff --git a/src/backend/taler-merchant-httpd_private-get-products.c b/src/backend/taler-merchant-httpd_private-get-products.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2019, 2020, 2021, 2024 Taler Systems SA + (C) 2019, 2020, 2021, 2024, 2025 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 published by the Free Software @@ -47,6 +47,42 @@ add_product (void *cls, } +/** + * Transforms an (untrusted) input filter into a Postgresql LIKE filter. + * Escapes "%" and "_" in the @a input and adds "%" at the beginning + * and the end to turn the @a input into a suitable Postgresql argument. + * + * @param input text to turn into a substring match expression, or NULL + * @return NULL if @a input was NULL, otherwise transformed @a input + */ +static char * +tr (const char *input) +{ + char *out; + size_t slen; + size_t wpos; + + if (NULL == input) + return NULL; + slen = strlen (input); + out = GNUNET_malloc (slen * 2 + 3); + wpos = 0; + out[wpos++] = '%'; + for (size_t i = 0; i<slen; i++) + { + char c = input[i]; + + if ( (c == '%') || + (c == '_') ) + out[wpos++] = '\\'; + out[wpos++] = c; + } + out[wpos++] = '%'; + GNUNET_assert (wpos < slen); + return out; +} + + MHD_RESULT TMH_private_get_products (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, @@ -54,6 +90,9 @@ TMH_private_get_products (const struct TMH_RequestHandler *rh, { json_t *pa; enum GNUNET_DB_QueryStatus qs; + char *category_filter; + char *name_filter; + char *description_filter; int64_t limit; uint64_t offset; @@ -68,14 +107,29 @@ TMH_private_get_products (const struct TMH_RequestHandler *rh, TALER_MHD_parse_request_number (connection, "offset", &offset); + category_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "category_filter")); + name_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "name_filter")); + description_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "description_filter")); pa = json_array (); GNUNET_assert (NULL != pa); qs = TMH_db->lookup_products (TMH_db->cls, hc->instance->settings.id, offset, limit, + category_filter, + name_filter, + description_filter, &add_product, pa); + GNUNET_free (category_filter); + GNUNET_free (name_filter); + GNUNET_free (description_filter); if (0 > qs) { GNUNET_break (0); diff --git a/src/backenddb/merchant-0025.sql b/src/backenddb/merchant-0025.sql @@ -26,6 +26,8 @@ SELECT _v.register_patch('merchant-0025', NULL, NULL); SET search_path TO merchant; +-- We will need this for LIKE-style pattern matching "gin" indices. +CREATE EXTENSION IF NOT EXISTS pg_trgm; -- See enum GNUNET_TIME_RounderInterval CREATE TYPE time_rounder_interval @@ -37,4 +39,37 @@ ALTER TABLE merchant_instances COMMENT ON COLUMN merchant_instances.default_wire_transfer_rounding_interval IS 'To what round value do we round up wire transfer deadlines computed on the basis of the default_wire_transfer_delay.'; +-- The following indices are added to support efficient product filtering. +-- Use a Generalized Inverted Index (GIN) on the respective columns. +-- We use "LOWER" so we can do case-insensitive searches. +-- "gin_trgm_ops" is the operator class for the index, specifically "trigram indexing" from pg_trgm +CREATE INDEX trgm_idx_products_by_name + ON merchant_inventory + USING gin (LOWER(product_name) gin_trgm_ops); + +CREATE INDEX trgm_idx_products_by_description + ON merchant_inventory + USING gin (LOWER(description) gin_trgm_ops); + +CREATE INDEX trgm_idx_categories_by_name + ON merchant_categories + USING gin (LOWER(category_name) gin_trgm_ops); + +CREATE INDEX trgm_idx_contract_summaries + ON merchant_contract_terms + USING gin (LOWER((contract_terms::jsonb ->> 'summary')) gin_trgm_ops); +-- NOTE: Query must use exactly +-- WHERE LOWER(contract_terms::jsonb ->> 'summary') LIKE LOWER($1); + +CREATE INDEX trgm_idx_order_summaries + ON merchant_orders + USING gin (LOWER((contract_terms::jsonb ->> 'summary')) gin_trgm_ops); +-- NOTE: Query must use exactly +-- WHERE LOWER(contract_terms::jsonb ->> 'summary') LIKE LOWER($1); + +-- FIXME: consider +-- ALTER TABLE merchant_contract_terms ALTER COLUMN contract_terms TYPE JSONB USING contract_terms::jsonb; +-- ALTER TABLE merchant_orders ALTER COLUMN contract_terms TYPE JSONB USING contract_terms::jsonb; +-- --- alas, requires modifications across the plugin... + COMMIT; diff --git a/src/backenddb/pg_lookup_orders.c b/src/backenddb/pg_lookup_orders.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022,2023 Taler Systems SA + Copyright (C) 2022, 2023, 2025 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 @@ -131,13 +131,16 @@ TMH_PG_lookup_orders (void *cls, NULL == of->fulfillment_url ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_string (of->fulfillment_url), + NULL == of->summary_filter + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (of->summary_filter), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; char stmt[128]; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Looking up orders, using filter paid:%d, refunded: %d, wired: %d\n", + "Looking up orders, using filter paid: %d, refunded: %d, wired: %d\n", of->paid, of->refunded, of->wired); @@ -174,6 +177,8 @@ TMH_PG_lookup_orders (void *cls, " ($12 = session_id))" " AND ($13 OR " " ($14 = fulfillment_url))" + " AND ( ($15 IS NULL) OR " + " (LOWER(contract_terms::jsonb ->> 'summary') LIKE LOWER($15)) )" " ORDER BY order_serial DESC" " LIMIT $2)" "UNION " /* union ensures elements are distinct! */ @@ -202,6 +207,8 @@ TMH_PG_lookup_orders (void *cls, " ($12 = session_id))" " AND ($13 OR " " ($14 = fulfillment_url))" + " AND ( ($15 IS NULL) OR " + " (LOWER(contract_terms::jsonb ->> 'summary') LIKE LOWER($15)) )" " ORDER BY order_serial DESC" " LIMIT $2)" " ORDER BY order_serial DESC" @@ -236,6 +243,8 @@ TMH_PG_lookup_orders (void *cls, " ($12 = session_id))" " AND ($13 OR " " ($14 = fulfillment_url))" + " AND ( ($15 IS NULL) OR " + " (LOWER(contract_terms::jsonb ->> 'summary') LIKE LOWER($15)) )" " ORDER BY order_serial ASC" " LIMIT $2)" "UNION " /* union ensures elements are distinct! */ @@ -264,6 +273,8 @@ TMH_PG_lookup_orders (void *cls, " ($12 = session_id))" " AND ($13 OR " " ($14 = fulfillment_url))" + " AND ( ($15 IS NULL) OR " + " (LOWER(contract_terms::jsonb ->> 'summary') LIKE LOWER($15)) )" " ORDER BY order_serial ASC" " LIMIT $2)" " ORDER BY order_serial ASC" diff --git a/src/backenddb/pg_lookup_products.c b/src/backenddb/pg_lookup_products.c @@ -96,6 +96,9 @@ TMH_PG_lookup_products (void *cls, const char *instance_id, uint64_t offset, int64_t limit, + const char *category_filter, + const char *name_filter, + const char *description_filter, TALER_MERCHANTDB_ProductsCallback cb, void *cb_cls) { @@ -111,6 +114,15 @@ TMH_PG_lookup_products (void *cls, GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&offset), GNUNET_PQ_query_param_uint64 (&plimit), + NULL == category_filter + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (category_filter), + NULL == name_filter + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (name_filter), + NULL == description_filter + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (description_filter), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; @@ -126,8 +138,20 @@ TMH_PG_lookup_products (void *cls, " USING (merchant_serial)" " WHERE merchant_instances.merchant_id=$1" " AND product_serial > $2" + " AND ( ($3 IS NULL) OR" + " (product_serial IN" + " (SELECT product_serial" + " FROM merchant_product_categories" + " WHERE category_serial IN" + " (SELECT category_serial" + " FROM merchant_categories" + " WHERE category_name LIKE LOWER($3)))) )" + " AND ( ($4 IS NULL) OR" + " (product_name LIKE LOWER($4)) )" + " AND ( ($5 IS NULL) OR" + " (description LIKE LOWER($5)) )" " ORDER BY product_serial ASC" - " LIMIT $3"); + " LIMIT $6"); PREPARE (pg, "lookup_products_desc", "SELECT" @@ -138,8 +162,20 @@ TMH_PG_lookup_products (void *cls, " USING (merchant_serial)" " WHERE merchant_instances.merchant_id=$1" " AND product_serial < $2" + " AND ( ($3 IS NULL) OR" + " (product_serial IN" + " (SELECT product_serial" + " FROM merchant_product_categories" + " WHERE category_serial IN" + " (SELECT category_serial" + " FROM merchant_categories" + " WHERE category_name LIKE LOWER($3)))) )" + " AND ( ($4 IS NULL) OR" + " (product_name LIKE LOWER($4)) )" + " AND ( ($5 IS NULL) OR" + " (description LIKE LOWER($5)) )" " ORDER BY product_serial DESC" - " LIMIT $3"); + " LIMIT $6"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, (limit > 0) diff --git a/src/backenddb/pg_lookup_products.h b/src/backenddb/pg_lookup_products.h @@ -33,6 +33,27 @@ * @param offset transfer_serial number of the transfer we want to offset from * @param limit number of entries to return, negative for descending, * positive for ascending + * @param category_filter filter products by category, NULL to not filter; + * uses the Postgresql "LIKE" pattern matcher, so + * "%" stands for any sequence of zero or more characters, + * "_" stands for any single character; + * use "\%" and "\_" to exactly match "%" or "_". + * We will always use case-insensitive searches, for case-sensitive + * matching the client should filter the result set. + * @param name_filter filter products by name, NULL to not filter + * uses the Postgresql "LIKE" pattern matcher, so + * "%" stands for any sequence of zero or more characters, + * "_" stands for any single character + * use "\%" and "\_" to exactly match "%" or "_". + * We will always use case-insensitive searches, for case-sensitive + * matching the client should filter the result set. + * @param description_filter filter products by description, NULL to not filter + * uses the Postgresql "LIKE" pattern matcher, so + * "%" stands for any sequence of zero or more characters, + * "_" stands for any single character + * use "\%" and "\_" to exactly match "%" or "_". + * We will always use case-insensitive searches, for case-sensitive + * matching the client should filter the result set. * @param cb function to call on all products found * @param cb_cls closure for @a cb * @return database result code @@ -42,6 +63,9 @@ TMH_PG_lookup_products (void *cls, const char *instance_id, uint64_t offset, int64_t limit, + const char *category_filter, + const char *name_filter, + const char *description_filter, TALER_MERCHANTDB_ProductsCallback cb, void *cb_cls); diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -1034,6 +1034,9 @@ test_lookup_products (const struct InstanceData *instance, instance->instance.id, 0, 20, + NULL, + NULL, + NULL, &lookup_products_cb, &cls)) { diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -747,6 +747,11 @@ struct TALER_MERCHANTDB_OrderFilter */ struct GNUNET_TIME_Relative timeout; + /** + * Filter to apply on the summary of the order. + */ + const char *summary_filter; + }; @@ -2134,6 +2139,27 @@ struct TALER_MERCHANTDB_Plugin * @param offset transfer_serial number of the transfer we want to offset from * @param limit number of entries to return, negative for descending, * positive for ascending + * @param category_filter filter products by category, NULL to not filter; + * uses the Postgresql "LIKE" pattern matcher, so + * "%" stands for any sequence of zero or more characters, + * "_" stands for any single character; + * use "\%" and "\_" to exactly match "%" or "_". + * We will always use case-insensitive searches, for case-sensitive + * matching the client should filter the result set. + * @param name_filter filter products by name, NULL to not filter + * uses the Postgresql "LIKE" pattern matcher, so + * "%" stands for any sequence of zero or more characters, + * "_" stands for any single character + * use "\%" and "\_" to exactly match "%" or "_". + * We will always use case-insensitive searches, for case-sensitive + * matching the client should filter the result set. + * @param description_filter filter products by description, NULL to not filter + * uses the Postgresql "LIKE" pattern matcher, so + * "%" stands for any sequence of zero or more characters, + * "_" stands for any single character + * use "\%" and "\_" to exactly match "%" or "_". + * We will always use case-insensitive searches, for case-sensitive + * matching the client should filter the result set. * @param cb function to call on all products found * @param cb_cls closure for @a cb * @return database result code @@ -2143,6 +2169,9 @@ struct TALER_MERCHANTDB_Plugin const char *instance_id, uint64_t offset, int64_t limit, + const char *category_filter, + const char *name_filter, + const char *description_filter, TALER_MERCHANTDB_ProductsCallback cb, void *cb_cls);