commit 5779a18b87620c4d62e3841c82879c42e099c697
parent e74802272f11e733b2ddbf405b20a8dbf2b304f8
Author: Florian Dold <dold@taler.net>
Date: Tue, 25 Aug 2026 23:10:08 +0200
merchant orders: report and repair wired status
Diffstat:
8 files changed, 138 insertions(+), 4 deletions(-)
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 "37:0:25"
+#define MERCHANT_PROTOCOL_VERSION "38:0:26"
/**
diff --git a/src/backend/taler-merchant-httpd_get-private-orders.c b/src/backend/taler-merchant-httpd_get-private-orders.c
@@ -789,7 +789,9 @@ add_order (void *cls,
GNUNET_JSON_pack_bool ("refundable",
refundable),
GNUNET_JSON_pack_bool ("paid",
- paid))));
+ paid),
+ GNUNET_JSON_pack_bool ("wired",
+ wired))));
break;
case POF_CSV:
{
diff --git a/src/backenddb/sql-schema/merchant-0045.sql b/src/backenddb/sql-schema/merchant-0045.sql
@@ -0,0 +1,86 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2026 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
+-- Foundation; either version 3, or (at your option) any later version.
+--
+-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+-- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License along with
+-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+-- @file merchant-0045.sql
+-- @brief repair settlement flags left stale by the reconciliation SQL bug
+
+BEGIN;
+
+SELECT _v.register_patch('merchant-0045', NULL, NULL);
+
+SET search_path TO merchant;
+
+CREATE PROCEDURE merchant.merchant_0045_init(s TEXT)
+ LANGUAGE plpgsql
+ AS $OUTER$
+BEGIN
+
+ EXECUTE format('SET LOCAL search_path TO %I', s);
+
+ -- From July 2025 until April 2026, the reconciliation query missed
+ -- parentheses around its retry/WTID test. An unrelated deposit without a
+ -- WTID could therefore keep completed deposit confirmations wire-pending.
+ -- Reconstruct the flag from the per-deposit settlement evidence. Be
+ -- conservative about NULL retry state and require at least one deposit.
+ UPDATE merchant_deposit_confirmations mdc
+ SET wire_pending=FALSE
+ WHERE mdc.wire_pending
+ AND EXISTS
+ (SELECT 1
+ FROM merchant_deposits md
+ WHERE md.deposit_confirmation_serial=
+ mdc.deposit_confirmation_serial)
+ AND NOT EXISTS
+ (SELECT 1
+ FROM merchant_deposits md
+ WHERE md.deposit_confirmation_serial=
+ mdc.deposit_confirmation_serial
+ AND (COALESCE(md.settlement_retry_needed, TRUE)
+ OR (md.settlement_wtid IS NULL)
+ OR (COALESCE(md.settlement_last_ec, 0) <> 0)));
+
+ -- A paid contract is wired once all of its deposit confirmations are done.
+ -- Require a confirmation so externally marked payments are not changed.
+ -- This migration deliberately does not generate historical notifications
+ -- or order-settled webhooks.
+ UPDATE merchant_contract_terms mct
+ SET wired=TRUE
+ WHERE mct.paid
+ AND NOT mct.wired
+ AND EXISTS
+ (SELECT 1
+ FROM merchant_deposit_confirmations mdc
+ WHERE mdc.order_serial=mct.order_serial)
+ AND NOT EXISTS
+ (SELECT 1
+ FROM merchant_deposit_confirmations mdc
+ WHERE mdc.order_serial=mct.order_serial
+ AND mdc.wire_pending);
+
+ SET LOCAL search_path TO merchant;
+
+END
+$OUTER$;
+
+INSERT INTO merchant.instance_fixups
+ (migration_name
+ ,version)
+ VALUES
+ ('merchant_0045_init'
+ ,45);
+-- Apply new fix-up to existing instances
+CALL merchant.fixup_instance_schema (45::INT8);
+
+COMMIT;
diff --git a/src/backenddb/sql-schema/meson.build b/src/backenddb/sql-schema/meson.build
@@ -139,6 +139,7 @@ generated_sql = [
['merchant-0042.sql'],
['merchant-0043.sql'],
['merchant-0044.sql'],
+ ['merchant-0045.sql'],
]
foreach g : generated_sql
diff --git a/src/include/taler/merchant/get-private-orders.h b/src/include/taler/merchant/get-private-orders.h
@@ -490,6 +490,13 @@ struct TALER_MERCHANT_GetPrivateOrdersOrderEntry
bool paid;
/**
+ * True if the exchange reports that it wired the order's funds.
+ * False if the response came from a backend older than protocol v38.
+ * @since protocol v38.
+ */
+ bool wired;
+
+ /**
* Deadline by which the order must be paid.
* Zero if the response came from a backend older than protocol v37.
* @since protocol v37.
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 37
+#define MERCHANT_PROTOCOL_CURRENT 38
/**
* How many configs are we backwards-compatible with?
*/
-#define MERCHANT_PROTOCOL_AGE 13
+#define MERCHANT_PROTOCOL_AGE 14
/**
* 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
@@ -208,6 +208,10 @@ parse_orders (const json_t *ia,
&ie->refundable),
GNUNET_JSON_spec_bool ("paid",
&ie->paid),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_bool ("wired",
+ &ie->wired),
+ NULL),
GNUNET_JSON_spec_end ()
};
diff --git a/src/testing/test_merchant_transfer_tracking.sh b/src/testing/test_merchant_transfer_tracking.sh
@@ -228,6 +228,23 @@ check_order_listing 'max_age_s=3600' some
check_order_listing 'max_age_s=0' none
echo " OK"
+echo -n "Checking unwired value in order listing..."
+STATUS=$(curl 'http://localhost:9966/instances/test/private/orders?limit=20' \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+if [ "$STATUS" != "200" ]
+then
+ cat "$LAST_RESPONSE"
+ exit_fail "Expected 200 ok listing unwired order. got: $STATUS"
+fi
+LIST_WIRED=$(jq -e -r --arg order_id "$ORDER_ID" \
+ '.orders[] | select(.order_id == $order_id) | .wired' < "$LAST_RESPONSE")
+if [ "$LIST_WIRED" != "false" ]
+then
+ jq . < "$LAST_RESPONSE"
+ exit_fail "Order list .wired was not false before settlement"
+fi
+echo " OK"
+
# "max_age" was deprecated in protocol v33, but MUST still work.
echo -n "Checking order listing with deprecated max_age..."
check_order_listing 'max_age=3600000' some
@@ -511,6 +528,23 @@ fi
echo " OK"
+echo -n "Checking wired value and filter in order listing..."
+STATUS=$(curl 'http://localhost:9966/instances/test/private/orders?limit=20&wired=YES' \
+ -w "%{http_code}" -s -o "$LAST_RESPONSE")
+if [ "$STATUS" != "200" ]
+then
+ cat "$LAST_RESPONSE"
+ exit_fail "Expected 200 ok listing wired order. got: $STATUS"
+fi
+LIST_WIRED=$(jq -e -r --arg order_id "$ORDER_ID" \
+ '.orders[] | select(.order_id == $order_id) | .wired' < "$LAST_RESPONSE")
+if [ "$LIST_WIRED" != "true" ]
+then
+ jq . < "$LAST_RESPONSE"
+ exit_fail "Order list .wired was not true after settlement"
+fi
+echo " OK"
+
echo "================== 2nd order ====================== "