merchant

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

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:
Msrc/backend/taler-merchant-reconciliation.c | 3++-
Msrc/backenddb/insert_transfer_details.c | 62++++++++++++++++++++++++++++++++++++++------------------------
Msrc/backenddb/insert_transfer_details.sql | 14++++++++++++--
Msrc/backenddb/test_merchantdb.c | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/include/merchant-database/insert_transfer_details.h | 6+++++-
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