merchant

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

commit 96c5fa2db06b3adce92b855c6223c123dd003c32
parent fee49e7f4eda1c19e4609b0a0fca16e45af2c623
Author: Florian Dold <dold@taler.net>
Date:   Mon, 17 Aug 2026 18:57:38 +0200

merchant: filter order history by expiration

Diffstat:
Mmeson.build | 4++--
Msrc/backend/taler-merchant-httpd_get-config.c | 2+-
Msrc/backend/taler-merchant-httpd_get-private-orders.c | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/backenddb/iterate_orders.c | 19+++++++++++++++++--
Msrc/backenddb/test_merchantdb.c | 38++++++++++++++++++++++++++++++++++++++
Msrc/include/merchant-database/iterate_orders.h | 6++++++
Msrc/include/taler/merchant/get-private-orders.h | 34+++++++++++++++++++++++++++++++++-
Msrc/lib/merchant_api_get-config.c | 4++--
Msrc/lib/merchant_api_get-private-orders.c | 17+++++++++++++++++
9 files changed, 174 insertions(+), 15 deletions(-)

diff --git a/meson.build b/meson.build @@ -335,10 +335,10 @@ if not get_option('only-doc') libltversions = [ - ['libtalermerchant', '10:0:0'], + ['libtalermerchant', '11:0:0'], ['libtalermerchantutil', '1:1:1'], ['libtalermerchantbank', '0:1:0'], - ['libtalermerchantdb', '7:0:0'], + ['libtalermerchantdb', '8:0:0'], ['libtalermerchanttesting', '4:1:1'], ] diff --git a/src/backend/taler-merchant-httpd_get-config.c b/src/backend/taler-merchant-httpd_get-config.c @@ -44,7 +44,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_get_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "36:0:24" +#define MERCHANT_PROTOCOL_VERSION "37:0:25" /** diff --git a/src/backend/taler-merchant-httpd_get-private-orders.c b/src/backend/taler-merchant-httpd_get-private-orders.c @@ -188,6 +188,12 @@ struct TMH_PendingOrder struct GNUNET_SCHEDULER_Task *order_timeout_task; /** + * True if a suspended request must run its database query again instead of + * replying with the response accumulated so far. + */ + bool recheck; + + /** * HTTP status to return with @e response. */ unsigned int http_status; @@ -254,6 +260,7 @@ order_timeout (void *cls) struct TMH_MerchantInstance *mi = po->mi; po->order_timeout_task = NULL; + po->recheck = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resuming long polled job due to timeout\n"); GNUNET_assert (po->in_dll); @@ -758,6 +765,11 @@ add_order (void *cls, order_serial), GNUNET_JSON_pack_timestamp ("timestamp", creation_time), + GNUNET_JSON_pack_timestamp ( + "pay_deadline", + (NULL != contract) + ? contract->pc->pay_deadline + : order->pay_deadline), TALER_JSON_pack_amount ("amount", amount), GNUNET_JSON_pack_allow_null ( @@ -942,7 +954,8 @@ resume_by_event (void *cls, po); continue; } - po->of.delta--; + if (TALER_EXCHANGE_YNA_ALL == po->of.expired) + po->of.delta--; } else { @@ -962,15 +975,25 @@ resume_by_event (void *cls, po); continue; } - po->of.delta++; + if (TALER_EXCHANGE_YNA_ALL == po->of.expired) + po->of.delta++; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Waking up client %p!\n", po); - add_order (po, - NULL, - order_serial_id, - date); + if (TALER_EXCHANGE_YNA_ALL != po->of.expired) + { + /* The event flags do not carry a payment deadline. Re-run the query + so the expiration predicate is evaluated by the database. */ + po->recheck = true; + } + else + { + add_order (po, + NULL, + order_serial_id, + date); + } GNUNET_assert (po->in_dll); GNUNET_CONTAINER_DLL_remove (mi->po_head, mi->po_tail, @@ -1319,6 +1342,16 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, if (NULL != po) { + if (po->recheck) + { + if (NULL != po->order_timeout_task) + { + GNUNET_SCHEDULER_cancel (po->order_timeout_task); + po->order_timeout_task = NULL; + } + po->recheck = false; + goto run_query; + } if (TALER_EC_NONE != po->result) { /* Resumed from long-polling with error */ @@ -1434,6 +1467,17 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_PARAMETER_MALFORMED, "wired"); } + if (! (TALER_MHD_arg_to_yna (connection, + "expired", + TALER_EXCHANGE_YNA_ALL, + &po->of.expired)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "expired"); + } po->of.delta = -20; /* deprecated in protocol v12 */ TALER_MHD_parse_request_snumber (connection, @@ -1615,6 +1659,7 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, mi); } +run_query: po->of.timeout = GNUNET_TIME_absolute_get_remaining (po->long_poll_timeout); qs = TALER_MERCHANTDB_iterate_orders (TMH_db, @@ -1638,9 +1683,15 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) && (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) ) { + struct GNUNET_TIME_Absolute wakeup = po->long_poll_timeout; + + if (TALER_EXCHANGE_YNA_YES == po->of.expired) + wakeup = GNUNET_TIME_absolute_min ( + wakeup, + GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_SECONDS)); GNUNET_assert (NULL == po->order_timeout_task); po->order_timeout_task - = GNUNET_SCHEDULER_add_at (po->long_poll_timeout, + = GNUNET_SCHEDULER_add_at (wakeup, &order_timeout, po); GNUNET_CONTAINER_DLL_insert (mi->po_head, diff --git a/src/backenddb/iterate_orders.c b/src/backenddb/iterate_orders.c @@ -107,6 +107,7 @@ TALER_MERCHANTDB_iterate_orders ( .cb_cls = cb_cls }; uint64_t limit = (of->delta > 0) ? of->delta : -of->delta; + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); struct GNUNET_PQ_QueryParam params[] = { /* $1 */ GNUNET_PQ_query_param_uint64 (&limit), @@ -131,6 +132,9 @@ TALER_MERCHANTDB_iterate_orders ( NULL == of->summary_filter ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_string (of->summary_filter), + GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_ALL == of->expired)), + GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_YES == of->expired)), + GNUNET_PQ_query_param_timestamp (&now), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; @@ -139,10 +143,11 @@ TALER_MERCHANTDB_iterate_orders ( GNUNET_assert (0 == strcmp (instance_id, pg->current_merchant_id)); 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, expired: %d\n", of->paid, of->refunded, - of->wired); + of->wired, + of->expired); if (of->delta > 0) { @@ -171,6 +176,8 @@ TALER_MERCHANTDB_iterate_orders ( " ($13 = fulfillment_url))" " AND ( ($14::TEXT IS NULL) OR " " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " AND ($15 OR" + " (CAST($16 AS BOOL) = (pay_deadline < $17)))" " ORDER BY order_serial ASC" " LIMIT $1)" "UNION " /* union ensures elements are distinct! */ @@ -196,6 +203,9 @@ TALER_MERCHANTDB_iterate_orders ( " ($13 = fulfillment_url))" " AND ( ($14::TEXT IS NULL) OR " " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " AND ($15 OR" + " (CAST($16 AS BOOL) = ((NOT paid) AND" + " (pay_deadline < $17))))" " ORDER BY order_serial ASC" " LIMIT $1)" " ORDER BY order_serial ASC" @@ -228,6 +238,8 @@ TALER_MERCHANTDB_iterate_orders ( " ($13 = fulfillment_url))" " AND ( ($14::TEXT IS NULL) OR " " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " AND ($15 OR" + " (CAST($16 AS BOOL) = (pay_deadline < $17)))" " ORDER BY order_serial DESC" " LIMIT $1)" "UNION " /* union ensures elements are distinct! */ @@ -253,6 +265,9 @@ TALER_MERCHANTDB_iterate_orders ( " ($13 = fulfillment_url))" " AND ( ($14::TEXT IS NULL) OR " " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " AND ($15 OR" + " (CAST($16 AS BOOL) = ((NOT paid) AND" + " (pay_deadline < $17))))" " ORDER BY order_serial DESC" " LIMIT $1)" " ORDER BY order_serial DESC" diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -3393,6 +3393,7 @@ get_order_serial (const struct InstanceData *instance, .paid = TALER_EXCHANGE_YNA_ALL, .refunded = TALER_EXCHANGE_YNA_ALL, .wired = TALER_EXCHANGE_YNA_ALL, + .expired = TALER_EXCHANGE_YNA_ALL, .date = GNUNET_TIME_UNIT_ZERO_TS, .start_row = 0, .delta = 256 @@ -3949,6 +3950,7 @@ run_test_orders (struct TestOrders_Closure *cls) .paid = TALER_EXCHANGE_YNA_ALL, .refunded = TALER_EXCHANGE_YNA_ALL, .wired = TALER_EXCHANGE_YNA_ALL, + .expired = TALER_EXCHANGE_YNA_ALL, .date = GNUNET_TIME_UNIT_ZERO_TS, .start_row = 0, .delta = 8 @@ -3990,6 +3992,17 @@ run_test_orders (struct TestOrders_Closure *cls) TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[1], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + filter.expired = TALER_EXCHANGE_YNA_NO; + TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, + &filter, + 2, + cls->orders)); + filter.expired = TALER_EXCHANGE_YNA_YES; + TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, + &filter, + 0, + NULL)); + filter.expired = TALER_EXCHANGE_YNA_ALL; serial = get_order_serial (&cls->instance, &cls->orders[0]); TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, @@ -4145,6 +4158,13 @@ run_test_orders (struct TestOrders_Closure *cls) TEST_RET_ON_FAIL (test_lookup_order_status (&cls->instance, &cls->orders[0], true)); + filter.paid = TALER_EXCHANGE_YNA_ALL; + filter.expired = TALER_EXCHANGE_YNA_YES; + TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, + &filter, + 0, + NULL)); + filter.expired = TALER_EXCHANGE_YNA_ALL; filter.paid = TALER_EXCHANGE_YNA_YES; TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, &filter, @@ -4200,9 +4220,26 @@ run_test_orders (struct TestOrders_Closure *cls) TEST_RET_ON_FAIL (test_insert_order (&cls->instance, &cls->orders[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + filter.paid = TALER_EXCHANGE_YNA_ALL; + filter.expired = TALER_EXCHANGE_YNA_YES; + TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, + &filter, + 1, + &cls->orders[2])); + filter.expired = TALER_EXCHANGE_YNA_NO; + TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, + &filter, + 0, + NULL)); TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance, &cls->orders[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + filter.expired = TALER_EXCHANGE_YNA_YES; + TEST_RET_ON_FAIL (test_lookup_orders (&cls->instance, + &filter, + 1, + &cls->orders[2])); + filter.expired = TALER_EXCHANGE_YNA_ALL; TEST_RET_ON_FAIL (test_delete_order (&cls->instance, &cls->orders[2], GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); @@ -7908,6 +7945,7 @@ run_test_lookup_orders_all_filters ( .paid = (i % 3) + 1, .refunded = ((i / 3) % 3) + 1, .wired = ((i / 9) % 3) + 1, + .expired = TALER_EXCHANGE_YNA_ALL, .date = GNUNET_TIME_UNIT_ZERO_TS, .start_row = 0, .delta = 64 diff --git a/src/include/merchant-database/iterate_orders.h b/src/include/merchant-database/iterate_orders.h @@ -57,6 +57,12 @@ struct TALER_MERCHANTDB_OrderFilter enum TALER_EXCHANGE_YesNoAll wired; /** + * Filter by expiration status. An order is expired if it is unpaid and + * its payment deadline is in the past. + */ + enum TALER_EXCHANGE_YesNoAll expired; + + /** * Filter orders by date, exact meaning depends on @e delta. */ struct GNUNET_TIME_Timestamp date; diff --git a/src/include/taler/merchant/get-private-orders.h b/src/include/taler/merchant/get-private-orders.h @@ -92,7 +92,13 @@ enum TALER_MERCHANT_GetPrivateOrdersOption * Only applicable if limit is positive. * @since protocol v27. */ - TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_MAX_AGE + TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_MAX_AGE, + + /** + * Filter by expiration status. + * @since protocol v37. + */ + TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_EXPIRED }; @@ -134,6 +140,12 @@ struct TALER_MERCHANT_GetPrivateOrdersOptionValue /** * Value if @e option is + * #TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_EXPIRED. + */ + enum TALER_EXCHANGE_YesNoAll expired; + + /** + * Value if @e option is * #TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_LIMIT. */ int64_t limit; @@ -257,6 +269,19 @@ TALER_MERCHANT_get_private_orders_create ( } /** + * Set filter on expiration status. + * + * @param e expiration filter to use + * @return representation of the option as a struct TALER_MERCHANT_GetPrivateOrdersOptionValue + */ +#define TALER_MERCHANT_get_private_orders_option_expired(e) \ + (const struct TALER_MERCHANT_GetPrivateOrdersOptionValue) \ + { \ + .option = TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_EXPIRED, \ + .details.expired = (e) \ + } + +/** * Set limit on the number of results to return. * * @param l limit on the number of results to return @@ -464,6 +489,13 @@ struct TALER_MERCHANT_GetPrivateOrdersOrderEntry */ bool paid; + /** + * Deadline by which the order must be paid. + * Zero if the response came from a backend older than protocol v37. + * @since protocol v37. + */ + struct GNUNET_TIME_Timestamp pay_deadline; + }; diff --git 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 36 +#define MERCHANT_PROTOCOL_CURRENT 37 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 12 +#define MERCHANT_PROTOCOL_AGE 13 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_get-private-orders.c b/src/lib/merchant_api_get-private-orders.c @@ -87,6 +87,11 @@ struct TALER_MERCHANT_GetPrivateOrdersHandle enum TALER_EXCHANGE_YesNoAll wired; /** + * Expiration filter. + */ + enum TALER_EXCHANGE_YesNoAll expired; + + /** * Date threshold for filtering. */ struct GNUNET_TIME_Timestamp date; @@ -181,6 +186,10 @@ parse_orders (const json_t *ia, &ie->order_id), GNUNET_JSON_spec_timestamp ("timestamp", &ie->timestamp), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("pay_deadline", + &ie->pay_deadline), + NULL), GNUNET_JSON_spec_uint64 ("row_id", &ie->order_serial), TALER_JSON_spec_amount_any ("amount", @@ -313,6 +322,7 @@ TALER_MERCHANT_get_private_orders_create ( oph->paid = TALER_EXCHANGE_YNA_ALL; oph->refunded = TALER_EXCHANGE_YNA_ALL; oph->wired = TALER_EXCHANGE_YNA_ALL; + oph->expired = TALER_EXCHANGE_YNA_ALL; oph->date = GNUNET_TIME_UNIT_FOREVER_TS; oph->offset = UINT64_MAX; oph->limit = -20; @@ -344,6 +354,9 @@ TALER_MERCHANT_get_private_orders_set_options_ ( case TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_WIRED: oph->wired = opt->details.wired; break; + case TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_EXPIRED: + oph->expired = opt->details.expired; + break; case TALER_MERCHANT_GET_PRIVATE_ORDERS_OPTION_LIMIT: oph->limit = opt->details.limit; break; @@ -460,6 +473,10 @@ TALER_MERCHANT_get_private_orders_start ( (TALER_EXCHANGE_YNA_ALL != oph->wired) ? TALER_yna_to_string (oph->wired) : NULL, + "expired", + (TALER_EXCHANGE_YNA_ALL != oph->expired) + ? TALER_yna_to_string (oph->expired) + : NULL, "date_s", have_date ? dstr