commit fcbbda7508f61edf6588aca86f5bd0a6a2bee24b
parent 2436287b5d7747f058cc6fae818f549a5221ebaa
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 5 Aug 2026 23:49:15 +0200
notify on EVERY order that was reconciled, not just the last one
Diffstat:
5 files changed, 164 insertions(+), 30 deletions(-)
diff --git a/src/backend/taler-merchant-reconciliation.c b/src/backend/taler-merchant-reconciliation.c
@@ -826,7 +826,8 @@ wire_transfer_cb (struct Inquiry *w,
w->exchange->exchange_url,
w->payto_uri,
&w->wtid,
- td);
+ td,
+ NULL);
if (0 > qs)
{
/* Always report on DB error as well to enable diagnostics */
diff --git a/src/backenddb/insert_transfer_details.c b/src/backenddb/insert_transfer_details.c
@@ -39,7 +39,8 @@ TALER_MERCHANTDB_insert_transfer_details (
const char *exchange_url,
struct TALER_FullPayto payto_uri,
const struct TALER_WireTransferIdentifierRawP *wtid,
- const struct TALER_EXCHANGE_TransferData *td)
+ const struct TALER_EXCHANGE_TransferData *td,
+ unsigned int *num_settled_orders)
{
unsigned int len = td->details_length;
struct TALER_Amount coin_values[GNUNET_NZL (len)];
@@ -49,6 +50,8 @@ TALER_MERCHANTDB_insert_transfer_details (
enum GNUNET_DB_QueryStatus qs;
bool duplicate = false;
+ if (NULL != num_settled_orders)
+ *num_settled_orders = 0;
GNUNET_assert (NULL != pg->current_merchant_id);
GNUNET_assert (0 == strcmp (instance_id,
pg->current_merchant_id));
@@ -122,7 +125,8 @@ TALER_MERCHANTDB_insert_transfer_details (
bool no_account;
bool no_exchange;
bool conflict;
- char *order_id = NULL;
+ size_t num_order_ids = 0;
+ char *order_ids = NULL;
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_bool ("out_no_account",
&no_account),
@@ -132,10 +136,10 @@ TALER_MERCHANTDB_insert_transfer_details (
&duplicate),
GNUNET_PQ_result_spec_bool ("out_conflict",
&conflict),
- GNUNET_PQ_result_spec_allow_null (
- GNUNET_PQ_result_spec_string ("out_order_id",
- &order_id),
- NULL),
+ GNUNET_PQ_result_spec_array_string (pg->conn,
+ "out_order_ids",
+ &num_order_ids,
+ &order_ids),
GNUNET_PQ_result_spec_end
};
@@ -145,7 +149,7 @@ TALER_MERCHANTDB_insert_transfer_details (
",out_no_exchange"
",out_duplicate"
",out_conflict"
- ",out_order_id"
+ ",out_order_ids"
" FROM merchant_do_insert_transfer_details"
" ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13);");
qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
@@ -164,25 +168,35 @@ TALER_MERCHANTDB_insert_transfer_details (
qs);
return qs;
}
- if (NULL != order_id)
{
- struct TMH_OrderPayEventP pay_eh = {
- .header.size = htons (sizeof (pay_eh)),
- .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
- .merchant_pub = pg->current_merchant_pub
- };
+ /* One aggregated transfer can settle many orders; notify
+ long-pollers about every single one of them. */
+ const char *pos = order_ids;
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Notifying clients about status change of order %s\n",
- order_id);
- GNUNET_CRYPTO_hash (order_id,
- strlen (order_id),
- &pay_eh.h_order_id);
- GNUNET_PQ_event_notify (pg->conn,
- &pay_eh.header,
- NULL,
- 0);
- GNUNET_free (order_id);
+ if (NULL != num_settled_orders)
+ *num_settled_orders = (unsigned int) num_order_ids;
+ for (size_t k = 0; k<num_order_ids; k++)
+ {
+ const char *order_id = pos;
+ struct TMH_OrderPayEventP pay_eh = {
+ .header.size = htons (sizeof (pay_eh)),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
+ .merchant_pub = pg->current_merchant_pub
+ };
+
+ pos += strlen (order_id) + 1;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Notifying clients about status change of order %s\n",
+ order_id);
+ GNUNET_CRYPTO_hash (order_id,
+ strlen (order_id),
+ &pay_eh.h_order_id);
+ GNUNET_PQ_event_notify (pg->conn,
+ &pay_eh.header,
+ NULL,
+ 0);
+ }
+ GNUNET_free (order_ids);
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Transfer details inserted: %s%s%s%s\n",
diff --git a/src/backenddb/insert_transfer_details.sql b/src/backenddb/insert_transfer_details.sql
@@ -34,7 +34,9 @@ CREATE FUNCTION merchant_do_insert_transfer_details (
OUT out_no_exchange BOOL,
OUT out_duplicate BOOL,
OUT out_conflict BOOL,
- OUT out_order_id TEXT)
+ -- IDs of ALL orders that became fully wired by this transfer;
+ -- one aggregated transfer can settle many orders.
+ OUT out_order_ids TEXT[])
LANGUAGE plpgsql
AS $$
DECLARE
@@ -57,6 +59,8 @@ DECLARE
ini_deposit_fee merchant.taler_amount_currency;
BEGIN
+out_order_ids=ARRAY[]::TEXT[];
+
-- Determine account that was credited.
SELECT expected_credit_serial
INTO my_expected_credit_serial
@@ -217,7 +221,13 @@ LOOP
INTO my_order_id
FROM merchant_contract_terms
WHERE order_serial=my_affected_orders.order_serial;
- out_order_id = my_order_id;
+ -- Remember ALL orders that completed, not just the last
+ -- one: an aggregated transfer settles many orders and
+ -- every one of them needs an ORDER_STATUS_CHANGED event.
+ IF NOT (my_order_id = ANY(out_order_ids))
+ THEN
+ out_order_ids = array_append(out_order_ids, my_order_id);
+ END IF;
-- Insert pending webhook if it exists
INSERT INTO merchant.merchant_pending_webhooks
(merchant_serial
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -6053,13 +6053,55 @@ test_insert_transfer_details (
transfer->exchange_url,
account->payto_uri,
&transfer->wtid,
- &transfer->data),
+ &transfer->data,
+ NULL),
"Insert transfer details failed\n");
return 0;
}
/**
+ * Inserts details for a transfer into the database and checks how
+ * many orders were reported as fully settled by it. An aggregated
+ * transfer settles several orders at once and every one of them must
+ * be reported, otherwise clients long-polling on the others never
+ * learn that the order was wired.
+ *
+ * @param instance the instance the transfer is in.
+ * @param account the destination account for the transfer.
+ * @param transfer the transfer we are adding details to.
+ * @param expected_result the result expected from the db.
+ * @param expected_settled number of orders that must be reported.
+ * @return 0 on success, 1 otherwise.
+ */
+static int
+test_insert_transfer_details_settling (
+ const struct InstanceData *instance,
+ const struct TALER_MERCHANTDB_AccountDetails *account,
+ const struct TransferData *transfer,
+ enum GNUNET_DB_QueryStatus expected_result,
+ unsigned int expected_settled)
+{
+ unsigned int num_settled = 0;
+
+ TEST_SET_INSTANCE (instance->instance.id, expected_result);
+ TEST_COND_RET_ON_FAIL (expected_result ==
+ TALER_MERCHANTDB_insert_transfer_details (
+ pg,
+ instance->instance.id,
+ transfer->exchange_url,
+ account->payto_uri,
+ &transfer->wtid,
+ &transfer->data,
+ &num_settled),
+ "Insert transfer details failed\n");
+ TEST_COND_RET_ON_FAIL (expected_settled == num_settled,
+ "Wrong number of settled orders reported\n");
+ return 0;
+}
+
+
+/**
* Container for data used when testing transfers.
*/
struct TestTransfers_Closure
@@ -6085,6 +6127,11 @@ struct TestTransfers_Closure
struct OrderData order;
/**
+ * Two more orders, both settled by one aggregated transfer.
+ */
+ struct OrderData orders_agg[2];
+
+ /**
* The deposit data.
*/
struct DepositData deposit;
@@ -6096,6 +6143,11 @@ struct TestTransfers_Closure
struct DepositData deposit2;
/**
+ * One deposit for each of @e orders_agg.
+ */
+ struct DepositData deposits_agg[2];
+
+ /**
* Wire fee data.
*/
struct WireFeeData wire_fee[2];
@@ -6103,7 +6155,7 @@ struct TestTransfers_Closure
/**
* The transfers.
*/
- struct TransferData transfers[3];
+ struct TransferData transfers[4];
};
@@ -6175,6 +6227,22 @@ pre_test_transfers (struct TestTransfers_Closure *cls)
&cls->transfers[2]);
}
cls->transfers[2].confirmed = true;
+ /* Two orders settled by one aggregated wire transfer. */
+ make_order ("test_transfers_od_agg_1",
+ &cls->orders_agg[0]);
+ make_order ("test_transfers_od_agg_2",
+ &cls->orders_agg[1]);
+ for (unsigned int i = 0; i < 2; i++)
+ make_deposit (&cls->instance,
+ &cls->account,
+ &cls->orders_agg[i],
+ &cls->signkey,
+ &cls->deposits_agg[i]);
+ make_transfer (&cls->signkey,
+ 2,
+ cls->deposits_agg,
+ &cls->transfers[3]);
+ cls->transfers[3].confirmed = true;
}
@@ -6192,8 +6260,13 @@ post_test_transfers (struct TestTransfers_Closure *cls)
GNUNET_array_grow (cls->transfers[2].data.details,
cls->transfers[2].data.details_length,
0);
+ GNUNET_array_grow (cls->transfers[3].data.details,
+ cls->transfers[3].data.details_length,
+ 0);
free_instance_data (&cls->instance);
free_order_data (&cls->order);
+ free_order_data (&cls->orders_agg[0]);
+ free_order_data (&cls->orders_agg[1]);
}
@@ -6392,6 +6465,38 @@ run_test_transfers (struct TestTransfers_Closure *cls)
&cls->account,
&cls->transfers[2],
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ /* One aggregated transfer settles two orders: BOTH of them must be
+ reported, otherwise clients long-polling on the order that is not
+ reported never learn that it was wired. */
+ for (unsigned int i = 0; i < 2; i++)
+ {
+ TEST_RET_ON_FAIL (test_insert_order (&cls->instance,
+ &cls->orders_agg[i],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_insert_contract_terms (&cls->instance,
+ &cls->orders_agg[i],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_insert_deposit (&cls->instance,
+ &cls->signkey,
+ &cls->deposits_agg[i],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_mark_contract_paid (&cls->instance,
+ &cls->orders_agg[i],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance,
+ &cls->signkey,
+ &cls->orders_agg[i],
+ &cls->deposits_agg[i],
+ &cls->transfers[3],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false));
+ }
+ TEST_RET_ON_FAIL (test_insert_transfer_details_settling (
+ &cls->instance,
+ &cls->account,
+ &cls->transfers[3],
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ 2));
return 0;
}
diff --git a/src/include/merchant-database/insert_transfer_details.h b/src/include/merchant-database/insert_transfer_details.h
@@ -36,6 +36,9 @@
* @param payto_uri what is the merchant's bank account that received the transfer
* @param wtid identifier of the wire transfer
* @param td transfer details to store
+ * @param[out] num_settled_orders set to the number of orders that
+ * became fully wired by this transfer and were thus notified
+ * about; NULL if the caller is not interested
* @return transaction status,
* #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the @a wtid and @a exchange_uri are not known for this @a instance_id
* #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT on success
@@ -47,6 +50,7 @@ TALER_MERCHANTDB_insert_transfer_details (
const char *exchange_url,
struct TALER_FullPayto payto_uri,
const struct TALER_WireTransferIdentifierRawP *wtid,
- const struct TALER_EXCHANGE_TransferData *td);
+ const struct TALER_EXCHANGE_TransferData *td,
+ unsigned int *num_settled_orders);
#endif