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:
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