merchant

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

commit ef93cc616fdc7085f422d6966ffed932076c8466
parent 86327fdbf0ac41cbf5a90247e0eb7281c9e98ac3
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sun,  8 Dec 2024 14:53:41 +0100

Merge branch 'master' into dev/bohdan-potuzhnyi/donau-integration

Diffstat:
Msrc/backend/taler-merchant-httpd_exchanges.c | 6+++++-
Msrc/backenddb/Makefile.am | 9+++++++--
Asrc/backenddb/merchant-0013.sql | 495+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_insert_deposit_to_transfer.sql | 20++++++++++++++++++++
Msrc/backenddb/pg_insert_transfer_details.sql | 25+++++++++++++++++++++++++
Asrc/backenddb/test_webhook_trigger.sh | 369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 921 insertions(+), 3 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -685,7 +685,11 @@ TMH_exchange_check_debit ( if (0 != strcasecmp (keys->currency, max_amount->currency)) { - GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Currency missmatch: exchange %s uses %s, we need %s\n", + exchange->url, + keys->currency, + max_amount->currency); return GNUNET_SYSERR; } np = TALER_payto_normalize (wm->payto_uri); diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -246,15 +246,20 @@ test_merchantdb_postgres_LDFLAGS = \ -ljansson \ $(XLIB) +check_SCRIPTS = \ + test_webhook_trigger.sh + test_merchantdb_postgres_LDADD = \ $(top_builddir)/src/util/libtalermerchantutil.la \ $(top_builddir)/src/backenddb/libtalermerchantdb.la TESTS = \ - test-merchantdb-postgres + $(check_PROGRAMS) \ + $(check_SCRIPTS) EXTRA_DIST = \ test-merchantdb-postgres.conf \ merchantdb-postgres.conf \ $(sqlinputs) \ - $(sql_DATA) + $(sql_DATA) \ + $(check_SCRIPTS) diff --git a/src/backenddb/merchant-0013.sql b/src/backenddb/merchant-0013.sql @@ -0,0 +1,495 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 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-0013.sql +-- @brief Creating trigger for the category change webhook +-- @author Bohdan Potuzhnyi +-- @author Vlada Svirsh + + +BEGIN; + +-- Check patch versioning is in place. +SELECT _v.register_patch('merchant-0013', NULL, NULL); + +SET search_path TO merchant; + +-- Function to replace placeholders in a string with a given value +CREATE OR REPLACE FUNCTION replace_placeholder( + template TEXT, + key TEXT, + value TEXT +) RETURNS TEXT AS $$ +BEGIN + RETURN regexp_replace( + template, + '{{\s*' || key || '\s*}}', -- Match the key with optional spaces + value, + 'g' -- Global replacement + ); +END; +$$ LANGUAGE plpgsql; + +-- Trigger function to handle pending webhooks for category changes +CREATE OR REPLACE FUNCTION handle_category_changes() +RETURNS TRIGGER AS $$ +DECLARE + my_merchant_serial BIGINT; + resolved_body TEXT; + webhook RECORD; -- To iterate over all webhooks matching the event type +BEGIN + -- Fetch the merchant_serial directly from the NEW or OLD row + my_merchant_serial := COALESCE(OLD.merchant_serial, NEW.merchant_serial); + + -- INSERT case: Add a webhook for category addition + IF TG_OP = 'INSERT' THEN + FOR webhook IN + SELECT webhook_serial, + merchant_serial, + url, + http_method, + body_template + FROM merchant_webhook + WHERE event_type = 'category_added' + AND merchant_serial = my_merchant_serial + LOOP + -- Resolve placeholders for the current webhook + resolved_body := webhook.body_template; + resolved_body := replace_placeholder(resolved_body, + 'webhook_type', + 'category_added'); + resolved_body := replace_placeholder(resolved_body, + 'category_serial', + NEW.category_serial::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'category_name', + NEW.category_name); + resolved_body := replace_placeholder(resolved_body, + 'merchant_serial', + my_merchant_serial::TEXT); + + -- Insert into pending webhooks for this webhook + INSERT INTO merchant_pending_webhooks + (merchant_serial, webhook_serial, url, http_method, body) + VALUES + (webhook.merchant_serial, + webhook.webhook_serial, + webhook.url, + webhook.http_method, + resolved_body); + END LOOP; + + -- Notify the webhook service for the TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; + + -- UPDATE case: Add a webhook for category update + IF TG_OP = 'UPDATE' THEN + FOR webhook IN + SELECT webhook_serial, + merchant_serial, + url, + http_method, + body_template + FROM merchant_webhook + WHERE event_type = 'category_updated' + AND merchant_serial = my_merchant_serial + LOOP + -- Resolve placeholders for the current webhook + resolved_body := webhook.body_template; + resolved_body := replace_placeholder(resolved_body, + 'webhook_type', + 'category_updated'); + resolved_body := replace_placeholder(resolved_body, + 'category_serial', + NEW.category_serial::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'old_category_name', + OLD.category_name); + resolved_body := replace_placeholder(resolved_body, + 'category_name', + NEW.category_name); + resolved_body := replace_placeholder(resolved_body, + 'category_name_i18n', + encode(NEW.category_name_i18n, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'old_category_name_i18n', + encode(OLD.category_name_i18n, + 'escape')); + + -- Insert into pending webhooks for this webhook + INSERT INTO merchant_pending_webhooks + (merchant_serial, webhook_serial, url, http_method, body) + VALUES + (webhook.merchant_serial, + webhook.webhook_serial, + webhook.url, + webhook.http_method, + resolved_body); + END LOOP; + + -- Notify the webhook service for the TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; + + -- DELETE case: Add a webhook for category deletion + IF TG_OP = 'DELETE' THEN + FOR webhook IN + SELECT webhook_serial, + merchant_serial, + url, + http_method, + body_template + FROM merchant_webhook + WHERE event_type = 'category_deleted' + AND merchant_serial = my_merchant_serial + LOOP + -- Resolve placeholders for the current webhook + resolved_body := webhook.body_template; + resolved_body := replace_placeholder(resolved_body, + 'webhook_type', + 'category_deleted'); + resolved_body := replace_placeholder(resolved_body, + 'category_serial', + OLD.category_serial::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'category_name', + OLD.category_name); + + -- Insert into pending webhooks for this webhook + INSERT INTO merchant_pending_webhooks + (merchant_serial, webhook_serial, url, http_method, body) + VALUES + (webhook.merchant_serial, + webhook.webhook_serial, + webhook.url, + webhook.http_method, + resolved_body); + END LOOP; + + -- Notify the webhook service for the TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to invoke the trigger function +CREATE TRIGGER trigger_category_changes +AFTER INSERT OR UPDATE OR DELETE +ON merchant_categories +FOR EACH ROW +EXECUTE FUNCTION handle_category_changes(); + +-- Function to handle inventory changes and notify webhooks +CREATE OR REPLACE FUNCTION handle_inventory_changes() + RETURNS TRIGGER AS $$ +DECLARE + my_merchant_serial BIGINT; + resolved_body TEXT; + webhook RECORD; -- To iterate over all matching webhooks +BEGIN + -- Fetch the merchant_serial directly from the NEW or OLD row + my_merchant_serial := COALESCE(OLD.merchant_serial, NEW.merchant_serial); + + -- INSERT case: Notify webhooks for inventory addition + IF TG_OP = 'INSERT' THEN + FOR webhook IN + SELECT webhook_serial, + merchant_serial, + url, + http_method, + body_template + FROM merchant_webhook + WHERE event_type = 'inventory_added' + AND merchant_serial = my_merchant_serial + LOOP + -- Resolve placeholders for the current webhook + resolved_body := webhook.body_template; + resolved_body := replace_placeholder(resolved_body, + 'webhook_type', + 'inventory_added'); + resolved_body := replace_placeholder(resolved_body, + 'product_serial', + NEW.product_serial::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'product_id', + NEW.product_id); + resolved_body := replace_placeholder(resolved_body, + 'description', + NEW.description); + resolved_body := replace_placeholder(resolved_body, + 'description_i18n', + encode(NEW.description_i18n, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'unit', + NEW.unit); + resolved_body := replace_placeholder(resolved_body, + 'image', + NEW.image); + resolved_body := replace_placeholder(resolved_body, + 'taxes', + encode(NEW.taxes, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'price', + NEW.price::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_stock', + NEW.total_stock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_sold', + NEW.total_sold::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_lost', + NEW.total_lost::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'address', + encode(NEW.address, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'next_restock', + NEW.next_restock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'minimum_age', + NEW.minimum_age::TEXT); + + -- Insert into pending webhooks for this webhook + INSERT INTO merchant_pending_webhooks + (merchant_serial, webhook_serial, url, http_method, body) + VALUES + (webhook.merchant_serial, + webhook.webhook_serial, + webhook.url, + webhook.http_method, + resolved_body); + END LOOP; + + -- Notify the webhook service + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; + + -- UPDATE case: Notify webhooks for inventory update + IF TG_OP = 'UPDATE' THEN + FOR webhook IN + SELECT webhook_serial, + merchant_serial, + url, + http_method, + body_template + FROM merchant_webhook + WHERE event_type = 'inventory_updated' + AND merchant_serial = my_merchant_serial + LOOP + -- Resolve placeholders for the current webhook + resolved_body := webhook.body_template; + resolved_body := replace_placeholder(resolved_body, + 'webhook_type', + 'inventory_updated'); + resolved_body := replace_placeholder(resolved_body, + 'product_serial', + NEW.product_serial::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'product_id', + NEW.product_id); + resolved_body := replace_placeholder(resolved_body, + 'old_description', + OLD.description); + resolved_body := replace_placeholder(resolved_body, + 'description', + NEW.description); + resolved_body := replace_placeholder(resolved_body, + 'old_description_i18n', + encode(OLD.description_i18n, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'description_i18n', + encode(NEW.description_i18n, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'old_unit', + OLD.unit); + resolved_body := replace_placeholder(resolved_body, + 'unit', + NEW.unit); + resolved_body := replace_placeholder(resolved_body, + 'old_image', + OLD.image); + resolved_body := replace_placeholder(resolved_body, + 'image', + NEW.image); + resolved_body := replace_placeholder(resolved_body, + 'old_taxes', + encode(OLD.taxes, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'taxes', + encode(NEW.taxes, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'old_price', + OLD.price::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'price', + NEW.price::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'old_total_stock', + OLD.total_stock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_stock', + NEW.total_stock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'old_total_sold', + OLD.total_sold::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_sold', + NEW.total_sold::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'old_total_lost', + OLD.total_lost::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_lost', + NEW.total_lost::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'old_address', + encode(OLD.address, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'address', + encode(NEW.address, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'old_next_restock', + OLD.next_restock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'next_restock', + NEW.next_restock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'old_minimum_age', + OLD.minimum_age::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'minimum_age', + NEW.minimum_age::TEXT); + + -- Insert into pending webhooks for this webhook + INSERT INTO merchant_pending_webhooks + (merchant_serial, webhook_serial, url, http_method, body) + VALUES + (webhook.merchant_serial, + webhook.webhook_serial, + webhook.url, + webhook.http_method, + resolved_body); + END LOOP; + + -- Notify the webhook service + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; + + -- DELETE case: Notify webhooks for inventory deletion + IF TG_OP = 'DELETE' THEN + FOR webhook IN + SELECT webhook_serial, + merchant_serial, + url, + http_method, + body_template + FROM merchant_webhook + WHERE event_type = 'inventory_deleted' + AND merchant_serial = my_merchant_serial + LOOP + -- Resolve placeholders for the current webhook + resolved_body := webhook.body_template; + resolved_body := replace_placeholder(resolved_body, + 'webhook_type', + 'inventory_deleted'); + resolved_body := replace_placeholder(resolved_body, + 'product_serial', + OLD.product_serial::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'product_id', + OLD.product_id); + resolved_body := replace_placeholder(resolved_body, + 'description', + OLD.description); + resolved_body := replace_placeholder(resolved_body, + 'description_i18n', + encode(OLD.description_i18n, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'unit', + OLD.unit); + resolved_body := replace_placeholder(resolved_body, + 'image', + OLD.image); + resolved_body := replace_placeholder(resolved_body, + 'taxes', + encode(OLD.taxes, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'price', + OLD.price::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_stock', + OLD.total_stock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_sold', + OLD.total_sold::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'total_lost', + OLD.total_lost::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'address', + encode(OLD.address, + 'escape')); + resolved_body := replace_placeholder(resolved_body, + 'next_restock', + OLD.next_restock::TEXT); + resolved_body := replace_placeholder(resolved_body, + 'minimum_age', + OLD.minimum_age::TEXT); + + -- Insert into pending webhooks for this webhook + INSERT INTO merchant_pending_webhooks + (merchant_serial, webhook_serial, url, http_method, body) + VALUES + (webhook.merchant_serial, + webhook.webhook_serial, + webhook.url, + webhook.http_method, + resolved_body); + END LOOP; + + -- Notify the webhook service + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to invoke the trigger function +CREATE TRIGGER trigger_inventory_changes + AFTER INSERT OR UPDATE OR DELETE + ON merchant_inventory + FOR EACH ROW +EXECUTE FUNCTION handle_inventory_changes(); + + +COMMIT; diff --git a/src/backenddb/pg_insert_deposit_to_transfer.sql b/src/backenddb/pg_insert_deposit_to_transfer.sql @@ -35,6 +35,10 @@ DECLARE my_decose INT8; DECLARE my_order_serial INT8; +DECLARE + my_merchant_serial INT8; +DECLARE + my_order_id TEXT; BEGIN -- Find exchange sign key @@ -157,4 +161,20 @@ UPDATE merchant_contract_terms WHERE mdc.wire_pending AND mdc.order_serial=my_order_serial); +-- POSSIBLE LOCATION FOR THE WIRE WEBHOOK OF ORDER +-- +-- INSERT INTO merchant_pending_webhooks +-- (merchant_serial +-- ,webhook_serial +-- ,url +-- ,http_method +-- ,body) +-- SELECT mw.merchant_serial +-- ,mw.webhook_serial +-- ,mw.url +-- ,mw.http_method +-- ,json_build_object('order_id', my_order_id)::TEXT +-- FROM merchant_webhook mw +-- WHERE mw.event_type = 'order_settled' +-- AND mw.merchant_serial = my_merchant_serial; END $$; diff --git a/src/backenddb/pg_insert_transfer_details.sql b/src/backenddb/pg_insert_transfer_details.sql @@ -41,6 +41,8 @@ DECLARE my_signkey_serial INT8; my_credit_serial INT8; my_affected_orders RECORD; + my_merchant_serial INT8; + my_order_id TEXT; i INT8; curs CURSOR (arg_coin_pub BYTEA) FOR SELECT mcon.deposit_confirmation_serial, @@ -220,6 +222,29 @@ LOOP FROM merchant_deposit_confirmations mdc WHERE mdc.wire_pending AND mdc.order_serial=my_affected_orders.order_serial); + + -- Select merchant_serial and order_id for webhook + SELECT merchant_serial, order_id + INTO my_merchant_serial, my_order_id + FROM merchant_contract_terms + WHERE order_serial=my_affected_orders.order_serial; + + -- Insert pending webhook if it exists + INSERT INTO merchant_pending_webhooks + (merchant_serial + ,webhook_serial + ,url + ,http_method + ,body) + SELECT mw.merchant_serial + ,mw.webhook_serial + ,mw.url + ,mw.http_method + ,replace_placeholder(mw.body_template, 'order_id', my_order_id)::TEXT + FROM merchant_webhook mw + WHERE mw.event_type = 'order_settled' + AND mw.merchant_serial = my_merchant_serial; + END IF; END LOOP; -- END curs LOOP diff --git a/src/backenddb/test_webhook_trigger.sh b/src/backenddb/test_webhook_trigger.sh @@ -0,0 +1,369 @@ +#!/bin/bash +# This file is in the public domain. + +set -eu + +echo -n "Test if OpenSSL is available..." +openssl &>/dev/null || (echo " No!" ; exit 77 ) +echo " OK" + +taler-merchant-dbinit -r -c test-merchantdb-postgres.conf + +# Generate random BYTEA values for testing +WTID=$(openssl rand -hex 32) +EXCHANGE_PUB=$(openssl rand -hex 32) +EXCHANGE_SIG=$(openssl rand -hex 64) +COIN_PUB=$(openssl rand -hex 32) + +# Generate valid BYTEA values +MERCHANT_PUB=$(openssl rand -hex 32) +AUTH_HASH=$(openssl rand -hex 64) +AUTH_SALT=$(openssl rand -hex 32) + +# Define other parameters +INSTANCE_ID="test-merchant" +EXCHANGE_URL="http://example.com/exchange" +PAYTO_URI="payto://example.com/account" +EXECUTION_TIME=$(date +%s) + +# Generate taler_amount_currency values +TOTAL_AMOUNT="ROW(100, 0, 'USD')::taler_amount_currency" +WIRE_FEE="ROW(1, 0, 'USD')::taler_amount_currency" + +# Generate arrays for taler_amount_currency and BYTEA +# Generate arrays for taler_amount_currency and BYTEA +COIN_VALUES="ARRAY[ROW(10, 0, 'USD')::taler_amount_currency, ROW(20, 0, 'USD')::taler_amount_currency, ROW(30, 0, 'USD')::taler_amount_currency]::taler_amount_currency[]" +DEPOSIT_FEES="ARRAY[ROW(1, 0, 'USD')::taler_amount_currency, ROW(2, 50000000, 'USD')::taler_amount_currency, ROW(3, 25000000, 'USD')::taler_amount_currency]::taler_amount_currency[]" +COIN_PUBS="ARRAY[decode('$COIN_PUB', 'hex'), decode('$(openssl rand -hex 32)', 'hex'), decode('$(openssl rand -hex 32)', 'hex')]::BYTEA[]" +CONTRACT_TERMS="ARRAY[decode('$(openssl rand -hex 32)', 'hex'), decode('$(openssl rand -hex 32)', 'hex'), decode('$(openssl rand -hex 32)', 'hex')]::BYTEA[]" + +# Create the merchant database +echo "SET search_path TO merchant; + INSERT INTO merchant_instances ( + merchant_serial, + merchant_name, + merchant_id, + address, + jurisdiction, + default_wire_transfer_delay, + default_pay_delay, + merchant_pub, + auth_hash, + auth_salt + ) VALUES ( + 1, + 'Test Merchant', + 'test-merchant', + '{"city":"TestCity"}', + '{"country":"TestCountry"}', + 3600, + 3600, + decode('$MERCHANT_PUB', 'hex'), + decode('$AUTH_HASH', 'hex'), + decode('$AUTH_SALT', 'hex') + );" | psql talercheck + +# Create the webhooks +echo "SET search_path TO merchant; + INSERT INTO merchant_webhook (merchant_serial, webhook_id, event_type, url, http_method, body_template) + VALUES + (1, 'category_added_hook', 'category_added', 'http://example.com/category_added', 'POST', '{"event":"{{ webhook_type }}"}'), + (1, 'category_updated_hook', 'category_updated', 'http://example.com/category_updated', 'POST', '{"event":"{{ webhook_type }}"}'), + (1, 'category_deleted_hook', 'category_deleted', 'http://example.com/category_deleted', 'POST', '{"event":"{{ webhook_type }}"}'), + (1, 'inventory_added_hook', 'inventory_added', 'http://example.com/inventory_added', 'POST', '{"event":"{{ webhook_type }}"}'), + (1, 'inventory_updated_hook', 'inventory_updated', 'http://example.com/inventory_updated', 'POST', '{"event":"{{ webhook_type }}"}'), + (1, 'inventory_deleted_hook', 'inventory_deleted', 'http://example.com/inventory_deleted', 'POST', '{"event":"{{ webhook_type }}"}'), + (1, 'order_settled_hook', 'order_settled', 'http://example.com/order_settled', 'POST', '{"event":"{{ webhook_type }}"}');" | psql talercheck + +# Create the category +echo "SET search_path TO merchant; + INSERT INTO merchant_categories (merchant_serial, category_name, category_name_i18n) + VALUES (1, 'Test Category', '{"en":"Test Category"}');" | psql talercheck + +# Update the category +echo "SET search_path TO merchant; + UPDATE merchant_categories + SET category_name = 'Updated Category' + WHERE category_name = 'Test Category';" | psql talercheck + +# Delete the category +echo "SET search_path TO merchant; + DELETE FROM merchant_categories + WHERE category_name = 'Updated Category';" | psql talercheck + +# Add product to inventory +echo "SET search_path TO merchant; + INSERT INTO merchant_inventory (merchant_serial, product_id, description, description_i18n, unit, image, taxes, price, total_stock, address, next_restock) + VALUES + (1, 'test-product', 'Test Product', '{"en":"Test Product"}', 'unit', 'image-data-url', '[]', ROW(10, 0, 'USD'), 100, '{"city":"TestCity"}', 0);" | psql talercheck + +# Update product in inventory +echo "SET search_path TO merchant; + UPDATE merchant_inventory + SET description = 'Updated Test Product' + WHERE product_id = 'test-product';" | psql talercheck + +# Delete product from inventory +echo "SET search_path TO merchant; + DELETE FROM merchant_inventory + WHERE product_id = 'test-product';" | psql talercheck + +# Create the merchant account +echo "SET search_path TO merchant; + INSERT INTO merchant_accounts ( + merchant_serial, + h_wire, + salt, + credit_facade_url, + credit_facade_credentials, + last_bank_serial, + payto_uri, + active + ) VALUES ( + 1, + decode('$(openssl rand -hex 64)', 'hex'), + decode('$(openssl rand -hex 16)', 'hex'), + 'http://example.com/credit', + '{"key":"value"}', + 0, + '$PAYTO_URI', + TRUE + );" | psql talercheck + +# Create the merchant transfer +echo "SET search_path TO merchant; + INSERT INTO merchant_transfers ( + credit_serial, + exchange_url, + wtid, + credit_amount, + account_serial + ) VALUES ( + 1, + 'http://example.com/exchange', + decode('$WTID', 'hex'), + ROW(100, 0, 'USD')::taler_amount_currency, + 1 + );" | psql talercheck + +# Create the exchange signing key +echo "SET search_path TO merchant; + INSERT INTO merchant_exchange_signing_keys ( + master_pub, + exchange_pub, + start_date, + expire_date, + end_date, + master_sig + ) VALUES ( + decode('$(openssl rand -hex 32)', 'hex'), + decode('$EXCHANGE_PUB', 'hex'), + EXTRACT(EPOCH FROM NOW()), + EXTRACT(EPOCH FROM NOW()) + 3600, + EXTRACT(EPOCH FROM NOW()) + 7200, + decode('$(openssl rand -hex 64)', 'hex') + );" | psql talercheck + +# Create the merchant_contract_terms +echo "SET search_path TO merchant; + INSERT INTO merchant_contract_terms ( + order_serial, + merchant_serial, + order_id, + contract_terms, + wallet_data, + h_contract_terms, + creation_time, + pay_deadline, + refund_deadline, + paid, + wired, + fulfillment_url, + session_id, + pos_key, + pos_algorithm, + claim_token + ) VALUES ( + 1001, + 1, + 'order-1234', + decode('$(openssl rand -hex 64)', 'hex'), + '{"wallet":"test"}', + decode('$(openssl rand -hex 64)', 'hex'), + EXTRACT(EPOCH FROM NOW()), + EXTRACT(EPOCH FROM NOW()) + 3600, + EXTRACT(EPOCH FROM NOW()) + 7200, + FALSE, + FALSE, + 'http://example.com/fulfillment', + '', + NULL, + 0, + decode('$(openssl rand -hex 16)', 'hex') + );" | psql talercheck + +echo "SET search_path TO merchant; + INSERT INTO merchant_deposit_confirmations ( + order_serial, + deposit_timestamp, + exchange_url, + total_without_fee, + wire_fee, + signkey_serial, + exchange_sig, + account_serial, + wire_pending + ) VALUES ( + 1001, + EXTRACT(EPOCH FROM NOW()), + '$EXCHANGE_URL', + ROW(90, 0, 'USD')::taler_amount_currency, + ROW(10, 0, 'USD')::taler_amount_currency, + 1, + decode('$EXCHANGE_SIG', 'hex'), + 1, + TRUE + );" | psql talercheck + +echo "SET search_path TO merchant; + UPDATE merchant_contract_terms + SET wired = TRUE + WHERE order_serial = 1001 + AND NOT EXISTS ( + SELECT 1 + FROM merchant_deposit_confirmations + WHERE wire_pending + AND order_serial = 1001 + );" | psql talercheck + +# Handle deposit confirmation +echo "SET search_path TO merchant; + INSERT INTO merchant_deposit_confirmations ( + order_serial, + deposit_timestamp, + exchange_url, + total_without_fee, + wire_fee, + signkey_serial, + exchange_sig, + account_serial, + wire_pending + ) VALUES ( + 1001, + EXTRACT(EPOCH FROM NOW()), + 'http://example.com/exchange', + ROW(90, 0, 'USD')::taler_amount_currency, + ROW(10, 0, 'USD')::taler_amount_currency, + 1, + decode('$(openssl rand -hex 64)', 'hex'), + 1, + FALSE + );" | psql talercheck + +echo "SET search_path TO merchant; + UPDATE merchant_contract_terms + SET wired = TRUE + WHERE order_serial = 1001 + AND NOT EXISTS ( + SELECT 1 + FROM merchant_deposit_confirmations + WHERE wire_pending + AND order_serial = 1001 + );" | psql talercheck + +# Add entries to merchant_deposits +echo "SET search_path TO merchant; + INSERT INTO merchant_deposits ( + deposit_serial, + coin_offset, + deposit_confirmation_serial, + coin_pub, + coin_sig, + amount_with_fee, + deposit_fee, + refund_fee + ) VALUES ( + (SELECT COALESCE(MAX(deposit_serial), 0) + 1 FROM merchant_deposits), -- deposit_serial + 0, -- coin_offset + 1, -- deposit_confirmation_serial (link to confirmation) + decode('$COIN_PUB', 'hex'), -- coin_pub + decode('$(openssl rand -hex 64)', 'hex'), -- coin_sig + ROW(30, 0, 'USD')::taler_amount_currency, -- amount_with_fee + ROW(1, 0, 'USD')::taler_amount_currency, -- deposit_fee + ROW(0, 0, 'USD')::taler_amount_currency -- refund_fee + );" | psql talercheck + +# Link deposits to wire transfers +echo "SET search_path TO merchant; + INSERT INTO merchant_deposit_to_transfer ( + deposit_serial, + coin_contribution_value, + execution_time, + signkey_serial, + exchange_sig, + wtid + ) VALUES ( + (SELECT MAX(deposit_serial) FROM merchant_deposits), -- deposit_serial + ROW(10, 0, 'USD')::taler_amount_currency, -- coin_contribution_value + $EXECUTION_TIME, -- execution_time + 1, -- signkey_serial + decode('$(openssl rand -hex 64)', 'hex'), -- exchange_sig + decode('$WTID', 'hex') -- wtid + );" | psql talercheck + + +# Debug the UPDATE condition +echo "SET search_path TO merchant; + SELECT md.deposit_confirmation_serial, mdtt.wtid + FROM merchant_deposits md + LEFT JOIN merchant_deposit_to_transfer mdtt + USING (deposit_serial) + WHERE md.deposit_confirmation_serial=1 + AND mdtt.wtid IS NULL;" | psql talercheck + +# Re-run the update logic +echo "SET search_path TO merchant; + UPDATE merchant_deposit_confirmations + SET wire_pending=FALSE + WHERE deposit_confirmation_serial=1 + AND NOT EXISTS + (SELECT 1 + FROM merchant_deposits md + LEFT JOIN merchant_deposit_to_transfer mdtt + USING (deposit_serial) + WHERE md.deposit_confirmation_serial=1 + AND mdtt.wtid IS NULL);" | psql talercheck + + +echo "SET search_path TO merchant; + SELECT merchant_do_insert_transfer_details( + '$INSTANCE_ID', + '$EXCHANGE_URL', + '$PAYTO_URI', + decode('$WTID', 'hex')::BYTEA, + $EXECUTION_TIME::BIGINT, + decode('$EXCHANGE_PUB', 'hex')::BYTEA, + decode('$EXCHANGE_SIG', 'hex')::BYTEA, + $TOTAL_AMOUNT, + $WIRE_FEE, + $COIN_VALUES, + $DEPOSIT_FEES, + $COIN_PUBS, + $CONTRACT_TERMS + );" | psql talercheck + +# Echo all webhooks +echo "SET search_path TO merchant; + SELECT * FROM merchant_pending_webhooks;" | psql talercheck + +# Fetch and validate the number of pending webhooks +WEBHOOK_COUNT=$(echo "SET search_path TO merchant; + SELECT COUNT(*) + FROM merchant_pending_webhooks;" | psql talercheck -t -A | tr -d '[:space:]') + +if [ "$WEBHOOK_COUNT" -ne 7 ]; then + echo "ERROR: Expected 7 webhook entries, but received $WEBHOOK_COUNT." + exit 1 +else + echo "SUCCESS: Received 7 webhook entries as expected." +fi