merchant

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

commit 2d50f9bdd6a89f7a75acab039791acb3ede40f73
parent 9d7b48f002a81a015f02de7972fe1f4bbcbc75a6
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue,  2 Jun 2026 13:41:22 +0200

Merging 'insanity' branch.

This migrates all per-instance tables into their own schema.
Setting the search_path (via set_instance()) is now required
prior to accessing per-instance data. Global data remains
in the "merchant" schema. The "merchant-instances" schema
serves to hold the per-instance stored procedures.
Instead of using named prepared statements, we now use
anonymous prepared statements (one-shot) as prepared statements
do not work well with a changing search_path (the SQL is bound
to the current search_path on first preparation).

This simplifies many SQL statements by avoiding the
"WHERE instance-matches" clause entirely.

Future impact:
- #9453: this will enable per-instance data export by simply
  exporting the respective schema (plus one entry from the
  merchant_instances) table, giving a clear path towards
  implementing data portability.
- In the future, this should also enable us to implement
  tighter access control on the database by dropping rights
  to a per-instance role instead of always acting as the
  "taler-merchant-httpd" role.

Note that this change comes with a MAJOR database migration
transaction that is likely to blow through some default
PostgreSQL limits, so if taler-merchant-dbinit fails, the
solution is likely to increase limits. See README for
details.

Diffstat:
MREADME | 20+++++++++++++++++++-
Msrc/backend/taler-merchant-depositcheck.c | 14++++++++++++++
Msrc/backend/taler-merchant-httpd.c | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/backend/taler-merchant-httpd_delete-private-units-UNIT.c | 7-------
Msrc/backend/taler-merchant-httpd_exchanges.c | 13+++++++++++++
Msrc/backend/taler-merchant-httpd_get-private-accounts.c | 11++++++-----
Msrc/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.c | 10----------
Msrc/backend/taler-merchant-httpd_patch-private-units-UNIT.c | 10----------
Msrc/backend/taler-merchant-httpd_post-management-instances.c | 53++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c | 40+++++++++++++++++++++++++++++-----------
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 21++++++++++++++++++---
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.c | 33++++++++++++++++++++++++++-------
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c | 1-
Msrc/backend/taler-merchant-httpd_post-private-accounts.c | 83+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/backend/taler-merchant-httpd_post-private-donau.c | 30++++++++++++++++++++++--------
Msrc/backend/taler-merchant-httpd_post-private-orders.c | 19++++++++-----------
Msrc/backend/taler-merchant-httpd_post-private-products.c | 11-----------
Msrc/backend/taler-merchant-httpd_post-private-transfers.c | 13-------------
Msrc/backend/taler-merchant-httpd_post-private-units.c | 10----------
Msrc/backend/taler-merchant-kyccheck.c | 37++++++++++++++++++++++++++++++++++++-
Msrc/backend/taler-merchant-reconciliation.c | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-report-generator.c | 13+++++++++++++
Msrc/backend/taler-merchant-wirewatch.c | 39++++++++++++++++++++++++++++++---------
Msrc/backenddb/account_kyc_get_outdated.c | 29++++++++++++-----------------
Asrc/backenddb/account_kyc_get_outdated.sql | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/account_kyc_get_status.c | 35+++++++++++++++++------------------
Asrc/backenddb/account_kyc_get_status.sql | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/account_kyc_set_failed.c | 38+++++++++++++++++---------------------
Asrc/backenddb/account_kyc_set_failed.sql | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/account_kyc_set_status.c | 54+++++++++++++++++++++++++-----------------------------
Asrc/backenddb/account_kyc_set_status.sql | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/activate_account.c | 38+++++++++++++++++++-------------------
Asrc/backenddb/activate_account.sql | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/check_donau_instance.c | 33+++++++++++++++------------------
Msrc/backenddb/check_money_pots.c | 39+++++++++++++++++++--------------------
Msrc/backenddb/check_report.c | 31+++++++++++++------------------
Asrc/backenddb/check_report.sql | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/check_transfer_exists.c | 36++++++++++++++----------------------
Msrc/backenddb/create_mfa_challenge.c | 16++++++++--------
Msrc/backenddb/create_tables.c | 24+++++++++++++++++++++++-
Asrc/backenddb/create_tables.sql | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/delete_category.c | 28++++++++++++----------------
Msrc/backenddb/delete_contract_terms.c | 35++++++++++++++++-------------------
Msrc/backenddb/delete_donau_instance.c | 29+++++++++++++----------------
Msrc/backenddb/delete_exchange_accounts.c | 7+++----
Msrc/backenddb/delete_instance_private_key.c | 20+++++++++-----------
Msrc/backenddb/delete_login_token.c | 53++++++++++++++++++++++++-----------------------------
Msrc/backenddb/delete_money_pot.c | 28++++++++++++----------------
Msrc/backenddb/delete_order.c | 68+++++++++++++++++++++++++++++---------------------------------------
Msrc/backenddb/delete_otp.c | 28++++++++++++----------------
Msrc/backenddb/delete_pending_webhook.c | 9+++++----
Msrc/backenddb/delete_product.c | 37+++++++++++++++++--------------------
Msrc/backenddb/delete_product_group.c | 29+++++++++++++----------------
Msrc/backenddb/delete_report.c | 28++++++++++++----------------
Msrc/backenddb/delete_template.c | 28++++++++++++----------------
Msrc/backenddb/delete_token_family.c | 32++++++++++++++------------------
Msrc/backenddb/delete_transfer.c | 31+++++++++++--------------------
Msrc/backenddb/delete_unit.c | 45+++++++++++++++++++--------------------------
Asrc/backenddb/delete_unit.sql | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/delete_webhook.c | 30+++++++++++++-----------------
Msrc/backenddb/drop_tables.c | 1+
Msrc/backenddb/example-statistics-0001.sql | 5++---
Msrc/backenddb/expire_locks.c | 62+++++++++++++++++++-------------------------------------------
Asrc/backenddb/expire_locks.sql | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/finalize_transfer_status.c | 63+++++++++++++++++++++++++++++++--------------------------------
Msrc/backenddb/future.sql | 2+-
Msrc/backenddb/gc.c | 4+---
Asrc/backenddb/gc.sql | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/get_kyc_limits.c | 51++++++++++++++++++++++++---------------------------
Msrc/backenddb/get_kyc_status.c | 80++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/helper.h | 46+++++++++++++++++++++++++++++++++++++++++++++-
Msrc/backenddb/inactivate_account.c | 25+++++++++++++------------
Asrc/backenddb/inactivate_account.sql | 36++++++++++++++++++++++++++++++++++++
Msrc/backenddb/increase_refund.c | 128++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/backenddb/increment_money_pots.c | 28+++++++++++++++-------------
Asrc/backenddb/increment_money_pots.sql | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_account.c | 49++++++++++++++++++++++++-------------------------
Msrc/backenddb/insert_category.c | 40++++++++++++++++++----------------------
Msrc/backenddb/insert_contract_terms.c | 83+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/backenddb/insert_deposit.c | 50++++++++++++++++++++++++--------------------------
Msrc/backenddb/insert_deposit_confirmation.c | 64+++++++++++++++++++++++++++-------------------------------------
Asrc/backenddb/insert_deposit_confirmation.sql | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_deposit_to_transfer.c | 27+++++++++++++--------------
Asrc/backenddb/insert_deposit_to_transfer.sql | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_donau_instance.c | 49++++++++++++++++++++++++-------------------------
Msrc/backenddb/insert_exchange_account.c | 17++++++++---------
Msrc/backenddb/insert_exchange_keys.c | 7+++----
Msrc/backenddb/insert_exchange_signkey.c | 20++++++++++----------
Msrc/backenddb/insert_instance.c | 35+++++++++--------------------------
Msrc/backenddb/insert_issued_token.c | 31++++++++++++++-----------------
Rsrc/backenddb/pg_insert_issued_token.sql -> src/backenddb/insert_issued_token.sql | 0
Msrc/backenddb/insert_login_token.c | 47++++++++++++++++++++++-------------------------
Msrc/backenddb/insert_money_pot.c | 37+++++++++++++++++--------------------
Msrc/backenddb/insert_order.c | 62++++++++++++++++++++++++++++++--------------------------------
Msrc/backenddb/insert_order_blinded_sigs.c | 39+++++++++++++++++++--------------------
Msrc/backenddb/insert_order_lock.c | 112++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/insert_otp.c | 40++++++++++++++++++----------------------
Msrc/backenddb/insert_pending_webhook.c | 32++++++++++++++++----------------
Msrc/backenddb/insert_product.c | 74+++++++++++++++++++++++++++++++++++---------------------------------------
Asrc/backenddb/insert_product.sql | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_product_group.c | 37+++++++++++++++++--------------------
Msrc/backenddb/insert_refund_proof.c | 37++++++++++++++++++-------------------
Msrc/backenddb/insert_report.c | 39++++++++++++++++++---------------------
Msrc/backenddb/insert_spent_token.c | 32++++++++++++++++----------------
Rsrc/backenddb/pg_insert_spent_token.sql -> src/backenddb/insert_spent_token.sql | 0
Msrc/backenddb/insert_template.c | 43++++++++++++++++++++-----------------------
Msrc/backenddb/insert_token_family.c | 57+++++++++++++++++++++++++++------------------------------
Msrc/backenddb/insert_token_family_key.c | 58+++++++++++++++++++++++++++-------------------------------
Msrc/backenddb/insert_transfer.c | 44++++++++++++++++++++------------------------
Asrc/backenddb/insert_transfer.sql | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_transfer_details.c | 61+++++++++++++++++++++++++++----------------------------------
Asrc/backenddb/insert_transfer_details.sql | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_unclaim_signature.c | 42+++++++++++++++++++++++++++---------------
Asrc/backenddb/insert_unclaim_signature.sql | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_unit.c | 40+++++++++++++++++-----------------------
Asrc/backenddb/insert_unit.sql | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/insert_webhook.c | 43++++++++++++++++++++-----------------------
Msrc/backenddb/lock_product.c | 99++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/backenddb/lookup_account.c | 37+++++++++++++++++--------------------
Msrc/backenddb/lookup_all_products.c | 81++++++++++++++++++++++++++++++++++++++-----------------------------------------
Asrc/backenddb/lookup_all_webhooks.c | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/lookup_categories.c | 45+++++++++++++++++++++------------------------
Msrc/backenddb/lookup_categories_by_ids.c | 38+++++++++++++++++---------------------
Msrc/backenddb/lookup_contract_terms.c | 44++++++++++++++++++++------------------------
Msrc/backenddb/lookup_contract_terms2.c | 53+++++++++++++++++++++++++----------------------------
Msrc/backenddb/lookup_contract_terms3.c | 67++++++++++++++++++++++++++++++-------------------------------------
Msrc/backenddb/lookup_custom_units_by_names.c | 52+++++++++++++++++++++++++---------------------------
Msrc/backenddb/lookup_deposits.c | 41++++++++++++++++++-----------------------
Msrc/backenddb/lookup_deposits_by_contract_and_coin.c | 92+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/backenddb/lookup_deposits_by_order.c | 47++++++++++++++++++++++-------------------------
Msrc/backenddb/lookup_donau_keys.c | 12++++++------
Msrc/backenddb/lookup_expected_transfer.c | 71++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/backenddb/lookup_expected_transfers.c | 179++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_instance_auth.c | 12++++++------
Msrc/backenddb/lookup_instances.c | 189+++++++++++++++++++++++++++----------------------------------------------------
Msrc/backenddb/lookup_inventory_products.c | 122++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_inventory_products_filtered.c | 146++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_login_tokens.c | 89++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_mfa_challenge.c | 8++++----
Msrc/backenddb/lookup_order.c | 27+++++++++++----------------
Msrc/backenddb/lookup_order_by_fulfillment.c | 62+++++++++++++++++++++++++++++---------------------------------
Msrc/backenddb/lookup_order_charity.c | 41+++++++++++++++--------------------------
Msrc/backenddb/lookup_order_status.c | 53+++++++++++++++++++++++------------------------------
Msrc/backenddb/lookup_order_status_by_serial.c | 41+++++++++++++++++++----------------------
Msrc/backenddb/lookup_order_summary.c | 53+++++++++++++++++++++++------------------------------
Msrc/backenddb/lookup_orders.c | 262++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/backenddb/lookup_otp_devices.c | 22+++++++++-------------
Msrc/backenddb/lookup_pending_deposits.c | 62+++++++++++++++++++++-----------------------------------------
Asrc/backenddb/lookup_pending_deposits.sql | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/lookup_pending_webhooks.c | 112++++++++++++++++++-------------------------------------------------------------
Msrc/backenddb/lookup_product.c | 97++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_product_image.c | 38+++++++++++++++-----------------------
Msrc/backenddb/lookup_products.c | 112++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_reconciliation_details.c | 46+++++++++++++++++++++-------------------------
Msrc/backenddb/lookup_refund_proof.c | 35+++++++++++++++++------------------
Msrc/backenddb/lookup_refunds.c | 42+++++++++++++++++++-----------------------
Msrc/backenddb/lookup_refunds_detailed.c | 55+++++++++++++++++++++++++------------------------------
Msrc/backenddb/lookup_reports_pending.c | 52+++++++++++++++++++++++-----------------------------
Asrc/backenddb/lookup_reports_pending.sql | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/lookup_spent_tokens_by_order.c | 35++++++++++++++++-------------------
Msrc/backenddb/lookup_statistics_amount_by_bucket.c | 38+++++++++++++++++---------------------
Msrc/backenddb/lookup_statistics_amount_by_bucket2.c | 42+++++++++++++++++++-----------------------
Msrc/backenddb/lookup_statistics_amount_by_interval.c | 26++++++++++++--------------
Asrc/backenddb/lookup_statistics_amount_by_interval.sql | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/lookup_statistics_counter_by_bucket.c | 36++++++++++++++++--------------------
Msrc/backenddb/lookup_statistics_counter_by_bucket2.c | 40++++++++++++++++++----------------------
Msrc/backenddb/lookup_statistics_counter_by_interval.c | 29+++++++++++++++--------------
Asrc/backenddb/lookup_statistics_counter_by_interval.sql | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/lookup_template.c | 104++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/backenddb/lookup_templates.c | 31++++++++++++++-----------------
Msrc/backenddb/lookup_token_families.c | 32++++++++++++++------------------
Msrc/backenddb/lookup_token_family.c | 177++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/backenddb/lookup_token_family_key.c | 252+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/backenddb/lookup_token_family_keys.c | 66+++++++++++++++++++++++++++++++-----------------------------------
Msrc/backenddb/lookup_transfer_details.c | 42++++++++++++++++++++----------------------
Msrc/backenddb/lookup_transfer_details_by_order.c | 53+++++++++++++++++++++++++----------------------------
Msrc/backenddb/lookup_transfer_summary.c | 38++++++++++++++++++--------------------
Msrc/backenddb/lookup_transfers.c | 144++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/backenddb/lookup_units.c | 80+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/backenddb/lookup_webhook.c | 44+++++++++++++++++++++-----------------------
Msrc/backenddb/lookup_webhook_by_event.c | 43++++++++++++++++++++-----------------------
Msrc/backenddb/lookup_webhooks.c | 31++++++++++++++-----------------
Msrc/backenddb/lookup_wire_fee.c | 22+++++++++++-----------
Msrc/backenddb/mark_contract_paid.c | 104++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/backenddb/mark_order_wired.c | 21++++++++++-----------
Msrc/backenddb/meson.build | 3+++
Msrc/backenddb/pg.c | 11++++++++---
Dsrc/backenddb/pg_account_kyc_get_status.sql | 124-------------------------------------------------------------------------------
Dsrc/backenddb/pg_account_kyc_set_failed.sql | 100-------------------------------------------------------------------------------
Dsrc/backenddb/pg_account_kyc_set_status.sql | 127-------------------------------------------------------------------------------
Dsrc/backenddb/pg_activate_account.sql | 146-------------------------------------------------------------------------------
Msrc/backenddb/pg_base32_crockford.sql | 16++++++++--------
Asrc/backenddb/pg_create_instance_schema.sql | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_create_instance_trigger.sql | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_do_handle_category_changes.sql | 47++++++++++++++++++++++-------------------------
Msrc/backenddb/pg_do_handle_inventory_changes.sql | 134++++++++++++++++++++++++++++++++++++++-----------------------------------------
Asrc/backenddb/pg_fixup_instance_schema.sql | 48++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/backenddb/pg_inactivate_account.sql | 49-------------------------------------------------
Dsrc/backenddb/pg_increment_money_pots.sql | 109-------------------------------------------------------------------------------
Dsrc/backenddb/pg_insert_deposit_confirmation.sql | 171-------------------------------------------------------------------------------
Dsrc/backenddb/pg_insert_deposit_to_transfer.sql | 136-------------------------------------------------------------------------------
Dsrc/backenddb/pg_insert_product.sql | 246-------------------------------------------------------------------------------
Dsrc/backenddb/pg_insert_transfer.sql | 122-------------------------------------------------------------------------------
Dsrc/backenddb/pg_insert_transfer_details.sql | 269-------------------------------------------------------------------------------
Dsrc/backenddb/pg_insert_unclaim_signature.sql | 85-------------------------------------------------------------------------------
Asrc/backenddb/pg_interval_to_start.sql | 29+++++++++++++++++++++++++++++
Msrc/backenddb/pg_merchant_send_kyc_notification.sql | 2+-
Asrc/backenddb/pg_replace_placeholder.sql | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_select_accounts.sql | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/backenddb/pg_solve_mfa_challenge.sql | 84-------------------------------------------------------------------------------
Msrc/backenddb/pg_statistics_examples.sql | 56+++++++++++++++++++-------------------------------------
Msrc/backenddb/pg_statistics_helpers.sql | 418+++----------------------------------------------------------------------------
Dsrc/backenddb/pg_update_money_pot.sql | 82-------------------------------------------------------------------------------
Dsrc/backenddb/pg_update_product.sql | 199-------------------------------------------------------------------------------
Dsrc/backenddb/pg_update_product_group.sql | 55-------------------------------------------------------
Msrc/backenddb/pg_uri_escape.sql | 16+++++++++-------
Msrc/backenddb/purge_instance.c | 12++++++------
Msrc/backenddb/refund_coin.c | 76+++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/backenddb/select_account.c | 42+++++++++++++++++++-----------------------
Msrc/backenddb/select_account_by_uri.c | 40++++++++++++++++++----------------------
Msrc/backenddb/select_accounts.c | 54+++++++++++++++++++-----------------------------------
Msrc/backenddb/select_accounts_by_exchange.c | 2--
Asrc/backenddb/select_accounts_by_instance.c | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/select_all_donau_instances.c | 44+++++++++++++++++++-------------------------
Asrc/backenddb/select_all_donau_instances.sql | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/select_category.c | 108++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/backenddb/select_category_by_name.c | 35++++++++++++++++-------------------
Msrc/backenddb/select_donau_instance_by_serial.c | 25++++++++++++-------------
Msrc/backenddb/select_donau_instances.c | 57+++++++++++++++++++++++++++++++--------------------------
Msrc/backenddb/select_donau_instances_filtered.c | 18++++++------------
Msrc/backenddb/select_exchange_keys.c | 4+---
Msrc/backenddb/select_exchanges.c | 4+---
Msrc/backenddb/select_login_token.c | 36++++++++++++++++--------------------
Msrc/backenddb/select_money_pot.c | 41+++++++++++++++++++----------------------
Msrc/backenddb/select_money_pots.c | 73+++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/backenddb/select_open_transfers.c | 35+++++++++++++----------------------
Asrc/backenddb/select_open_transfers.sql | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/select_order_blinded_sigs.c | 32+++++++++++++++-----------------
Msrc/backenddb/select_otp.c | 91+++++++++++++++++++++++++++++++------------------------------------------------
Msrc/backenddb/select_otp_serial.c | 31++++++++++++++-----------------
Msrc/backenddb/select_product_groups.c | 73+++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/backenddb/select_report.c | 68+++++++++++++++++++++++++++++++++-----------------------------------
Msrc/backenddb/select_reports.c | 64++++++++++++++++++++++++++++++----------------------------------
Msrc/backenddb/select_unit.c | 168+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/backenddb/select_wirewatch_accounts.c | 28+++++++++++-----------------
Asrc/backenddb/select_wirewatch_accounts.sql | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/set_instance.c | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/solve_mfa_challenge.c | 17+++++++----------
Asrc/backenddb/solve_mfa_challenge.sql | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/sql-schema/amalgamate-sql.sh | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/sql-schema/drop.sql | 15+++++++++++++++
Msrc/backenddb/sql-schema/gen-procedures.sh | 37+++++++------------------------------
Msrc/backenddb/sql-schema/merchant-0013.sql | 16----------------
Msrc/backenddb/sql-schema/merchant-0027.sql | 304-------------------------------------------------------------------------------
Asrc/backenddb/sql-schema/merchant-0036-copy.sql.fragment | 477+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/sql-schema/merchant-0036-drop.sql.fragment | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/sql-schema/merchant-0036-init.sql.fragment | 964+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/sql-schema/merchant-0036-setval.sql.fragment | 33+++++++++++++++++++++++++++++++++
Asrc/backenddb/sql-schema/merchant-0036.sql.in | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/sql-schema/meson.build | 197++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Asrc/backenddb/sql-schema/preprocess-sql.sh | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/start.c | 3++-
Msrc/backenddb/store_wire_fee_by_exchange.c | 19+++++++++----------
Msrc/backenddb/test_merchantdb.c | 169++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/backenddb/unlock_inventory.c | 20++++++++++----------
Msrc/backenddb/update_account.c | 40++++++++++++++++++----------------------
Msrc/backenddb/update_category.c | 34+++++++++++++++-------------------
Msrc/backenddb/update_contract_session.c | 46+++++++++++++++++++++-------------------------
Msrc/backenddb/update_contract_terms.c | 40+++++++++++++++++++---------------------
Msrc/backenddb/update_deposit_confirmation_status.c | 37++++++++++++++++++-------------------
Msrc/backenddb/update_donau_instance.c | 43+++++++++++++++++++++----------------------
Msrc/backenddb/update_donau_instance_receipts_amount.c | 26+++++++++++---------------
Msrc/backenddb/update_instance.c | 9++++-----
Msrc/backenddb/update_instance_auth.c | 12++++++------
Msrc/backenddb/update_mfa_challenge.c | 17++++++++---------
Msrc/backenddb/update_money_pot.c | 46+++++++++++++++++++++++-----------------------
Asrc/backenddb/update_money_pot.sql | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/update_otp.c | 36++++++++++++++++--------------------
Msrc/backenddb/update_pending_webhook.c | 13++++++-------
Msrc/backenddb/update_product.c | 90++++++++++++++++++++++++++++++++++++++-----------------------------------------
Asrc/backenddb/update_product.sql | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/update_product_group.c | 21++++++++++-----------
Asrc/backenddb/update_product_group.sql | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/update_report.c | 56++++++++++++++++++++++++++------------------------------
Msrc/backenddb/update_report_status.c | 38+++++++++++++++++---------------------
Msrc/backenddb/update_template.c | 52++++++++++++++++++++++------------------------------
Msrc/backenddb/update_token_family.c | 38++++++++++++++++++--------------------
Msrc/backenddb/update_transfer_status.c | 41++++++++++++++++++++---------------------
Msrc/backenddb/update_unit.c | 52++++++++++++++++++++++++----------------------------
Asrc/backenddb/update_unit.sql | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/update_webhook.c | 39++++++++++++++++++---------------------
Msrc/backenddb/update_wirewatch_progress.c | 32++++++++++++++------------------
Msrc/backenddb/upsert_donau_keys.c | 11++++++-----
Msrc/include/merchant-database/delete_unit.h | 3---
Msrc/include/merchant-database/insert_product.h | 25+++++++++++--------------
Msrc/include/merchant-database/insert_transfer.h | 22++++++++++------------
Msrc/include/merchant-database/insert_unclaim_signature.h | 17+++++++----------
Msrc/include/merchant-database/insert_unit.h | 15++++++---------
Asrc/include/merchant-database/lookup_all_webhooks.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/merchant-database/lookup_order_charity.h | 2--
Msrc/include/merchant-database/select_accounts.h | 12+++++-------
Asrc/include/merchant-database/select_accounts_by_instance.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/include/merchant-database/set_instance.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/merchant-database/update_product.h | 31++++++++++++++-----------------
Msrc/include/merchant-database/update_unit.h | 29+++++++++++++----------------
Msrc/testing/test_merchant_accounts.sh | 47+++++++++++++++++++++++++++++++++++++----------
Msrc/testing/test_merchant_instance_auth.sh | 17++++++++++-------
Msrc/testing/test_merchant_mfa.sh | 2++
Msrc/testing/test_merchant_order_creation.sh | 1+
Msrc/testing/test_merchant_product_creation.sh | 2++
Msrc/testing/test_merchant_wirewatch.sh | 2++
311 files changed, 11274 insertions(+), 8449 deletions(-)

diff --git a/README b/README @@ -18,6 +18,24 @@ machine integration (taler-mdb) and various demonstrator front-ends at https://git.taler.net/. +Upgrading to v0.16 +================== + +Upgrading a taler-merchant-httpd < 0.15 to >= 0.16 requires stopping the +existing service and running taler-merchant-dbinit to upgrade the database +schema. The migration will be done in one LARGE transaction that can take +several minutes. If the existing database has non-trivial amounts of data, +that transaction is likely to run into a PostgreSQL error "out of shared +memory" with the default PostgreSQL database settings. It is thus recommended +to set + +max_locks_per_transaction = 512 +max_pred_locks_per_transaction = 512 + +(and restart the database) if this problem is encountered. +The values can afterwards be lowered again for production. + + Joining GNU =========== @@ -53,7 +71,7 @@ Direct dependencies These are the direct dependencies for running a Taler exchange: -- GNU Taler exchange >= 0.14.0 +- GNU Taler exchange >= 0.16.0 - PostgreSQL >= 15.0 - libqrencode diff --git a/src/backend/taler-merchant-depositcheck.c b/src/backend/taler-merchant-depositcheck.c @@ -34,6 +34,7 @@ struct ExchangeInteraction; #include "merchant-database/select_exchange_keys.h" #include "merchant-database/preflight.h" #include "merchant-database/account_kyc_set_failed.h" +#include "merchant-database/set_instance.h" #include "merchant-database/insert_deposit_to_transfer.h" #include "merchant-database/update_deposit_confirmation_status.h" #include "merchant-database/start.h" @@ -439,6 +440,15 @@ deposit_get_cb ( "Exchange returned wire transfer over %s for deposited coin %s\n", TALER_amount2s (&dr->details.ok.coin_contribution), TALER_B2S (&w->coin_pub)); + qs = TALER_MERCHANTDB_set_instance ( + pg, + w->instance_id); + if (qs <= 0) + { + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + } qs = TALER_MERCHANTDB_insert_deposit_to_transfer ( pg, w->deposit_serial, @@ -451,6 +461,10 @@ deposit_get_cb ( GNUNET_SCHEDULER_shutdown (); return; } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); break; } case MHD_HTTP_ACCEPTED: diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -52,7 +52,8 @@ #include "taler-merchant-httpd_post-orders-ORDER_ID-refund.h" #include "taler-merchant-httpd_post-private-accounts-H_WIRE-kycauth.h" #include "merchant-database/lookup_instances.h" -#include "merchant-database/select_accounts.h" +#include "merchant-database/set_instance.h" +#include "merchant-database/select_accounts_by_instance.h" #include "merchant-database/event_listen.h" #include "merchant-database/preflight.h" #include "merchant-database/event_notify.h" @@ -681,24 +682,17 @@ url_handler (void *cls, (void) cls; (void) version; - if (NULL != hc->url) - { - /* MHD calls us again for a request, we already identified - the handler, just continue processing with the handler */ - return process_upload_with_handler (hc, - upload_data, - upload_data_size); - } - hc->url = url; - log_request (hc, - method); - - /* Find out the merchant backend instance for the request. - * If there is an instance, remove the instance specification - * from the beginning of the request URL. */ + if (NULL == hc->url) { + /* First time. + * Find out the merchant backend instance for the request. + * If there is an instance, remove the instance specification + * from the beginning of the request URL. */ enum GNUNET_GenericReturnValue ret; + hc->url = url; + log_request (hc, + method); ret = identify_instance (hc, &url, &use_admin); @@ -706,6 +700,59 @@ url_handler (void *cls, return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } + if (NULL != hc->instance) + { + /* Narrow DB interaction to selected instance */ + enum GNUNET_DB_QueryStatus qs; + + qs = TALER_MERCHANTDB_set_instance (TMH_db, + hc->instance->settings.id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SETUP_FAILED, + "set_instance"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SETUP_FAILED, + "set_instance"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->url); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + if (NULL != hc->rh) + { + enum MHD_Result res; + + /* MHD calls us again for a request, we already identified + the handler, just continue processing with the handler */ + res = process_upload_with_handler (hc, + upload_data, + upload_data_size); + if (NULL != hc->instance) + { + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (TMH_db, + NULL)); + } + return res; + } + + /* First time, let's figure out the handler */ { enum GNUNET_GenericReturnValue ret; @@ -715,7 +762,15 @@ url_handler (void *cls, use_admin, &is_public); if (GNUNET_OK != ret) + { + if (NULL != hc->instance) + { + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (TMH_db, + NULL)); + } return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } } /* At this point, we must have found a handler */ @@ -741,7 +796,21 @@ url_handler (void *cls, ret = TMH_perform_access_control (hc); if (GNUNET_OK != ret) + { + if (NULL != hc->instance) + { + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (TMH_db, + NULL)); + } return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + } + if (NULL != hc->instance) + { + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (TMH_db, + NULL)); } if ( (NULL != hc->instance) && /* make static analysis happy */ @@ -862,18 +931,34 @@ add_instance_cb (void *cls, else mi->deleted = true; mi->merchant_pub = *merchant_pub; - qs = TALER_MERCHANTDB_select_accounts (TMH_db, - mi->settings.id, - &add_account_cb, - mi); + qs = TALER_MERCHANTDB_set_instance (TMH_db, + mi->settings.id); + if (0 > qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Error setting instance `%s'\n", + mi->settings.id); + GNUNET_SCHEDULER_shutdown (); + return; + } + qs = TALER_MERCHANTDB_select_accounts_by_instance ( + TMH_db, + mi->settings.id, + &add_account_cb, + mi); if (0 > qs) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Error loading accounts of `%s' from database\n", mi->settings.id); + GNUNET_SCHEDULER_shutdown (); + return; } GNUNET_assert (GNUNET_OK == TMH_add_instance (mi)); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (TMH_db, + NULL)); } diff --git a/src/backend/taler-merchant-httpd_delete-private-units-UNIT.c b/src/backend/taler-merchant-httpd_delete-private-units-UNIT.c @@ -29,7 +29,6 @@ TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, struct TMH_HandlerContext *hc) { enum GNUNET_DB_QueryStatus qs; - bool no_instance = false; bool no_unit = false; bool builtin_conflict = false; @@ -37,7 +36,6 @@ TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, qs = TALER_MERCHANTDB_delete_unit (TMH_db, hc->instance->settings.id, hc->infix, - &no_instance, &no_unit, &builtin_conflict); switch (qs) @@ -58,11 +56,6 @@ TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_DB_STORE_FAILED, "delete_unit"); } - if (no_instance) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->instance->settings.id); if (no_unit) return TALER_MHD_reply_with_error (connection, MHD_HTTP_NOT_FOUND, diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -25,6 +25,7 @@ #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd.h" #include "merchant-database/get_kyc_limits.h" +#include "merchant-database/set_instance.h" #include "merchant-database/select_exchange_keys.h" #include "merchant-database/event_listen.h" #include "merchant-database/event_notify.h" @@ -723,6 +724,14 @@ TMH_exchange_check_debit ( json_t *jlimits = NULL; enum GNUNET_DB_QueryStatus qs; + qs = TALER_MERCHANTDB_set_instance ( + TMH_db, + instance_id); + if (0 >= qs) + { + GNUNET_break (0); + return TMH_ES_NO_KEYS | TMH_ES_RETRY_OK; + } qs = TALER_MERCHANTDB_get_kyc_limits (TMH_db, wm->payto_uri, instance_id, @@ -731,6 +740,10 @@ TMH_exchange_check_debit ( &no_access_token, &jlimits); GNUNET_break (qs >= 0); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + TMH_db, + NULL)); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "get_kyc_limits for %s at %s returned %s/%s\n", wm->payto_uri.full_payto, diff --git a/src/backend/taler-merchant-httpd_get-private-accounts.c b/src/backend/taler-merchant-httpd_get-private-accounts.c @@ -21,7 +21,7 @@ #include "platform.h" #include "taler-merchant-httpd_get-private-accounts.h" #include <taler/taler_json_lib.h> -#include "merchant-database/select_accounts.h" +#include "merchant-database/select_accounts_by_instance.h" /** @@ -62,10 +62,11 @@ TMH_private_get_accounts (const struct TMH_RequestHandler *rh, pa = json_array (); GNUNET_assert (NULL != pa); - qs = TALER_MERCHANTDB_select_accounts (TMH_db, - hc->instance->settings.id, - &add_account, - pa); + qs = TALER_MERCHANTDB_select_accounts_by_instance ( + TMH_db, + hc->instance->settings.id, + &add_account, + pa); if (0 > qs) { GNUNET_break (0); diff --git a/src/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.c b/src/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.c @@ -140,7 +140,6 @@ TMH_private_patch_products_ID ( enum MHD_Result ret; size_t num_cats = 0; uint64_t *cats = NULL; - bool no_instance; ssize_t no_cat; bool no_product; bool lost_reduced; @@ -356,7 +355,6 @@ TMH_private_patch_products_ID ( &pd, num_cats, cats, - &no_instance, &no_cat, &no_product, &lost_reduced, @@ -391,14 +389,6 @@ TMH_private_patch_products_ID ( break; } - if (no_instance) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } if (-1 != no_cat) { char cat_str[24]; diff --git a/src/backend/taler-merchant-httpd_patch-private-units-UNIT.c b/src/backend/taler-merchant-httpd_patch-private-units-UNIT.c @@ -74,7 +74,6 @@ TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, const uint32_t *unit_precision_ptr = NULL; const bool *unit_active_ptr = NULL; enum GNUNET_DB_QueryStatus qs; - bool no_instance = false; bool no_unit = false; bool builtin_conflict = false; enum MHD_Result ret = MHD_YES; @@ -169,7 +168,6 @@ TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, unit_allow_fraction_ptr, unit_precision_ptr, unit_active_ptr, - &no_instance, &no_unit, &builtin_conflict); switch (qs) @@ -193,14 +191,6 @@ TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, goto cleanup; } - if (no_instance) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } if (no_unit) { ret = TALER_MHD_reply_with_error (connection, diff --git a/src/backend/taler-merchant-httpd_post-management-instances.c b/src/backend/taler-merchant-httpd_post-management-instances.c @@ -33,6 +33,7 @@ #include <taler/taler_json_lib.h> #include <regex.h> #include "merchant-database/insert_instance.h" +#include "merchant-database/set_instance.h" #include "merchant-database/insert_login_token.h" #include "merchant-database/start.h" @@ -299,10 +300,11 @@ post_instances (const struct TMH_RequestHandler *rh, { GNUNET_JSON_parse_free (spec); GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED, - is.id); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED, + is.id); } /* Check for idempotency */ if ( (0 == strcmp (mi->settings.id, @@ -487,6 +489,7 @@ post_instances (const struct TMH_RequestHandler *rh, TALER_MERCHANTDB_start (TMH_db, "post /instances")) { + GNUNET_break (0); mi->rc = 1; TMH_instance_decref (mi); GNUNET_JSON_parse_free (spec); @@ -548,6 +551,7 @@ retry: } /* for .. MAX_RETRIES */ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { + GNUNET_break (0); mi->rc = 1; TMH_instance_decref (mi); GNUNET_JSON_parse_free (spec); @@ -572,6 +576,39 @@ retry: } { + /* Narrow DB interaction to new instance */ + enum GNUNET_DB_QueryStatus qs; + + qs = TALER_MERCHANTDB_set_instance (TMH_db, + is.id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SETUP_FAILED, + "set_instance"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SETUP_FAILED, + "set_instance"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->url); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + { struct TALER_MERCHANTDB_LoginTokenP btoken; enum TMH_AuthScope iscope = TMH_AS_REFRESHABLE | TMH_AS_SPA; enum GNUNET_DB_QueryStatus qs; @@ -596,9 +633,11 @@ retry: case GNUNET_DB_STATUS_SOFT_ERROR: case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_login_token"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_login_token"); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c @@ -37,6 +37,7 @@ struct RefundDetails; #include "merchant-database/refund_coin.h" #include "merchant-database/preflight.h" #include "merchant-database/start.h" +#include "merchant-database/set_instance.h" /** @@ -740,6 +741,18 @@ begin_transaction (struct AbortContext *ac) /* First, try to see if we have all we need already done */ TALER_MERCHANTDB_preflight (TMH_db); + qs = TALER_MERCHANTDB_set_instance ( + TMH_db, + ac->hc->instance->settings.id); + if (0 >= qs) + { + GNUNET_break (0); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + "set_instance"); + goto cleanup; + } if (GNUNET_OK != TALER_MERCHANTDB_start (TMH_db, "run abort")) @@ -749,7 +762,7 @@ begin_transaction (struct AbortContext *ac) MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, NULL); - return; + goto cleanup; } /* check payment was indeed incomplete @@ -773,7 +786,7 @@ begin_transaction (struct AbortContext *ac) if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); - return; + goto cleanup; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); @@ -781,14 +794,14 @@ begin_transaction (struct AbortContext *ac) MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "order status"); - return; + goto cleanup; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: TALER_MERCHANTDB_rollback (TMH_db); resume_abort_with_error (ac, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND, "Could not find contract"); - return; + goto cleanup; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: if (paid) { @@ -798,7 +811,7 @@ begin_transaction (struct AbortContext *ac) MHD_HTTP_PRECONDITION_FAILED, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE, "Payment was complete, refusing to abort"); - return; + goto cleanup; } } if (0 != @@ -811,7 +824,7 @@ begin_transaction (struct AbortContext *ac) MHD_HTTP_FORBIDDEN, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH, "Provided hash does not match order on file"); - return; + goto cleanup; } } @@ -827,7 +840,7 @@ begin_transaction (struct AbortContext *ac) if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); - return; + goto cleanup; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); @@ -835,7 +848,7 @@ begin_transaction (struct AbortContext *ac) MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "deposits"); - return; + goto cleanup; } qs = TALER_MERCHANTDB_commit (TMH_db); @@ -845,13 +858,13 @@ begin_transaction (struct AbortContext *ac) if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); - return; + goto cleanup; } resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); - return; + goto cleanup; } /* At this point, the refund got correctly committed @@ -859,9 +872,14 @@ begin_transaction (struct AbortContext *ac) if (ac->pending > 0) { find_next_exchange (ac); - return; + goto cleanup; } generate_success_response (ac); +cleanup: + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + TMH_db, + NULL)); } diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c @@ -63,6 +63,7 @@ struct ExchangeGroup; #include "merchant-database/lookup_deposits_by_order.h" #include "merchant-database/lookup_order_charity.h" #include "merchant-database/lookup_refunds.h" +#include "merchant-database/set_instance.h" #include "merchant-database/lookup_spent_tokens_by_order.h" #include "merchant-database/lookup_token_family_key.h" #include "merchant-database/mark_contract_paid.h" @@ -1053,6 +1054,11 @@ batch_deposit_transaction ( uint64_t b_dep_serial; uint32_t off = 0; + qs = TALER_MERCHANTDB_set_instance ( + TMH_db, + pc->hc->instance->settings.id); + if (qs <= 0) + return qs; /* failure, we're done */ qs = TALER_MERCHANTDB_insert_deposit_confirmation ( TMH_db, pc->hc->instance->settings.id, @@ -1067,7 +1073,7 @@ batch_deposit_transaction ( dr->details.ok.exchange_pub, &b_dep_serial); if (qs <= 0) - return qs; /* Entire batch already known or failure, we're done */ + goto cleanup; /* Entire batch already known or failure, we're done */ for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) { @@ -1099,9 +1105,14 @@ batch_deposit_transaction ( pc->check_contract.contract_terms->pc->wire_deadline.abs_time, GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES))); if (qs < 0) - return qs; + goto cleanup; GNUNET_break (qs > 0); } +cleanup: + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + TMH_db, + NULL)); return qs; } @@ -4757,7 +4768,6 @@ phase_parse_wallet_data (struct PayContext *pc) pc->hc->instance->settings.id, donau_url_tmp, &pc->parse_wallet_data.charity_id, - &pc->parse_wallet_data.charity_priv, &pc->parse_wallet_data.charity_max_per_year, &pc->parse_wallet_data.charity_receipts_to_date, &donau_keys_json, @@ -4785,6 +4795,11 @@ phase_parse_wallet_data (struct PayContext *pc) donau_url_tmp)); return; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_static_assert (sizeof (pc->parse_wallet_data.charity_priv) == + sizeof (pc->hc->instance->merchant_priv)); + memcpy (&pc->parse_wallet_data.charity_priv, + &pc->hc->instance->merchant_priv, + sizeof (pc->hc->instance->merchant_priv)); pc->parse_wallet_data.donau.donau_url = GNUNET_strdup (donau_url_tmp); break; diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.c @@ -35,6 +35,7 @@ struct CoinRefund; #include "taler-merchant-httpd_post-orders-ORDER_ID-refund.h" #include "merchant-database/insert_refund_proof.h" #include "merchant-database/lookup_contract_terms.h" +#include "merchant-database/set_instance.h" #include "merchant-database/lookup_refund_proof.h" #include "merchant-database/lookup_refunds_detailed.h" #include "merchant-database/event_notify.h" @@ -414,12 +415,9 @@ refund_cb (struct CoinRefund *cr, { enum GNUNET_DB_QueryStatus qs; - cr->exchange_pub = rr->details.ok.exchange_pub; - cr->exchange_sig = rr->details.ok.exchange_sig; - qs = TALER_MERCHANTDB_insert_refund_proof (TMH_db, - cr->refund_serial, - &rr->details.ok.exchange_sig, - &rr->details.ok.exchange_pub); + qs = TALER_MERCHANTDB_set_instance ( + TMH_db, + cr->prd->hc->instance->settings.id); if (0 >= qs) { /* generally, this is relatively harmless for the merchant, but let's at @@ -430,7 +428,28 @@ refund_cb (struct CoinRefund *cr, } else { - notify_refund_obtained (cr->prd); + cr->exchange_pub = rr->details.ok.exchange_pub; + cr->exchange_sig = rr->details.ok.exchange_sig; + qs = TALER_MERCHANTDB_insert_refund_proof (TMH_db, + cr->refund_serial, + &rr->details.ok.exchange_sig, + &rr->details.ok.exchange_pub); + if (0 >= qs) + { + /* generally, this is relatively harmless for the merchant, but let's at + least log this. */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to persist exchange response to /refund in database: %d\n", + qs); + } + else + { + notify_refund_obtained (cr->prd); + } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + TMH_db, + NULL)); } } break; diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c @@ -83,7 +83,6 @@ TMH_post_orders_ID_unclaim (const struct TMH_RequestHandler *rh, TALER_MERCHANTDB_preflight (TMH_db); qs = TALER_MERCHANTDB_insert_unclaim_signature (TMH_db, hc->instance->settings.id, - &hc->instance->merchant_pub, order_id, &nonce, &h_contract, diff --git a/src/backend/taler-merchant-httpd_post-private-accounts.c b/src/backend/taler-merchant-httpd_post-private-accounts.c @@ -31,7 +31,7 @@ #include "taler-merchant-httpd_mfa.h" #include <regex.h> #include "merchant-database/activate_account.h" -#include "merchant-database/select_accounts.h" +#include "merchant-database/select_accounts_by_instance.h" #include "merchant-database/preflight.h" /** @@ -239,10 +239,11 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, if (! ok) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - "The payment target type is forbidden by policy"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + "The payment target type is forbidden by policy"); } } @@ -255,20 +256,22 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, 0)) ) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - "The specific account is forbidden by policy"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + "The specific account is forbidden by policy"); } if ( (NULL == pac.credit_facade_url) != (NULL == pac.credit_facade_credentials) ) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - (NULL == pac.credit_facade_url) + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + (NULL == pac.credit_facade_url) ? "credit_facade_url" : "credit_facade_credentials"); } @@ -283,10 +286,11 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, &auth)) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "credit_facade_url or credit_facade_credentials"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "credit_facade_url or credit_facade_credentials"); } TALER_MERCHANT_BANK_auth_free (&auth); } @@ -295,24 +299,27 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, enum GNUNET_DB_QueryStatus qs; TALER_MERCHANTDB_preflight (TMH_db); - qs = TALER_MERCHANTDB_select_accounts (TMH_db, - mi->settings.id, - &account_cb, - &pac); + qs = TALER_MERCHANTDB_select_accounts_by_instance ( + TMH_db, + mi->settings.id, + &account_cb, + &pac); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_accounts"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_accounts"); case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_accounts"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_accounts"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: @@ -333,24 +340,28 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, &pac.h_wire)); } +#if 1 if (pac.have_conflicting_account) { /* Conflict, refuse request */ GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, - pac.uri.full_payto); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, + pac.uri.full_payto); } +#endif if (pac.have_any_account) { /* MFA needed */ enum GNUNET_GenericReturnValue ret; - ret = TMH_mfa_check_simple (hc, - TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, - mi); + ret = TMH_mfa_check_simple ( + hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + mi); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Account creation MFA check returned %d\n", (int) ret); diff --git a/src/backend/taler-merchant-httpd_post-private-donau.c b/src/backend/taler-merchant-httpd_post-private-donau.c @@ -31,6 +31,8 @@ #include "merchant-database/insert_donau_instance.h" #include "merchant-database/event_listen.h" #include "merchant-database/event_notify.h" +#include "merchant-database/set_instance.h" + /** * Context for the POST /donau request handler. @@ -134,6 +136,9 @@ donau_charity_get_cb (void *cls, enum GNUNET_DB_QueryStatus qs; pdc->get_handle = NULL; + pdc->suspended = GNUNET_NO; + MHD_resume_connection (pdc->connection); + TALER_MHD_daemon_trigger (); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing DONAU charity get response"); /* Anything but 200 => propagate Donau’s response. */ @@ -144,9 +149,6 @@ donau_charity_get_cb (void *cls, TALER_MHD_PACK_EC (gcr->hr.ec), GNUNET_JSON_pack_uint64 ("donau_http_status", gcr->hr.http_status)); - pdc->suspended = GNUNET_NO; - MHD_resume_connection (pdc->connection); - TALER_MHD_daemon_trigger (); return; } @@ -160,8 +162,19 @@ donau_charity_get_cb (void *cls, pdc->response = TALER_MHD_make_error ( TALER_EC_GENERIC_PARAMETER_MALFORMED, "charity_pub != merchant_pub"); - MHD_resume_connection (pdc->connection); - TALER_MHD_daemon_trigger (); + return; + } + + qs = TALER_MERCHANTDB_set_instance ( + TMH_db, + pdc->hc->instance->settings.id); + if (0 >= qs) + { + GNUNET_break (0); + pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + pdc->response = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "set_instance"); return; } @@ -169,6 +182,10 @@ donau_charity_get_cb (void *cls, pdc->donau_url, &gcr->details.ok.charity, pdc->charity_id); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + TMH_db, + NULL)); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -212,9 +229,6 @@ donau_charity_get_cb (void *cls, break; } } - pdc->suspended = GNUNET_NO; - MHD_resume_connection (pdc->connection); - TALER_MHD_daemon_trigger (); } diff --git a/src/backend/taler-merchant-httpd_post-private-orders.c b/src/backend/taler-merchant-httpd_post-private-orders.c @@ -1624,13 +1624,14 @@ add_input_token_family (struct OrderContext *oc, enum TALER_ErrorCode ec = TALER_EC_INVALID; /* make compiler happy */ unsigned int http_status = 0; /* make compiler happy */ - qs = TALER_MERCHANTDB_lookup_token_family_keys (TMH_db, - oc->hc->instance->settings.id, - slug, - now, - end, - &add_family_key, - oc); + qs = TALER_MERCHANTDB_lookup_token_family_keys ( + TMH_db, + oc->hc->instance->settings.id, + slug, + now, + end, + &add_family_key, + oc); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -3568,10 +3569,6 @@ phase_parse_choices (struct OrderContext *oc) contract_input->details.token.token_family_slug)) { GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, - contract_input->details.token.token_family_slug); return; } off++; diff --git a/src/backend/taler-merchant-httpd_post-private-products.c b/src/backend/taler-merchant-httpd_post-private-products.c @@ -130,7 +130,6 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, size_t num_cats = 0; uint64_t *cats = NULL; bool conflict; - bool no_instance; ssize_t no_cat; bool no_group; bool no_pot; @@ -340,7 +339,6 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, &pd, num_cats, cats, - &no_instance, &conflict, &no_cat, &no_group, @@ -367,15 +365,6 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } - if (no_instance) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } if (no_group) { ret = TALER_MHD_reply_with_error ( diff --git a/src/backend/taler-merchant-httpd_post-private-transfers.c b/src/backend/taler-merchant-httpd_post-private-transfers.c @@ -58,7 +58,6 @@ TMH_private_post_transfers (const struct TMH_RequestHandler *rh, }; enum GNUNET_GenericReturnValue res; enum GNUNET_DB_QueryStatus qs; - bool no_instance; bool no_account; bool conflict; @@ -83,7 +82,6 @@ TMH_private_post_transfers (const struct TMH_RequestHandler *rh, &amount, payto_uri, 0 /* no bank serial known! */, - &no_instance, &no_account, &conflict); switch (qs) @@ -105,17 +103,6 @@ TMH_private_post_transfers (const struct TMH_RequestHandler *rh, case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } - if (no_instance) - { - /* should be only possible if instance was concurrently deleted, - that's so theoretical we rather log as error... */ - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->instance->settings.id); - } if (no_account) { GNUNET_break_op (0); diff --git a/src/backend/taler-merchant-httpd_post-private-units.c b/src/backend/taler-merchant-httpd_post-private-units.c @@ -139,14 +139,12 @@ TMH_private_post_units (const struct TMH_RequestHandler *rh, nud.unit_builtin = false; { - bool no_instance = false; bool conflict = false; uint64_t unit_serial = 0; qs = TALER_MERCHANTDB_insert_unit (TMH_db, mi->settings.id, &nud, - &no_instance, &conflict, &unit_serial); @@ -177,14 +175,6 @@ TMH_private_post_units (const struct TMH_RequestHandler *rh, break; } - if (no_instance) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } if (conflict) { ret = TALER_MHD_reply_with_error (connection, diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c @@ -36,6 +36,7 @@ struct Inquiry; #include "merchant-database/account_kyc_get_outdated.h" #include "merchant-database/account_kyc_set_status.h" #include "merchant-database/get_kyc_status.h" +#include "merchant-database/set_instance.h" #include "merchant-database/select_accounts.h" #include "merchant-database/select_exchange_keys.h" #include "merchant-database/event_listen.h" @@ -678,6 +679,15 @@ exchange_check_cb ( { enum GNUNET_DB_QueryStatus qs; + qs = TALER_MERCHANTDB_set_instance (pg, + i->a->instance_id); + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } qs = TALER_MERCHANTDB_account_kyc_set_status ( pg, i->a->instance_id, @@ -695,6 +705,10 @@ exchange_check_cb ( i->jlimits, i->aml_review, i->kyc_ok); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); if (qs < 0) { GNUNET_break (0); @@ -862,6 +876,15 @@ start_inquiry (struct Exchange *e, GNUNET_CONTAINER_DLL_insert (a->i_head, a->i_tail, i); + qs = TALER_MERCHANTDB_set_instance (pg, + a->instance_id); + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } qs = TALER_MERCHANTDB_get_kyc_status (pg, a->merchant_account_uri, a->instance_id, @@ -976,6 +999,15 @@ flag_ineligible (const char *instance_id, "Account %s not eligible at exchange %s\n", TALER_B2S (h_wire), exchange_url); + qs = TALER_MERCHANTDB_set_instance (pg, + instance_id); + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } qs = TALER_MERCHANTDB_account_kyc_set_status ( pg, instance_id, @@ -991,6 +1023,10 @@ flag_ineligible (const char *instance_id, NULL, false, false); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); if (qs < 0) { GNUNET_break (0); @@ -1127,7 +1163,6 @@ find_accounts (void *cls) account_task = NULL; database_gen++; qs = TALER_MERCHANTDB_select_accounts (pg, - NULL, /* all instances */ &account_cb, NULL); if (qs < 0) diff --git a/src/backend/taler-merchant-reconciliation.c b/src/backend/taler-merchant-reconciliation.c @@ -36,6 +36,7 @@ struct Inquiry; #include "merchant-database/lookup_wire_fee.h" #include "merchant-database/select_exchange_keys.h" #include "merchant-database/select_open_transfers.h" +#include "merchant-database/set_instance.h" #include "merchant-database/update_transfer_status.h" #include "merchant-database/event_listen.h" #include "merchant-database/preflight.h" @@ -303,6 +304,15 @@ update_transaction_status (const struct Inquiry *w, { enum GNUNET_DB_QueryStatus qs; + qs = TALER_MERCHANTDB_set_instance (pg, + w->instance_id); + if (qs <= 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } qs = TALER_MERCHANTDB_update_transfer_status (pg, w->exchange->exchange_url, &w->wtid, @@ -311,6 +321,10 @@ update_transaction_status (const struct Inquiry *w, ec, last_hint, needs_retry); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); if (qs < 0) { GNUNET_break (0); @@ -781,6 +795,16 @@ wire_transfer_cb (struct Inquiry *w, { enum GNUNET_DB_QueryStatus qs; + qs = TALER_MERCHANTDB_set_instance (pg, + w->instance_id); + if (0 > qs) + { + /* Always report on DB error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } qs = TALER_MERCHANTDB_insert_transfer_details (pg, w->instance_id, w->exchange->exchange_url, @@ -804,6 +828,10 @@ wire_transfer_cb (struct Inquiry *w, { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Transfer already known. Ignoring duplicate.\n"); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); return; } } @@ -860,6 +888,10 @@ wire_transfer_cb (struct Inquiry *w, but still have no result? */ GNUNET_break (0); ctc.ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); return; case GNUNET_SYSERR: /* #check_transfer() failed, report conflict! */ @@ -882,6 +914,10 @@ wire_transfer_cb (struct Inquiry *w, ctc.ec, NULL /* no hint */, ! ctc.failure); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); end_inquiry (w); return; } @@ -899,6 +935,10 @@ wire_transfer_cb (struct Inquiry *w, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE, TALER_amount2s (&td->wire_fee), false); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); end_inquiry (w); return; } @@ -922,6 +962,10 @@ wire_transfer_cb (struct Inquiry *w, return; } } + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance ( + pg, + NULL)); end_inquiry (w); } diff --git a/src/backend/taler-merchant-report-generator.c b/src/backend/taler-merchant-report-generator.c @@ -35,6 +35,7 @@ #include "merchant-database/delete_report.h" #include "merchant-database/lookup_reports_pending.h" #include "merchant-database/update_report_status.h" +#include "merchant-database/set_instance.h" #include "merchant-database/event_listen.h" @@ -268,6 +269,18 @@ finish_transmission (struct ReportActivity *ra, struct GNUNET_TIME_Timestamp next_ts; next_ts = GNUNET_TIME_absolute_to_timestamp (ra->next_transmission); + qs = TALER_MERCHANTDB_set_instance (pg, + ra->instance_id); + if (qs <= 0) + { + free_ra (ra); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to set instance to update report status: %d\n", + qs); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } if ( (TALER_EC_NONE == ec) && (ra->one_shot) ) { diff --git a/src/backend/taler-merchant-wirewatch.c b/src/backend/taler-merchant-wirewatch.c @@ -29,6 +29,7 @@ #include "merchantdb_lib.h" #include "merchantdb_lib.h" #include "merchant-database/insert_transfer.h" +#include "merchant-database/set_instance.h" #include "merchant-database/select_wirewatch_accounts.h" #include "merchant-database/update_wirewatch_progress.h" #include "merchant-database/event_listen.h" @@ -187,10 +188,26 @@ save (struct Watch *w) { enum GNUNET_DB_QueryStatus qs; + qs = TALER_MERCHANTDB_set_instance (pg, + w->instance_id); + if (qs < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to set instance to %s (%d)\n", + w->instance_id, + qs); + GNUNET_SCHEDULER_shutdown (); + global_ret = EXIT_FAILURE; + return; + } + qs = TALER_MERCHANTDB_update_wirewatch_progress (pg, w->instance_id, w->payto_uri, w->start_row); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (pg, + NULL)); if (qs < 0) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, @@ -361,7 +378,6 @@ credit_cb ( enum GNUNET_DB_QueryStatus qs; char *exchange_url; struct TALER_WireTransferIdentifierRawP wtid; - bool no_instance; bool no_account; bool conflict; @@ -384,6 +400,16 @@ credit_cb ( } /* FIXME-Performance-Optimization: consider grouping multiple inserts into one bigger transaction with just one notify. */ + qs = TALER_MERCHANTDB_set_instance (pg, + w->instance_id); + if (qs < 0) + { + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + w->hh = NULL; + GNUNET_free (exchange_url); + return GNUNET_SYSERR; + } qs = TALER_MERCHANTDB_insert_transfer (pg, w->instance_id, exchange_url, @@ -391,10 +417,12 @@ credit_cb ( &details->amount, details->credit_account_uri, serial_id, - &no_instance, &no_account, &conflict); + GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (pg, + NULL)); GNUNET_free (exchange_url); if (qs < 0) { @@ -403,13 +431,6 @@ credit_cb ( w->hh = NULL; return GNUNET_SYSERR; } - if (no_instance) - { - GNUNET_break (0); - GNUNET_SCHEDULER_shutdown (); - w->hh = NULL; - return GNUNET_SYSERR; - } if (no_account) { GNUNET_break (0); diff --git a/src/backenddb/account_kyc_get_outdated.c b/src/backenddb/account_kyc_get_outdated.c @@ -76,11 +76,11 @@ kyc_status_cb (void *cls, char *exchange_url; char *instance_id; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("h_wire", + GNUNET_PQ_result_spec_auto_from_type ("out_h_wire", &h_wire), - GNUNET_PQ_result_spec_string ("exchange_url", + GNUNET_PQ_result_spec_string ("out_exchange_url", &exchange_url), - GNUNET_PQ_result_spec_string ("instance_id", + GNUNET_PQ_result_spec_string ("out_merchant_id", &instance_id), GNUNET_PQ_result_spec_end }; @@ -104,9 +104,10 @@ kyc_status_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_account_kyc_get_outdated (struct TALER_MERCHANTDB_PostgresContext *pg, - TALER_MERCHANTDB_KycOutdatedCallback kyc_cb, - void *kyc_cb_cls) +TALER_MERCHANTDB_account_kyc_get_outdated ( + struct TALER_MERCHANTDB_PostgresContext *pg, + TALER_MERCHANTDB_KycOutdatedCallback kyc_cb, + void *kyc_cb_cls) { struct KycStatusContext ksc = { .kyc_cb = kyc_cb, @@ -123,17 +124,11 @@ TALER_MERCHANTDB_account_kyc_get_outdated (struct TALER_MERCHANTDB_PostgresConte check_connection (pg); PREPARE (pg, "account_kyc_get_outdated", - "SELECT " - " mi.merchant_id" - " ,ma.h_wire" - " ,kyc.exchange_url" - " FROM merchant_kyc kyc" - " JOIN merchant_accounts ma" - " USING (account_serial)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE kyc.next_kyc_poll < $1" - " ORDER BY kyc.next_kyc_poll ASC;"); + "SELECT" + " out_merchant_id" + " ,out_h_wire" + " ,out_exchange_url" + " FROM merchant.account_kyc_get_outdated($1)"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, "account_kyc_get_outdated", diff --git a/src/backenddb/account_kyc_get_outdated.sql b/src/backenddb/account_kyc_get_outdated.sql @@ -0,0 +1,63 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.account_kyc_get_outdated; +CREATE FUNCTION merchant.account_kyc_get_outdated( + IN in_now INT8 +) +RETURNS TABLE( + out_merchant_id TEXT, + out_h_wire BYTEA, + out_exchange_url TEXT) +LANGUAGE plpgsql +AS $FN$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + FOR inner_rec + IN + EXECUTE format('SELECT ma.h_wire AS h_wire, kyc.exchange_url AS exchange_url' + ' FROM %I.merchant_kyc kyc' + ' JOIN %I.merchant_accounts ma USING (account_serial)' + ' WHERE kyc.next_kyc_poll < $1' + ' ORDER BY kyc.next_kyc_poll ASC', s, s) + USING in_now + LOOP + out_merchant_id := rec.merchant_id; + out_h_wire := inner_rec.h_wire; + out_exchange_url := inner_rec.exchange_url; + RETURN NEXT; + END LOOP; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; +END +$FN$; +COMMENT ON FUNCTION merchant.account_kyc_get_outdated(INT8) + IS 'Returns one row per outdated KYC entry across all instance schemas.' + ' An entry is outdated if its next_kyc_poll value is less than in_now.'; diff --git a/src/backenddb/account_kyc_get_status.c b/src/backenddb/account_kyc_get_status.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/account_kyc_get_status.h" #include "helper.h" @@ -172,7 +170,6 @@ TALER_MERCHANTDB_account_kyc_get_status ( struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (merchant_id), GNUNET_PQ_query_param_absolute_time (&now), NULL == exchange_url ? GNUNET_PQ_query_param_null () @@ -184,24 +181,26 @@ TALER_MERCHANTDB_account_kyc_get_status ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (merchant_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "account_kyc_get_status", - "SELECT " - " out_h_wire AS h_wire" - " ,out_payto_uri AS payto_uri" - " ,out_exchange_url AS exchange_url" - " ,out_kyc_timestamp AS kyc_timestamp" - " ,out_kyc_ok AS kyc_ok" - " ,out_access_token AS access_token" - " ,out_exchange_http_status AS exchange_http_status" - " ,out_exchange_ec_code AS exchange_ec_code" - " ,out_aml_review AS aml_review" - " ,out_jaccount_limits::TEXT AS jaccount_limits" - " FROM merchant_do_account_kyc_get_status($1, $2, $3, $4);"); + TMH_PQ_prepare_anon (pg, + "SELECT " + " out_h_wire AS h_wire" + " ,out_payto_uri AS payto_uri" + " ,out_exchange_url AS exchange_url" + " ,out_kyc_timestamp AS kyc_timestamp" + " ,out_kyc_ok AS kyc_ok" + " ,out_access_token AS access_token" + " ,out_exchange_http_status AS exchange_http_status" + " ,out_exchange_ec_code AS exchange_ec_code" + " ,out_aml_review AS aml_review" + " ,out_jaccount_limits::TEXT AS jaccount_limits" + " FROM merchant_do_account_kyc_get_status($1, $2, $3);"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "account_kyc_get_status", + "", params, &kyc_status_cb, &ksc); diff --git a/src/backenddb/account_kyc_get_status.sql b/src/backenddb/account_kyc_get_status.sql @@ -0,0 +1,111 @@ +-- +-- 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_account_kyc_get_status; +CREATE FUNCTION merchant_do_account_kyc_get_status ( + IN in_now INT8, + IN in_exchange_url TEXT, -- can be NULL + IN in_h_wire BYTEA -- can be NULL +) RETURNS TABLE ( + out_h_wire BYTEA, -- never NULL + out_payto_uri TEXT, -- never NULL + out_exchange_url TEXT, + out_kyc_timestamp INT8, + out_kyc_ok BOOLEAN, + out_access_token BYTEA, + out_exchange_http_status INT4, + out_exchange_ec_code INT4, + out_aml_review BOOLEAN, + out_jaccount_limits TEXT +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_account_serial INT8; + my_h_wire BYTEA; + my_payto_uri TEXT; + my_kyc_record RECORD; + +BEGIN + -- Iterate over merchant_accounts + FOR my_account_serial, my_h_wire, my_payto_uri + IN SELECT account_serial, h_wire, payto_uri + FROM merchant_accounts + WHERE active + AND (in_h_wire IS NULL OR h_wire = in_h_wire) + ORDER BY account_serial ASC + LOOP + + -- Fetch KYC info for this account (can have multiple results) + FOR my_kyc_record IN + SELECT + mk.kyc_serial_id + ,mk.exchange_url + ,mk.kyc_timestamp + ,mk.kyc_ok + ,mk.access_token + ,mk.exchange_http_status + ,mk.exchange_ec_code + ,mk.aml_review + ,mk.jaccount_limits::TEXT + FROM merchant_kyc mk + WHERE mk.account_serial = my_account_serial + AND (in_exchange_url IS NULL OR mk.exchange_url = in_exchange_url) + ORDER BY mk.kyc_serial_id ASC + LOOP + -- Ask taler-merchant-kyccheck to get us an update on the status ASAP + UPDATE merchant_kyc + SET next_kyc_poll=in_now + WHERE kyc_serial_id = my_kyc_record.kyc_serial_id; + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; -- MERCHANT_EXCHANGE_KYC_UPDATE_FORCED + RETURN QUERY + SELECT + my_h_wire, + my_payto_uri, + my_kyc_record.exchange_url, + my_kyc_record.kyc_timestamp, + my_kyc_record.kyc_ok, + my_kyc_record.access_token, + my_kyc_record.exchange_http_status, + my_kyc_record.exchange_ec_code, + my_kyc_record.aml_review, + my_kyc_record.jaccount_limits::TEXT; + END LOOP; -- loop over exchanges with KYC status for the given account + + IF NOT FOUND + THEN + -- Still return to server that we do NOT know anything + -- for the given exchange yet (but that the bank account exists) + RETURN QUERY + SELECT + my_h_wire, + my_payto_uri, + NULL::TEXT, + NULL::INT8, + NULL::BOOLEAN, + NULL::BYTEA, + NULL::INT4, + NULL::INT4, + NULL::BOOLEAN, + NULL::TEXT; + END IF; + + END LOOP; -- loop over merchant_accounts + +END $$; +COMMENT ON FUNCTION merchant_do_account_kyc_get_status + IS 'Returns the KYC status of selected exchanges and accounts, but ALSO resets the next_kyc_check time for all returned data points to the current time (in_now argument)'; diff --git a/src/backenddb/account_kyc_set_failed.c b/src/backenddb/account_kyc_set_failed.c @@ -19,7 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> #include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/account_kyc_set_failed.h" @@ -27,13 +26,14 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_account_kyc_set_failed (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire, - const char *exchange_url, - struct GNUNET_TIME_Timestamp timestamp, - unsigned int exchange_http_status, - bool kyc_ok) +TALER_MERCHANTDB_account_kyc_set_failed ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id, + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + struct GNUNET_TIME_Timestamp timestamp, + unsigned int exchange_http_status, + bool kyc_ok) { struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = { .header.size = htons (sizeof (ev)), @@ -50,7 +50,6 @@ TALER_MERCHANTDB_account_kyc_set_failed (struct TALER_MERCHANTDB_PostgresContext = GNUNET_PQ_get_event_notify_channel (&hdr); uint32_t http_status32 = (uint32_t) exchange_http_status; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (merchant_id), GNUNET_PQ_query_param_auto_from_type (h_wire), GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_timestamp (&timestamp), @@ -60,28 +59,26 @@ TALER_MERCHANTDB_account_kyc_set_failed (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_query_param_string (notify2_s), GNUNET_PQ_query_param_end }; - bool no_instance; bool no_account; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("no_instance", - &no_instance), GNUNET_PQ_result_spec_bool ("no_account", &no_account), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (merchant_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "account_kyc_set_failed", - "SELECT " - " out_no_instance AS no_instance" - " ,out_no_account AS no_account" - " FROM merchant_do_account_kyc_set_failed" - "($1, $2, $3, $4, $5, $6, $7, $8);"); + TMH_PQ_prepare_anon (pg, + "SELECT " + " out_no_account AS no_account" + " FROM merchant_do_account_kyc_set_failed" + "($1, $2, $3, $4, $5, $6, $7);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "account_kyc_set_failed", + "", params, rs); GNUNET_free (notify_s); @@ -93,7 +90,6 @@ TALER_MERCHANTDB_account_kyc_set_failed (struct TALER_MERCHANTDB_PostgresContext GNUNET_break (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs); return qs; } - GNUNET_break (! no_instance); GNUNET_break (! no_account); return qs; } diff --git a/src/backenddb/account_kyc_set_failed.sql b/src/backenddb/account_kyc_set_failed.sql @@ -0,0 +1,82 @@ +-- +-- 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_account_kyc_set_failed; +CREATE FUNCTION merchant_do_account_kyc_set_failed ( + IN in_h_wire BYTEA, + IN in_exchange_url TEXT, + IN in_timestamp INT8, + IN in_exchange_http_status INT4, + IN in_kyc_ok BOOL, + IN in_notify_str TEXT, + IN in_notify2_str TEXT, + OUT out_no_account BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_account_serial INT8; +BEGIN + +out_no_account=FALSE; + +SELECT account_serial + INTO my_account_serial + FROM merchant_accounts + WHERE h_wire=in_h_wire; + +IF NOT FOUND +THEN + out_no_account=TRUE; + RETURN; +END IF; + +UPDATE merchant_kyc + SET kyc_timestamp=in_timestamp + ,kyc_ok=in_kyc_ok + ,exchange_http_status=in_exchange_http_status + ,exchange_ec_code=0 + WHERE account_serial=my_account_serial + AND exchange_url=in_exchange_url; + +IF NOT FOUND +THEN + + INSERT INTO merchant_kyc + (kyc_timestamp + ,kyc_ok + ,account_serial + ,exchange_url + ,exchange_http_status) + VALUES + (in_timestamp + ,in_kyc_ok + ,my_account_serial + ,in_exchange_url + ,in_exchange_http_status); +END IF; + +EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify_str); + +EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify2_str); + + +-- Success! +END $$; diff --git a/src/backenddb/account_kyc_set_status.c b/src/backenddb/account_kyc_set_status.c @@ -20,7 +20,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> #include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/account_kyc_set_status.h" @@ -28,20 +27,21 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_account_kyc_set_status (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire, - const char *exchange_url, - struct GNUNET_TIME_Timestamp timestamp, - struct GNUNET_TIME_Absolute next_time, - struct GNUNET_TIME_Relative kyc_backoff, - unsigned int exchange_http_status, - enum TALER_ErrorCode exchange_ec_code, - uint64_t rule_gen, - const struct TALER_AccountAccessTokenP *access_token, - const json_t *jlimits, - bool in_aml_review, - bool kyc_ok) +TALER_MERCHANTDB_account_kyc_set_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id, + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + struct GNUNET_TIME_Timestamp timestamp, + struct GNUNET_TIME_Absolute next_time, + struct GNUNET_TIME_Relative kyc_backoff, + unsigned int exchange_http_status, + enum TALER_ErrorCode exchange_ec_code, + uint64_t rule_gen, + const struct TALER_AccountAccessTokenP *access_token, + const json_t *jlimits, + bool in_aml_review, + bool kyc_ok) { struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = { .header.size = htons (sizeof (ev)), @@ -59,7 +59,6 @@ TALER_MERCHANTDB_account_kyc_set_status (struct TALER_MERCHANTDB_PostgresContext uint32_t http_status32 = (uint32_t) exchange_http_status; uint32_t ec_code32 = (uint32_t) exchange_ec_code; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (merchant_id), GNUNET_PQ_query_param_auto_from_type (h_wire), GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_timestamp (&timestamp), @@ -80,29 +79,27 @@ TALER_MERCHANTDB_account_kyc_set_status (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_query_param_relative_time (&kyc_backoff), GNUNET_PQ_query_param_end }; - bool no_instance; bool no_account; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("no_instance", - &no_instance), GNUNET_PQ_result_spec_bool ("no_account", &no_account), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (merchant_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "account_kyc_set_status", - "SELECT " - " out_no_instance AS no_instance" - " ,out_no_account AS no_account" - " FROM merchant_do_account_kyc_set_status" - "($1, $2, $3, $4, $5, $6, $7, $8::TEXT::JSONB" - ",$9, $10, $11, $12, $13, $14, $15);"); + TMH_PQ_prepare_anon (pg, + "SELECT " + " out_no_account AS no_account" + " FROM merchant_do_account_kyc_set_status" + "($1, $2, $3, $4, $5, $6, $7::TEXT::JSONB" + ",$8, $9, $10, $11, $12, $13, $14);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "account_kyc_set_status", + "", params, rs); GNUNET_free (notify_s); @@ -114,7 +111,6 @@ TALER_MERCHANTDB_account_kyc_set_status (struct TALER_MERCHANTDB_PostgresContext GNUNET_break (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs); return qs; } - GNUNET_break (! no_instance); GNUNET_break (! no_account); return qs; } diff --git a/src/backenddb/account_kyc_set_status.sql b/src/backenddb/account_kyc_set_status.sql @@ -0,0 +1,109 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024, 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_account_kyc_set_status; +CREATE FUNCTION merchant_do_account_kyc_set_status ( + IN in_h_wire BYTEA, + IN in_exchange_url TEXT, + IN in_timestamp INT8, + IN in_exchange_http_status INT4, + IN in_exchange_ec_code INT4, + IN in_access_token BYTEA, -- can be NULL + IN in_jlimits JSONB, + IN in_aml_active BOOL, + IN in_kyc_ok BOOL, + IN in_notify_str TEXT, + IN in_notify2_str TEXT, + IN in_rule_gen INT8, + IN in_next_time INT8, + IN in_kyc_backoff INT8, + OUT out_no_account BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_account_serial INT8; +BEGIN + +out_no_account=FALSE; + +SELECT account_serial + INTO my_account_serial + FROM merchant_accounts + WHERE h_wire=in_h_wire; + +IF NOT FOUND +THEN + out_no_account=TRUE; + RETURN; +END IF; + +UPDATE merchant_kyc + SET kyc_timestamp=in_timestamp + ,kyc_ok=in_kyc_ok + ,jaccount_limits=in_jlimits + ,aml_review=in_aml_active + ,exchange_http_status=in_exchange_http_status + ,exchange_ec_code=in_exchange_ec_code + ,access_token=in_access_token + ,last_rule_gen=in_rule_gen + ,next_kyc_poll=in_next_time + ,kyc_backoff=in_kyc_backoff + WHERE account_serial=my_account_serial + AND exchange_url=in_exchange_url; + +IF NOT FOUND +THEN + + INSERT INTO merchant_kyc + (kyc_timestamp + ,kyc_ok + ,account_serial + ,exchange_url + ,jaccount_limits + ,aml_review + ,exchange_http_status + ,exchange_ec_code + ,access_token + ,last_rule_gen + ,next_kyc_poll + ,kyc_backoff) + VALUES + (in_timestamp + ,in_kyc_ok + ,my_account_serial + ,in_exchange_url + ,in_jlimits + ,in_aml_active + ,in_exchange_http_status + ,in_exchange_ec_code + ,in_access_token + ,in_rule_gen + ,in_next_time + ,in_kyc_backoff); +END IF; + +EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify_str); + +EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify2_str); + + +-- Success! +END $$; diff --git a/src/backenddb/activate_account.c b/src/backenddb/activate_account.c @@ -19,23 +19,21 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/activate_account.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_activate_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MERCHANTDB_AccountDetails *account_details, - struct TALER_MerchantWireHashP *h_wire, - struct TALER_WireSaltP *salt, - bool *not_found, - bool *conflict) +TALER_MERCHANTDB_activate_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MERCHANTDB_AccountDetails *account_details, + struct TALER_MerchantWireHashP *h_wire, + struct TALER_WireSaltP *salt, + bool *not_found, + bool *conflict) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (account_details->instance_id), GNUNET_PQ_query_param_auto_from_type (&account_details->h_wire), GNUNET_PQ_query_param_auto_from_type (&account_details->salt), GNUNET_PQ_query_param_string (account_details->payto_uri.full_payto), @@ -64,18 +62,20 @@ TALER_MERCHANTDB_activate_account (struct TALER_MERCHANTDB_PostgresContext *pg, }; GNUNET_assert (account_details->active); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (account_details->instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "activate_account", - "SELECT " - " out_h_wire AS h_wire" - " ,out_salt AS salt" - " ,out_conflict AS conflict" - " ,out_not_found AS not_found" - " FROM merchant_do_activate_account" - " ($1,$2,$3,$4,$5,$6,$7);"); + TMH_PQ_prepare_anon (pg, + "SELECT " + " out_h_wire AS h_wire" + " ,out_salt AS salt" + " ,out_conflict AS conflict" + " ,out_not_found AS not_found" + " FROM merchant_do_activate_account" + " ($1,$2,$3,$4,$5,$6);"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "activate_account", + "", params, rs); } diff --git a/src/backenddb/activate_account.sql b/src/backenddb/activate_account.sql @@ -0,0 +1,130 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_activate_account; +CREATE FUNCTION merchant_do_activate_account( + IN in_h_wire BYTEA + ,IN in_salt BYTEA + ,IN in_full_payto TEXT + ,IN in_credit_facade_url TEXT -- can be NULL + ,IN in_credit_facade_credentials TEXT -- can be NULL + ,IN in_extra_wire_subject_metadata TEXT -- can be NULL + ,OUT out_h_wire BYTEA + ,OUT out_salt BYTEA + ,OUT out_not_found BOOL + ,OUT out_conflict BOOL +) +LANGUAGE plpgsql +AS $$ +DECLARE my_active BOOL; +DECLARE my_cfu TEXT; +DECLARE my_cfc TEXT; +DECLARE my_ewsm TEXT; +DECLARE my_h_wire BYTEA; +DECLARE my_salt BYTEA; +BEGIN + out_not_found = FALSE; + out_conflict = FALSE; + out_h_wire = in_h_wire; + out_salt = in_salt; + + INSERT INTO merchant_accounts + AS ma + (h_wire + ,salt + ,payto_uri + ,credit_facade_url + ,credit_facade_credentials + ,active + ,extra_wire_subject_metadata + ) VALUES ( + in_h_wire + ,in_salt + ,in_full_payto + ,in_credit_facade_url + ,in_credit_facade_credentials::JSONB + ,TRUE + ,in_extra_wire_subject_metadata + ) ON CONFLICT DO NOTHING; + IF FOUND + THEN + -- Notify taler-merchant-kyccheck about the change in + -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; + RETURN; + END IF; + + SELECT h_wire + ,salt + ,active + ,credit_facade_url + ,credit_facade_credentials::TEXT + ,extra_wire_subject_metadata + INTO my_h_wire + ,my_salt + ,my_active + ,my_cfu + ,my_cfc + ,my_ewsm + FROM merchant_accounts + WHERE payto_uri=in_full_payto; + IF NOT FOUND + THEN + -- This should never happen (we had a conflict!) + -- Still, safe way is to return not found. + out_not_found = TRUE; + RETURN; + END IF; + + -- Check for conflict + IF (my_active AND + (ROW (my_cfu + ,my_cfc + ,my_ewsm) + IS DISTINCT FROM + ROW (in_credit_facade_url + ,in_credit_facade_credentials + ,in_extra_wire_subject_metadata))) + THEN + -- Active conflicting account, refuse! + out_conflict = TRUE; + RETURN; + END IF; + + -- Equivalent account exists, use its salt instead of the new salt + -- and just set it to active! + out_salt = my_salt; + out_h_wire = my_h_wire; + + -- Now check if existing account is already active + IF my_active + THEN + -- nothing to do + RETURN; + END IF; + + UPDATE merchant_accounts + SET active=TRUE + ,credit_facade_url=in_credit_facade_url + ,credit_facade_credentials=in_credit_facade_credentials::JSONB + ,extra_wire_subject_metadata=in_extra_wire_subject_metadata + WHERE h_wire=out_h_wire; + + -- Notify taler-merchant-kyccheck about the change in (active) + -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; + +END $$; diff --git a/src/backenddb/check_donau_instance.c b/src/backenddb/check_donau_instance.c @@ -27,36 +27,33 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_check_donau_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const char *donau_url, - uint64_t charity_id - ) +TALER_MERCHANTDB_check_donau_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MerchantPublicKeyP *merchant_pub, + const char *donau_url, + uint64_t charity_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (donau_url), GNUNET_PQ_query_param_uint64 (&charity_id), - GNUNET_PQ_query_param_auto_from_type (merchant_pub), GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == + GNUNET_memcmp (merchant_pub, + &pg->current_merchant_pub)); check_connection (pg); - PREPARE (pg, - "check_donau_instance", - "SELECT 1" - " FROM merchant_donau_instances" - " WHERE donau_url=$1" - " AND charity_id=$2 " - " AND merchant_instance_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances mi" - " WHERE mi.merchant_pub = $3)"); + TMH_PQ_prepare_anon (pg, + "SELECT 1" + " FROM merchant_donau_instances" + " WHERE donau_url=$1" + " AND charity_id=$2"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "check_donau_instance", + "", params, rs); - } diff --git a/src/backenddb/check_money_pots.c b/src/backenddb/check_money_pots.c @@ -27,14 +27,14 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_check_money_pots (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - unsigned int pots_len, - uint64_t pots[static pots_len], - uint64_t *pot_missing) +TALER_MERCHANTDB_check_money_pots ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + unsigned int pots_len, + uint64_t pots[static pots_len], + uint64_t *pot_missing) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_array_uint64 (pots_len, pots, pg->conn), @@ -47,23 +47,22 @@ TALER_MERCHANTDB_check_money_pots (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "check_money_pots", - "SELECT n AS out_missing" - " FROM UNNEST($2::INT8[]) AS n" - " WHERE NOT EXISTS (" - " SELECT 1" - " FROM merchant_money_pots mmp" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mmp.money_pot_serial=n" - " AND mi.merchant_id=$1" - " )" - " LIMIT 1;"); + TMH_PQ_prepare_anon (pg, + "SELECT n AS out_missing" + " FROM UNNEST($1::INT8[]) AS n" + " WHERE NOT EXISTS (" + " SELECT 1" + " FROM merchant_money_pots mmp" + " WHERE mmp.money_pot_serial=n" + " )" + " LIMIT 1;"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "check_money_pots", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/check_report.c b/src/backenddb/check_report.c @@ -19,19 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/check_report.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_check_report (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t report_id, - const struct TALER_MERCHANT_ReportToken *report_token, - const char *mime_type, - char **instance_id, - char **data_source) +TALER_MERCHANTDB_check_report ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t report_id, + const struct TALER_MERCHANT_ReportToken *report_token, + const char *mime_type, + char **instance_id, + char **data_source) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&report_id), @@ -40,9 +40,9 @@ TALER_MERCHANTDB_check_report (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("merchant_id", + GNUNET_PQ_result_spec_string ("out_merchant_id", instance_id), - GNUNET_PQ_result_spec_string ("data_source", + GNUNET_PQ_result_spec_string ("out_data_source", data_source), GNUNET_PQ_result_spec_end }; @@ -51,14 +51,9 @@ TALER_MERCHANTDB_check_report (struct TALER_MERCHANTDB_PostgresContext *pg, PREPARE (pg, "check_report", "SELECT" - " mi.merchant_id" - " ,mr.data_source" - " FROM merchant_reports mr" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mr.report_serial=$1" - " AND mr.report_token=$2" - " AND mr.mime_type=$3;"); + " out_merchant_id" + " ,out_data_source" + " FROM merchant.check_report($1, $2, $3)"); return GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, "check_report", diff --git a/src/backenddb/check_report.sql b/src/backenddb/check_report.sql @@ -0,0 +1,68 @@ +-- +-- 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/> +-- + + +DROP FUNCTION IF EXISTS merchant.check_report; +CREATE FUNCTION merchant.check_report( + IN in_report_id INT8, + IN in_report_token BYTEA, + IN in_mime_type TEXT) +RETURNS TABLE( + out_merchant_id TEXT, + out_data_source TEXT) +LANGUAGE plpgsql +AS $$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + EXECUTE format( + 'SELECT data_source AS ds' + ' FROM %I.merchant_reports' + ' WHERE report_serial=$1' + ' AND report_token=$2' + ' AND mime_type=$3', s) + USING in_report_id + ,in_report_token + ,in_mime_type + INTO inner_rec; + IF inner_rec IS NOT NULL + THEN + out_merchant_id := rec.merchant_id; + out_data_source := inner_rec.ds; + RETURN NEXT; + RETURN; + END IF; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; +END +$$; +COMMENT ON FUNCTION merchant.check_report(INT8, BYTEA, TEXT) + IS 'Searches all instance schemas for a report matching the given' + ' (report_serial, report_token, mime_type). Returns at most one row' + ' (the first matching instance encountered).'; diff --git a/src/backenddb/check_transfer_exists.c b/src/backenddb/check_transfer_exists.c @@ -19,19 +19,18 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/check_transfer_exists.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_check_transfer_exists (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t transfer_serial_id) +TALER_MERCHANTDB_check_transfer_exists ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t transfer_serial_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&transfer_serial_id), GNUNET_PQ_query_param_end }; @@ -39,24 +38,17 @@ TALER_MERCHANTDB_check_transfer_exists (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "check_transfer_exists", - "SELECT" - " 1" - " FROM merchant_transfers" - " JOIN merchant_accounts" - " USING (account_serial)" - " WHERE" - " credit_serial=$2" - " AND" - " merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); - + TMH_PQ_prepare_anon (pg, + "SELECT" + " 1" + " FROM merchant_transfers" + " WHERE credit_serial=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "check_transfer_exists", + "", params, rs); } diff --git a/src/backenddb/create_mfa_challenge.c b/src/backenddb/create_mfa_challenge.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "taler/taler_merchant_util.h" #include "merchantdb_lib.h" @@ -55,7 +53,7 @@ TALER_MERCHANTDB_create_mfa_challenge ( GNUNET_PQ_query_param_absolute_time (&retransmission_date), GNUNET_PQ_query_param_string (channel_str), GNUNET_PQ_query_param_string (required_address), /* $9 */ - GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_uint64 (&pg->current_merchant_serial), /* $10 */ GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { @@ -64,9 +62,13 @@ TALER_MERCHANTDB_create_mfa_challenge ( GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + GNUNET_assert (0 != pg->current_merchant_serial); PREPARE (pg, "create_mfa_challenge", - "INSERT INTO tan_challenges" + "INSERT INTO merchant.tan_challenges" " (h_body" " ,salt" " ,op" @@ -78,10 +80,8 @@ TALER_MERCHANTDB_create_mfa_challenge ( " ,tan_channel" " ,required_address" " ,merchant_serial)" - " SELECT" - " $1, $2, $3, $4, $5, $6, $7, 3, $8, $9, merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$10" + " VALUES" + " ($1, $2, $3, $4, $5, $6, $7, 3, $8, $9, $10)" " RETURNING challenge_id;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "create_mfa_challenge", diff --git a/src/backenddb/create_tables.c b/src/backenddb/create_tables.c @@ -21,6 +21,7 @@ #include <gnunet/gnunet_util_lib.h> #include "merchantdb_lib.h" #include "merchant-database/create_tables.h" +#include "helper.h" enum GNUNET_GenericReturnValue @@ -33,6 +34,10 @@ TALER_MERCHANTDB_create_tables ( GNUNET_PQ_EXECUTE_STATEMENT_END }; enum GNUNET_GenericReturnValue ret; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; conn = GNUNET_PQ_connect_with_cfg (cfg, "merchantdb-postgres", @@ -42,7 +47,24 @@ TALER_MERCHANTDB_create_tables ( if (NULL == conn) return GNUNET_SYSERR; ret = GNUNET_PQ_exec_sql (conn, - "procedures"); + "global_procedures"); + if (GNUNET_OK == ret) + ret = GNUNET_PQ_exec_sql (conn, + "instance_procedures"); + if (GNUNET_OK != + GNUNET_PQ_prepare_anon ( + conn, + "CALL merchant.sync_all_instance_procedures()")) + { + GNUNET_break (0); + GNUNET_PQ_disconnect (conn); + return GNUNET_SYSERR; + } + qs = GNUNET_PQ_eval_prepared_non_select (conn, + "", + params); + if (qs < 0) + return GNUNET_SYSERR; GNUNET_PQ_disconnect (conn); return ret; } diff --git a/src/backenddb/create_tables.sql b/src/backenddb/create_tables.sql @@ -0,0 +1,82 @@ +-- +-- 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/> +-- + +DROP PROCEDURE IF EXISTS merchant.sync_instance_procedures(BIGINT); +CREATE PROCEDURE merchant.sync_instance_procedures( + in_merchant_serial BIGINT +) + LANGUAGE plpgsql +AS $$ +DECLARE + rec RECORD; + r RECORD; + my_schema_name TEXT; + v_new_def TEXT; +BEGIN + my_schema_name = format('merchant_instance_%s.', in_merchant_serial); + FOR r IN + SELECT pg_get_functiondef(p.oid) AS definition + FROM pg_proc p + JOIN pg_namespace n + ON n.oid = p.pronamespace + WHERE n.nspname = 'merchant_instances' + LOOP + v_new_def := replace( + r.definition, + 'merchant_instances.', + my_schema_name + ); + EXECUTE v_new_def; + END LOOP; + + FOR r IN + SELECT pg_get_triggerdef(t.oid, true) AS trigger_def + FROM pg_trigger t + JOIN pg_class c + ON c.oid = t.tgrelid + JOIN pg_namespace n + ON n.oid = c.relnamespace + WHERE n.nspname = 'merchant_instances' + AND NOT t.tgisinternal + LOOP + v_new_def := replace( + r.trigger_def, + 'merchant_instances.', + my_schema_name + ); + EXECUTE v_new_def; + END LOOP; +END $$; + +COMMENT ON PROCEDURE merchant.sync_instance_procedures(BIGINT) + IS 'Synchronizes procedures and triggers for the given instance by copying the current version from merchant_instances into the per-instance SCHEMA'; + + +DROP PROCEDURE IF EXISTS merchant.sync_all_instance_procedures(); +CREATE PROCEDURE merchant.sync_all_instance_procedures() +LANGUAGE plpgsql +AS $$ +DECLARE + merchant_id BIGINT; +BEGIN + FOR merchant_id IN + SELECT merchant_serial + FROM merchant.merchant_instances + LOOP + CALL merchant.sync_instance_procedures + (merchant_id); + END LOOP; +END $$; diff --git a/src/backenddb/delete_category.c b/src/backenddb/delete_category.c @@ -19,35 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_category.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_category (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t category_id) +TALER_MERCHANTDB_delete_category ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t category_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&category_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_category", - "DELETE" - " FROM merchant_categories" - " WHERE merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND category_serial=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_categories" + " WHERE category_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_category", + "", params); } diff --git a/src/backenddb/delete_contract_terms.c b/src/backenddb/delete_contract_terms.c @@ -19,40 +19,37 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_contract_terms.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_contract_terms (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - struct GNUNET_TIME_Relative legal_expiration) +TALER_MERCHANTDB_delete_contract_terms ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + struct GNUNET_TIME_Relative legal_expiration) { struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_relative_time (&legal_expiration), GNUNET_PQ_query_param_absolute_time (&now), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_contract_terms", - "DELETE FROM merchant_contract_terms" - " WHERE order_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND ( ( (pay_deadline < $4) AND" - " (NOT paid) ) OR" - " (creation_time + $3 < $4) )"); + TMH_PQ_prepare_anon (pg, + "DELETE FROM merchant_contract_terms" + " WHERE order_id=$1" + " AND ( ( (pay_deadline < $3) AND" + " (NOT paid) ) OR" + " (creation_time + $2 < $3) )"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_contract_terms", + "", params); } diff --git a/src/backenddb/delete_donau_instance.c b/src/backenddb/delete_donau_instance.c @@ -20,32 +20,30 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_donau_instance.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_donau_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - const uint64_t donau_serial_id) +TALER_MERCHANTDB_delete_donau_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + const uint64_t donau_serial_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&donau_serial_id), - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_donau_instance", - "DELETE FROM merchant_donau_instances di" - " USING merchant_instances mi" - " WHERE di.merchant_instance_serial = mi.merchant_serial" - " AND di.donau_instances_serial = $1" - " AND mi.merchant_id = $2;"); + TMH_PQ_prepare_anon (pg, + "DELETE FROM merchant_donau_instances" + " WHERE donau_instances_serial = $1;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_donau_instance", + "", params); -} -\ No newline at end of file +} diff --git a/src/backenddb/delete_exchange_accounts.c b/src/backenddb/delete_exchange_accounts.c @@ -19,16 +19,15 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_exchange_accounts.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_exchange_accounts (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MasterPublicKeyP *master_pub) +TALER_MERCHANTDB_delete_exchange_accounts ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MasterPublicKeyP *master_pub) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (master_pub), diff --git a/src/backenddb/delete_instance_private_key.c b/src/backenddb/delete_instance_private_key.c @@ -19,15 +19,15 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_instance_private_key.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_instance_private_key (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id) +TALER_MERCHANTDB_delete_instance_private_key ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (merchant_id), @@ -36,13 +36,11 @@ TALER_MERCHANTDB_delete_instance_private_key (struct TALER_MERCHANTDB_PostgresCo check_connection (pg); PREPARE (pg, - "delete_key", - "DELETE FROM merchant_keys" - " USING merchant_instances" - " WHERE merchant_keys.merchant_serial" - " = merchant_instances.merchant_serial" - " AND merchant_instances.merchant_id = $1"); + "delete_instance_private_key", + "UPDATE merchant.merchant_instances" + " SET merchant_priv = NULL" + " WHERE merchant_id = $1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_key", + "delete_instance_private_key", params); } diff --git a/src/backenddb/delete_login_token.c b/src/backenddb/delete_login_token.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 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 @@ -19,59 +19,54 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_login_token.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_login_token_serial (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - uint64_t serial) +TALER_MERCHANTDB_delete_login_token_serial ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + uint64_t serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_uint64 (&serial), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_login_token_serial", - "DELETE FROM merchant_login_tokens" - " WHERE serial=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "DELETE FROM merchant_login_tokens" + " WHERE serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_login_token_serial", + "", params); } enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_login_token (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - const struct TALER_MERCHANTDB_LoginTokenP *token) +TALER_MERCHANTDB_delete_login_token ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + const struct TALER_MERCHANTDB_LoginTokenP *token) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_auto_from_type (token), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_login_token", - "DELETE FROM merchant_login_tokens" - " WHERE token=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "DELETE FROM merchant_login_tokens" + " WHERE token=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_login_token", + "", params); } diff --git a/src/backenddb/delete_money_pot.c b/src/backenddb/delete_money_pot.c @@ -19,35 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_money_pot.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t money_pot_id) +TALER_MERCHANTDB_delete_money_pot ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t money_pot_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&money_pot_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_money_pot", - "DELETE" - " FROM merchant_money_pots" - " WHERE merchant_money_pots.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_money_pots.money_pot_serial=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_money_pots" + " WHERE money_pot_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_money_pot", + "", params); } diff --git a/src/backenddb/delete_order.c b/src/backenddb/delete_order.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 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 @@ -19,70 +19,60 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_order.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_order (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - bool force) +TALER_MERCHANTDB_delete_order ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + bool force) { struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_absolute_time (&now), GNUNET_PQ_query_param_bool (force), GNUNET_PQ_query_param_end }; struct GNUNET_PQ_QueryParam params2[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; enum GNUNET_DB_QueryStatus qs2; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_order", - "WITH ms AS" - "(SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - ", mc AS" - "(SELECT paid" - " FROM merchant_contract_terms" - " JOIN ms USING (merchant_serial)" - " WHERE order_id=$2) " - "DELETE" - " FROM merchant_orders mo" - " WHERE order_id=$2" - " AND merchant_serial=(SELECT merchant_serial FROM ms)" - " AND ( (pay_deadline < $3)" - " OR (NOT EXISTS (SELECT paid FROM mc))" - " OR ($4 AND (FALSE=(SELECT paid FROM mc))) );"); + // FIXME: replace two statements by stored procedure! + TMH_PQ_prepare_anon (pg, + "WITH mc AS" + "(SELECT paid" + " FROM merchant_contract_terms" + " WHERE order_id=$1) " + "DELETE" + " FROM merchant_orders mo" + " WHERE order_id=$1" + " AND ( (pay_deadline < $2)" + " OR (NOT EXISTS (SELECT paid FROM mc))" + " OR ($3 AND (FALSE=(SELECT paid FROM mc))) );"); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_order", + "", params); if ( (qs < 0) || (! force) ) return qs; - PREPARE (pg, - "delete_contract", - "DELETE" - " FROM merchant_contract_terms" - " WHERE order_id=$2 AND" - " merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND NOT paid;"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_contract_terms" + " WHERE order_id=$1" + " AND NOT paid;"); qs2 = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_contract", + "", params2); if (qs2 < 0) return qs2; diff --git a/src/backenddb/delete_otp.c b/src/backenddb/delete_otp.c @@ -19,35 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_otp.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_otp (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *otp_id) +TALER_MERCHANTDB_delete_otp ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *otp_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (otp_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_otp", - "DELETE" - " FROM merchant_otp_devices" - " WHERE merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND otp_id=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_otp_devices" + " WHERE otp_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_otp", + "", params); } diff --git a/src/backenddb/delete_pending_webhook.c b/src/backenddb/delete_pending_webhook.c @@ -25,9 +25,11 @@ #include "merchant-database/delete_pending_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_pending_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t webhook_pending_serial) +TALER_MERCHANTDB_delete_pending_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t webhook_pending_serial) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&webhook_pending_serial), @@ -38,9 +40,8 @@ TALER_MERCHANTDB_delete_pending_webhook (struct TALER_MERCHANTDB_PostgresContext PREPARE (pg, "delete_pending_webhook", "DELETE" - " FROM merchant_pending_webhooks" + " FROM merchant.merchant_pending_webhooks" " WHERE webhook_pending_serial=$1"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, "delete_pending_webhook", params); diff --git a/src/backenddb/delete_product.c b/src/backenddb/delete_product.c @@ -19,38 +19,35 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_product.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_product (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *product_id) +TALER_MERCHANTDB_delete_product ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *product_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_product", - "DELETE" - " FROM merchant_inventory" - " WHERE merchant_inventory.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_inventory.product_id=$2" - " AND product_serial NOT IN " - " (SELECT product_serial FROM merchant_order_locks)" - " AND product_serial NOT IN " - " (SELECT product_serial FROM merchant_inventory_locks)"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_inventory" + " WHERE product_id=$1" + " AND product_serial NOT IN " + " (SELECT product_serial FROM merchant_order_locks)" + " AND product_serial NOT IN " + " (SELECT product_serial FROM merchant_inventory_locks)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_product", + "", params); } diff --git a/src/backenddb/delete_product_group.c b/src/backenddb/delete_product_group.c @@ -19,34 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_product_group.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_product_group (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t product_group_id) +TALER_MERCHANTDB_delete_product_group ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t product_group_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&product_group_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_product_group", - "DELETE" - " FROM merchant_product_groups" - " WHERE merchant_product_groups.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_product_groups.product_group_serial=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_product_groups" + " WHERE product_group_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_product_group", + "", params); } diff --git a/src/backenddb/delete_report.c b/src/backenddb/delete_report.c @@ -19,35 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_report.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_report (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t report_id) +TALER_MERCHANTDB_delete_report ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t report_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&report_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_report", - "DELETE" - " FROM merchant_reports" - " WHERE merchant_reports.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_reports.report_serial=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_reports" + " WHERE report_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_report", + "", params); } diff --git a/src/backenddb/delete_template.c b/src/backenddb/delete_template.c @@ -19,35 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_template.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_template (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *template_id) +TALER_MERCHANTDB_delete_template ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *template_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (template_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_template", - "DELETE" - " FROM merchant_template" - " WHERE merchant_template.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_template.template_id=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_template" + " WHERE template_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_template", + "", params); } diff --git a/src/backenddb/delete_token_family.c b/src/backenddb/delete_token_family.c @@ -19,34 +19,31 @@ * @author Christian Blättler */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_token_family.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_token_family (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *token_family_slug) +TALER_MERCHANTDB_delete_token_family ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *token_family_slug) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (token_family_slug), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_token_family", - "DELETE" - " FROM merchant_token_families" - " WHERE merchant_token_families.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND slug=$2"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_token_families" + " WHERE slug=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_token_family", + "", params); -} -\ No newline at end of file +} diff --git a/src/backenddb/delete_transfer.c b/src/backenddb/delete_transfer.c @@ -19,39 +19,30 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_transfer.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t transfer_serial_id) +TALER_MERCHANTDB_delete_transfer ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t transfer_serial_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&transfer_serial_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_transfer", - "DELETE FROM merchant_transfers" - " WHERE" - " credit_serial=$2" - " AND account_serial IN " - " (SELECT account_serial " - " FROM merchant_accounts" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); - + TMH_PQ_prepare_anon (pg, + "DELETE FROM merchant_transfers" + " WHERE credit_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_transfer", + "", params); } diff --git a/src/backenddb/delete_unit.c b/src/backenddb/delete_unit.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025 Taler Systems SA + Copyright (C) 2025 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 @@ -19,49 +19,42 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_unit.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_unit (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *unit_id, - bool *no_instance, - bool *no_unit, - bool *builtin_conflict) +TALER_MERCHANTDB_delete_unit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *unit_id, + bool *no_unit, + bool *builtin_conflict) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (unit_id), GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("out_no_instance", - no_instance), GNUNET_PQ_result_spec_bool ("out_no_unit", no_unit), GNUNET_PQ_result_spec_bool ("out_builtin_conflict", builtin_conflict), GNUNET_PQ_result_spec_end }; - enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_unit", - "SELECT" - " out_no_instance" - " ,out_no_unit" - " ,out_builtin_conflict" - " FROM merchant_do_delete_unit($1,$2);"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "delete_unit", - params, - rs); - GNUNET_PQ_cleanup_query_params_closures (params); - return qs; + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_no_unit" + " ,out_builtin_conflict" + " FROM merchant_do_delete_unit($1);"); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); } diff --git a/src/backenddb/delete_unit.sql b/src/backenddb/delete_unit.sql @@ -0,0 +1,58 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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 delete_unit.sql +-- @brief SQL for deleting units +-- @author Bohdan Potuzhnyi + + +DROP FUNCTION IF EXISTS merchant_do_delete_unit; +CREATE FUNCTION merchant_do_delete_unit ( + IN in_unit_id TEXT, + OUT out_no_instance BOOL, + OUT out_no_unit BOOL, + OUT out_builtin_conflict BOOL) + LANGUAGE plpgsql +AS $$ +DECLARE + my_unit RECORD; +BEGIN + out_no_unit := FALSE; + out_builtin_conflict := FALSE; + + SELECT * + INTO my_unit + FROM merchant_custom_units + WHERE unit = in_unit_id + FOR UPDATE; + + IF NOT FOUND THEN + IF EXISTS (SELECT 1 + FROM merchant.merchant_builtin_units bu + WHERE bu.unit = in_unit_id) + THEN + out_builtin_conflict := TRUE; + ELSE + out_no_unit := TRUE; + END IF; + RETURN; + END IF; + + DELETE + FROM merchant_custom_units + WHERE unit_serial = my_unit.unit_serial; + + RETURN; +END $$; diff --git a/src/backenddb/delete_webhook.c b/src/backenddb/delete_webhook.c @@ -19,35 +19,31 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/delete_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_delete_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *webhook_id) +TALER_MERCHANTDB_delete_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *webhook_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (webhook_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "delete_webhook", - "DELETE" - " FROM merchant_webhook" - " WHERE merchant_webhook.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_webhook.webhook_id=$2"); - + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_webhook" + " WHERE webhook_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_webhook", + "", params); } diff --git a/src/backenddb/drop_tables.c b/src/backenddb/drop_tables.c @@ -22,6 +22,7 @@ #include "merchantdb_lib.h" #include "merchant-database/drop_tables.h" + enum GNUNET_GenericReturnValue TALER_MERCHANTDB_drop_tables (const struct GNUNET_CONFIGURATION_Handle *cfg) { diff --git a/src/backenddb/example-statistics-0001.sql b/src/backenddb/example-statistics-0001.sql @@ -43,7 +43,7 @@ VALUES ('deposits' ,'sales (before refunds)' ,'amount' - ,ARRAY['second'::statistic_range, 'minute', 'hour', 'day', 'month', 'quarter', 'year'] + ,ARRAY['second'::merchant.statistic_range, 'minute', 'hour', 'day', 'month', 'quarter', 'year'] ,ARRAY[120, 120, 48, 95, 36, 40, 100] -- track last 120 s, 120 minutes, 48 hours, 95 days, 36 months, 40 quarters & 10 years ); @@ -76,7 +76,7 @@ VALUES ('products-sold' ,'products sold (only those tracked in inventory)' ,'number' - ,ARRAY['second'::statistic_range, 'minute' 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY['second'::merchant.statistic_range, 'minute' 'day', 'week', 'month', 'quarter', 'year'] ,ARRAY[120, 120, 60, 12, 24, 8, 10] -- track last 120s, 120 minutes, 60 days, 12 weeks, 24 months, 8 quarters and 10 years ); @@ -92,7 +92,6 @@ BEGIN THEN CALL merchant_do_bump_number_stat ('products-sold' - ,NEW.merchant_serial ,CURRENT_TIMESTAMP(0)::TIMESTAMP ,my_sold); END IF; diff --git a/src/backenddb/expire_locks.c b/src/backenddb/expire_locks.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/expire_locks.h" #include "helper.h" @@ -34,52 +32,30 @@ TALER_MERCHANTDB_expire_locks (struct TALER_MERCHANTDB_PostgresContext *pg) GNUNET_PQ_query_param_absolute_time (&now), GNUNET_PQ_query_param_end }; - enum GNUNET_DB_QueryStatus qs1; - enum GNUNET_DB_QueryStatus qs2; - enum GNUNET_DB_QueryStatus qs3; + uint64_t total; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("expire_locks", + &total), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, - "unlock_products", - "DELETE FROM merchant_inventory_locks" - " WHERE expiration < $1"); - qs1 = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "unlock_products", - params); - if (qs1 < 0) - { - GNUNET_break (0); - return qs1; - } - PREPARE (pg, - "unlock_orders", - "DELETE FROM merchant_orders" - " WHERE pay_deadline < $1"); - qs2 = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "unlock_orders", - params); - if (qs2 < 0) - { - GNUNET_break (0); - return qs2; - } - PREPARE (pg, - "unlock_contracts", - "DELETE FROM merchant_contract_terms" - " WHERE NOT paid" - " AND pay_deadline < $1"); - qs3 = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "unlock_contracts", - params); - if (qs3 < 0) + "expire_locks", + "SELECT expire_locks($1) " + " AS expire_locks"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "expire_locks", + params, + rs); + if (qs < 0) { GNUNET_break (0); - return qs3; + return qs; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Released %d+%d+%d locks\n", - qs1, - qs2, - qs3); - return qs1 + qs2 + qs3; + "Released %llu locks across all instances\n", + (unsigned long long) total); + return (enum GNUNET_DB_QueryStatus) total; } diff --git a/src/backenddb/expire_locks.sql b/src/backenddb/expire_locks.sql @@ -0,0 +1,53 @@ +-- +-- 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/> +-- + + +DROP FUNCTION IF EXISTS expire_locks; +CREATE FUNCTION expire_locks( + IN in_now INT8 +) +RETURNS INT8 +LANGUAGE plpgsql +AS $$ +DECLARE + rec RECORD; + s TEXT; + total INT8 := 0; + affected INT8; +BEGIN + DELETE FROM merchant_inventory_locks + WHERE expiration < in_now; + GET DIAGNOSTICS affected = ROW_COUNT; + total := total + affected; + + DELETE FROM merchant_orders + WHERE pay_deadline < in_now; + GET DIAGNOSTICS affected = ROW_COUNT; + total := total + affected; + + DELETE FROM merchant_contract_terms + WHERE NOT paid + AND pay_deadline < in_now; + GET DIAGNOSTICS affected = ROW_COUNT; + total := total + affected; + RETURN total; +END +$$; + +COMMENT ON FUNCTION expire_locks(INT8) + IS 'DELETEs expired inventory locks, unpaid orders past their' + ' pay_deadline, and unpaid contracts past their' + ' pay_deadline. Returns the total number of rows deleted.'; diff --git a/src/backenddb/finalize_transfer_status.c b/src/backenddb/finalize_transfer_status.c @@ -19,22 +19,21 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/finalize_transfer_status.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_finalize_transfer_status (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct GNUNET_HashCode *h_details, - const struct TALER_Amount *total_amount, - const struct TALER_Amount *wire_fee, - const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_ExchangeSignatureP *exchange_sig) +TALER_MERCHANTDB_finalize_transfer_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct GNUNET_HashCode *h_details, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (wtid), @@ -49,30 +48,30 @@ TALER_MERCHANTDB_finalize_transfer_status (struct TALER_MERCHANTDB_PostgresConte GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "finalize_transfer_status", - "WITH subquery AS (" - " SELECT signkey_serial" - " FROM merchant_exchange_signing_keys" - " WHERE exchange_pub=$6" - ")" - "UPDATE merchant_expected_transfers SET" - " last_http_status=200" - ",last_ec=0" - ",last_detail=NULL" - ",retry_needed=FALSE" - ",retry_time=0" - ",expected_credit_amount=$3" - ",wire_fee=$4" - ",h_details=$5" - ",signkey_serial=subquery.signkey_serial" - ",exchange_sig=$7" - " FROM subquery" - " WHERE wtid=$1" - " AND exchange_url=$2"); + TMH_PQ_prepare_anon (pg, + "WITH subquery AS (" + " SELECT signkey_serial" + " FROM merchant.merchant_exchange_signing_keys" + " WHERE exchange_pub=$6" + ")" + "UPDATE merchant_expected_transfers SET" + " last_http_status=200" + ",last_ec=0" + ",last_detail=NULL" + ",retry_needed=FALSE" + ",retry_time=0" + ",expected_credit_amount=$3" + ",wire_fee=$4" + ",h_details=$5" + ",signkey_serial=subquery.signkey_serial" + ",exchange_sig=$7" + " FROM subquery" + " WHERE wtid=$1" + " AND exchange_url=$2"); return GNUNET_PQ_eval_prepared_non_select ( pg->conn, - "finalize_transfer_status", + "", params); } diff --git a/src/backenddb/future.sql b/src/backenddb/future.sql @@ -19,7 +19,7 @@ CREATE TABLE tan_challenges ,confirmation_date INT8 DEFAULT NULL ,retry_counter INT4 NOT NULL ,merchant_serial INT8 NOT NULL - REFERENCES merchant_instances(merchant_serial) + REFERENCES merchant.merchant_instances(merchant_serial) ON DELETE CASCADE ,tan_channel tan_enum NULL DEFAULT NULL ,tan_info TEXT NULL DEFAULT NULL -- FIXME: when is this NULL? diff --git a/src/backenddb/gc.c b/src/backenddb/gc.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/gc.h" #include "helper.h" @@ -44,7 +42,7 @@ TALER_MERCHANTDB_gc (struct TALER_MERCHANTDB_PostgresContext *pg) }; struct GNUNET_PQ_PreparedStatement ps[] = { GNUNET_PQ_make_prepare ("run_gc", - "CALL merchant_do_gc ($1);"), + "CALL merchant.merchant_do_gc ($1);"), GNUNET_PQ_PREPARED_STATEMENT_END }; diff --git a/src/backenddb/gc.sql b/src/backenddb/gc.sql @@ -0,0 +1,64 @@ +-- +-- 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/> +-- + +DROP PROCEDURE IF EXISTS merchant_do_gc; +CREATE PROCEDURE merchant_do_gc(in_now INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + rec RECORD; + s TEXT; +BEGIN + -- Drop validation-pending instances that never confirmed in time. The + -- AFTER DELETE trigger on merchant.merchant_instances will DROP the + -- per-instance schema for each removed row. + DELETE FROM merchant.merchant_instances + WHERE validation_needed + AND validation_expiration < in_now; + + -- Per-instance GC: loop over all surviving instances and run the + -- per-instance GC helpers + targeted DELETEs in each schema. + FOR rec IN SELECT merchant_serial FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + EXECUTE format('SET LOCAL search_path TO %I, merchant', s); + CALL merchant_statistic_amount_gc (); + CALL merchant_statistic_bucket_gc (); + CALL merchant_statistic_counter_gc (); + EXECUTE format('DELETE FROM %I.merchant_unclaim_signatures' + ' WHERE expiration_time < $1', s) USING in_now; + EXCEPTION + WHEN undefined_table THEN + NULL; + WHEN undefined_function THEN + NULL; + END; + END LOOP; + DELETE FROM merchant.tan_challenges + WHERE expiration_date < $1; + -- Restore the global search_path so subsequent statements on this + -- session don't leak into the last visited instance schema. + SET LOCAL search_path TO merchant; +END $$; +COMMENT ON PROCEDURE merchant_do_gc + IS 'Calls per-instance garbage collection subroutines across every instance.' + ' Removes expired pending-validation instances first (whose ON DELETE' + ' trigger drops the entire per-instance schema), then for each surviving' + ' instance runs merchant_statistic_*_gc and DELETEs expired tan_challenges' + ' / merchant_unclaim_signatures.'; + +COMMIT; diff --git a/src/backenddb/get_kyc_limits.c b/src/backenddb/get_kyc_limits.c @@ -19,24 +19,23 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/get_kyc_limits.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_get_kyc_limits (struct TALER_MERCHANTDB_PostgresContext *pg, - struct TALER_FullPayto merchant_account_uri, - const char *instance_id, - const char *exchange_url, - bool *kyc_ok, - bool *no_access_token, - json_t **jlimits) +TALER_MERCHANTDB_get_kyc_limits ( + struct TALER_MERCHANTDB_PostgresContext *pg, + struct TALER_FullPayto merchant_account_uri, + const char *instance_id, + const char *exchange_url, + bool *kyc_ok, + bool *no_access_token, + json_t **jlimits) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (merchant_account_uri.full_payto), - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_end }; @@ -52,26 +51,24 @@ TALER_MERCHANTDB_get_kyc_limits (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "get_kyc_limits", - "SELECT" - " mk.kyc_ok" - ",mk.jaccount_limits::TEXT" - ",mk.access_token IS NULL AS no_access_token" - " FROM merchant_kyc mk" - " WHERE mk.exchange_url=$3" - " AND mk.account_serial=" - " (SELECT account_serial" - " FROM merchant_accounts" - " WHERE payto_uri=$1" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$2));"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mk.kyc_ok" + ",mk.jaccount_limits::TEXT" + ",mk.access_token IS NULL AS no_access_token" + " FROM merchant_kyc mk" + " WHERE mk.exchange_url=$2" + " AND mk.account_serial=" + " (SELECT account_serial" + " FROM merchant_accounts" + " WHERE payto_uri=$1);"); *jlimits = NULL; return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "get_kyc_limits", + "", params, rs); } diff --git a/src/backenddb/get_kyc_status.c b/src/backenddb/get_kyc_status.c @@ -19,33 +19,31 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/get_kyc_status.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_get_kyc_status (struct TALER_MERCHANTDB_PostgresContext *pg, - struct TALER_FullPayto merchant_account_uri, - const char *instance_id, - const char *exchange_url, - bool *auth_ok, - struct TALER_AccountAccessTokenP *access_token, - bool *kyc_ok, - unsigned int *last_http_status, - enum TALER_ErrorCode *last_ec, - uint64_t *rule_gen, - struct GNUNET_TIME_Timestamp *last_kyc_check, - struct GNUNET_TIME_Absolute *next_kyc_poll, - struct GNUNET_TIME_Relative *kyc_backoff, - bool *aml_review, - json_t **jlimits) +TALER_MERCHANTDB_get_kyc_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + struct TALER_FullPayto merchant_account_uri, + const char *instance_id, + const char *exchange_url, + bool *auth_ok, + struct TALER_AccountAccessTokenP *access_token, + bool *kyc_ok, + unsigned int *last_http_status, + enum TALER_ErrorCode *last_ec, + uint64_t *rule_gen, + struct GNUNET_TIME_Timestamp *last_kyc_check, + struct GNUNET_TIME_Absolute *next_kyc_poll, + struct GNUNET_TIME_Relative *kyc_backoff, + bool *aml_review, + json_t **jlimits) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (merchant_account_uri.full_payto), - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_end }; @@ -81,33 +79,31 @@ TALER_MERCHANTDB_get_kyc_status (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "get_kyc_status", - "SELECT" - " mk.access_token" - ",mk.exchange_http_status" - ",mk.exchange_ec_code" - ",mk.kyc_ok" - ",mk.last_rule_gen" - ",mk.kyc_timestamp" - ",mk.next_kyc_poll" - ",mk.kyc_backoff" - ",mk.aml_review" - ",mk.jaccount_limits::TEXT" - " FROM merchant_kyc mk" - " WHERE mk.exchange_url=$3" - " AND mk.account_serial=" - " (SELECT account_serial" - " FROM merchant_accounts" - " WHERE payto_uri=$1" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$2));"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mk.access_token" + ",mk.exchange_http_status" + ",mk.exchange_ec_code" + ",mk.kyc_ok" + ",mk.last_rule_gen" + ",mk.kyc_timestamp" + ",mk.next_kyc_poll" + ",mk.kyc_backoff" + ",mk.aml_review" + ",mk.jaccount_limits::TEXT" + " FROM merchant_kyc mk" + " WHERE mk.exchange_url=$2" + " AND mk.account_serial=" + " (SELECT account_serial" + " FROM merchant_accounts" + " WHERE payto_uri=$1);"); *jlimits = NULL; qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "get_kyc_status", + "", params, rs); *last_ec = (enum TALER_ErrorCode) (int) e32; diff --git a/src/backenddb/helper.h b/src/backenddb/helper.h @@ -24,6 +24,7 @@ #include <gnunet/gnunet_util_lib.h> #include <gnunet/gnunet_db_lib.h> #include <gnunet/gnunet_time_lib.h> +#include <taler/taler_util.h> /** * Type of the "cls" argument given to each of the functions in @@ -53,6 +54,29 @@ struct TALER_MERCHANTDB_PostgresContext const char *transaction_name; /** + * Instance id ("merchant_id") that the search_path is currently + * pointing at, or NULL if no per-instance schema is selected. + * Owned by this struct; set by TALER_MERCHANTDB_set_instance(). + */ + char *current_merchant_id; + + /** + * merchant_serial corresponding to @e current_merchant_id, or 0 + * if no per-instance schema is selected. Used as the suffix in + * per-instance prepared-statement names. + */ + uint64_t current_merchant_serial; + + /** + * Public key of the currently selected instance. Populated by + * TALER_MERCHANTDB_set_instance() together with @e current_merchant_id + * and @e current_merchant_serial. Used by call sites that emit + * cross-process events (e.g. order-pay notifications) which carry the + * instance public key in their payload. + */ + struct TALER_MerchantPublicKeyP current_merchant_pub; + + /** * How many times have we connected to the DB. */ uint64_t prep_gen; @@ -93,13 +117,33 @@ struct TALER_MERCHANTDB_PostgresContext /** + * Prepares SQL statement @a sql under no name ("") for + * connection @a pg. Useful for prepared statements that + * should not be cached. + * Returns with #GNUNET_DB_STATUS_HARD_ERROR on failure. + * + * @param pg a `struct TALER_MERCHANTDB_PostgresContext` + * @param sql actual SQL text + */ +#define TMH_PQ_prepare_anon(pg,sql) \ + do { \ + if (GNUNET_OK != \ + GNUNET_PQ_prepare_anon (pg->conn, \ + sql)) { \ + GNUNET_break (0); \ + return GNUNET_DB_STATUS_HARD_ERROR; \ + } } while (0) + + +/** * Check that the database connection is still up and automatically reconnects * unless we are already inside of a transaction. * * @param pg connection to check */ void -TALER_MERCHANTDB_check_connection (struct TALER_MERCHANTDB_PostgresContext *pg); +TALER_MERCHANTDB_check_connection ( + struct TALER_MERCHANTDB_PostgresContext *pg); #endif diff --git a/src/backenddb/inactivate_account.c b/src/backenddb/inactivate_account.c @@ -19,19 +19,18 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/inactivate_account.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_inactivate_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire) +TALER_MERCHANTDB_inactivate_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id, + const struct TALER_MerchantWireHashP *h_wire) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (merchant_id), GNUNET_PQ_query_param_auto_from_type (h_wire), GNUNET_PQ_query_param_end }; @@ -43,14 +42,16 @@ TALER_MERCHANTDB_inactivate_account (struct TALER_MERCHANTDB_PostgresContext *pg }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (merchant_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "inactivate_account", - "SELECT out_found AS found" - " FROM merchant_do_inactivate_account" - " ($1,$2);"); + TMH_PQ_prepare_anon (pg, + "SELECT out_found AS found" + " FROM merchant_do_inactivate_account" + " ($1);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "inactivate_account", + "", params, rs); if (qs < 0) diff --git a/src/backenddb/inactivate_account.sql b/src/backenddb/inactivate_account.sql @@ -0,0 +1,36 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_inactivate_account; +CREATE FUNCTION merchant_do_inactivate_account ( + IN in_h_wire BYTEA + ,OUT out_found BOOL +) +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE merchant_accounts + SET active=FALSE + WHERE h_wire=in_h_wire; + out_found = FOUND; + IF out_found + THEN + -- Notify taler-merchant-kyccheck about the change in + -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; + END IF; + +END $$; diff --git a/src/backenddb/increase_refund.c b/src/backenddb/increase_refund.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/increase_refund.h" #include "helper.h" @@ -133,6 +131,7 @@ struct InsertRefundContext * due to legal limits? */ bool legal_capped; + }; @@ -316,6 +315,50 @@ process_refund_cb (void *cls, /** + * Helper function to prepare statement to select refunds + * + * @param pg context to prepare statement in + * @return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS on success + */ +static enum GNUNET_DB_QueryStatus +prep_select_refund (struct TALER_MERCHANTDB_PostgresContext *pg) +{ + TMH_PQ_prepare_anon (pg, + "SELECT" + " refund_amount" + ",rtransaction_id" + " FROM merchant_refunds" + " WHERE coin_pub=$1" + " AND order_serial=$2"); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; +} + + +/** + * Helper function to prepare statement to insert refund + * + * @param pg context to prepare statement in + * @return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS on success + */ +static enum GNUNET_DB_QueryStatus +prep_insert_refund (struct TALER_MERCHANTDB_PostgresContext *pg) +{ + // FIXME: return 'refund_serial' from this INSERT statement for #10577 + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_refunds" + "(order_serial" + ",rtransaction_id" + ",refund_timestamp" + ",coin_pub" + ",reason" + ",refund_amount" + ") VALUES" + "($1, $2, $3, $4, $5, $6)"); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; +} + + +/** * Function to be called with the results of a SELECT statement * that has returned @a num_results results. * @@ -358,7 +401,7 @@ process_deposits_for_refund_cb (void *cls, GNUNET_PQ_result_spec_end }; struct FindRefundContext ictx = { - .pg = pg + .pg = pg, }; struct ExchangeLimit *el; @@ -393,9 +436,16 @@ process_deposits_for_refund_cb (void *cls, TALER_amount_set_zero ( ctx->refund->currency, &ictx.refunded_amount)); + ires = prep_select_refund (pg); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != ires) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + goto cleanup; + } ires = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "find_refunds_by_coin", + "", params, &process_refund_cb, &ictx); @@ -559,8 +609,15 @@ process_deposits_for_refund_cb (void *cls, }; check_connection (pg); + qs = prep_insert_refund (pg); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + goto cleanup; + } qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_refund", + "", params); switch (qs) { @@ -619,7 +676,6 @@ TALER_MERCHANTDB_increase_refund ( { enum GNUNET_DB_QueryStatus qs; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; @@ -628,54 +684,32 @@ TALER_MERCHANTDB_increase_refund ( .refund = refund, .olc = olc, .olc_cls = olc_cls, - .reason = reason + .reason = reason, }; - // FIXME: return 'refund_serial' from this INSERT statement for #10577 - PREPARE (pg, - "insert_refund", - "INSERT INTO merchant_refunds" - "(order_serial" - ",rtransaction_id" - ",refund_timestamp" - ",coin_pub" - ",reason" - ",refund_amount" - ") VALUES" - "($1, $2, $3, $4, $5, $6)"); - PREPARE (pg, - "find_refunds_by_coin", - "SELECT" - " refund_amount" - ",rtransaction_id" - " FROM merchant_refunds" - " WHERE coin_pub=$1" - " AND order_serial=$2"); - PREPARE (pg, - "find_deposits_for_refund", - "SELECT" - " dep.coin_pub" - ",dco.order_serial" - ",dep.amount_with_fee" - ",dco.exchange_url" - " FROM merchant_deposits dep" - " JOIN merchant_deposit_confirmations dco" - " USING (deposit_confirmation_serial)" - " WHERE order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE order_id=$2" - " AND paid" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + TMH_PQ_prepare_anon (pg, + "SELECT" + " dep.coin_pub" + ",dco.order_serial" + ",dep.amount_with_fee" + ",dco.exchange_url" + " FROM merchant_deposits dep" + " JOIN merchant_deposit_confirmations dco" + " USING (deposit_confirmation_serial)" + " WHERE order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE order_id=$1" + " AND paid)"); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Asked to refund %s on order %s\n", TALER_amount2s (refund), order_id); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "find_deposits_for_refund", + "", params, &process_deposits_for_refund_cb, &ctx); diff --git a/src/backenddb/increment_money_pots.c b/src/backenddb/increment_money_pots.c @@ -27,14 +27,14 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_increment_money_pots (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - size_t money_pots_len, - const uint64_t *money_pot_ids, - const struct TALER_Amount *pot_increments) +TALER_MERCHANTDB_increment_money_pots ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + size_t money_pots_len, + const uint64_t *money_pot_ids, + const struct TALER_Amount *pot_increments) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_array_uint64 (money_pots_len, money_pot_ids, pg->conn), @@ -51,15 +51,17 @@ TALER_MERCHANTDB_increment_money_pots (struct TALER_MERCHANTDB_PostgresContext * }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "increment_money_pots", - "SELECT" - " out_not_found AS not_found" - " FROM merchant_do_increment_money_pots" - "($1,$2,$3);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_not_found AS not_found" + " FROM merchant_do_increment_money_pots" + "($1,$2);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "increment_money_pots", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/increment_money_pots.sql b/src/backenddb/increment_money_pots.sql @@ -0,0 +1,93 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_increment_money_pots; +CREATE FUNCTION merchant_do_increment_money_pots ( + IN ina_money_pots_ids INT8[], + IN ina_increments merchant.taler_amount_currency[], + OUT out_not_found BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + i INT; + ini_current_pot_id INT8; + ini_current_increment merchant.taler_amount_currency; + my_totals merchant.taler_amount_currency[]; + currency_found BOOL; + j INT; +BEGIN + +out_not_found = FALSE; + +IF ( COALESCE(array_length(ina_money_pots_ids, 1), 0) != + COALESCE(array_length(ina_increments, 1), 0) ) +THEN + RAISE EXCEPTION 'Array lengths must match'; +END IF; + +FOR i IN 1..COALESCE(array_length(ina_money_pots_ids, 1),0) + LOOP + ini_current_pot_id = ina_money_pots_ids[i]; + ini_current_increment = ina_increments[i]; + + SELECT pot_totals + INTO my_totals + FROM merchant_money_pots + WHERE money_pot_serial = ini_current_pot_id; + + IF NOT FOUND + THEN + -- If pot does not exist, we just ignore the entire + -- requested increment, but update the return value. + -- (We may have other pots to update, so we continue + -- to iterate!). + out_not_found = TRUE; + ELSE + -- Check if currency exists in pot_totals and update + currency_found = FALSE; + + FOR j IN 1..COALESCE(array_length(my_totals, 1), 0) + LOOP + IF (my_totals[j]).curr = (ini_current_increment).curr + THEN + my_totals[j].frac + = my_totals[j].frac + ini_current_increment.frac; + my_totals[j].val + = my_totals[j].val + ini_current_increment.val; + IF my_totals[j].frac >= 100000000 + THEN + my_totals[j].frac = my_totals[j].frac - 100000000; + my_totals[j].val = my_totals[j].val + 1; + END IF; + currency_found = TRUE; + EXIT; -- break out of loop + END IF; + END LOOP; + + IF NOT currency_found + THEN + my_totals = array_append(my_totals, ini_current_increment); + END IF; + + UPDATE merchant_money_pots + SET pot_totals = my_totals + WHERE money_pot_serial = ini_current_pot_id; + + END IF; + END LOOP; + +END $$; diff --git a/src/backenddb/insert_account.c b/src/backenddb/insert_account.c @@ -27,11 +27,11 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MERCHANTDB_AccountDetails *account_details) +TALER_MERCHANTDB_insert_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MERCHANTDB_AccountDetails *account_details) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (account_details->instance_id), GNUNET_PQ_query_param_auto_from_type (&account_details->h_wire), GNUNET_PQ_query_param_auto_from_type (&account_details->salt), GNUNET_PQ_query_param_string (account_details->payto_uri.full_payto), @@ -49,29 +49,28 @@ TALER_MERCHANTDB_insert_account (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (account_details->instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_account", - "INSERT INTO merchant_accounts AS ma" - "(merchant_serial" - ",h_wire" - ",salt" - ",payto_uri" - ",credit_facade_url" - ",credit_facade_credentials" - ",active" - ",extra_wire_subject_metadata)" - " SELECT merchant_serial, $2, $3, $4, $5, $6::TEXT::JSONB, $7, $8" - " FROM merchant_instances" - " WHERE merchant_id=$1" - " ON CONFLICT(merchant_serial,payto_uri)" - " DO UPDATE SET" - " active = true" - ",credit_facade_url = EXCLUDED.credit_facade_url" - ",credit_facade_credentials = EXCLUDED.credit_facade_credentials" - ",extra_wire_subject_metadata = EXCLUDED.extra_wire_subject_metadata" - " WHERE NOT ma.active"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_accounts AS ma" + "(h_wire" + ",salt" + ",payto_uri" + ",credit_facade_url" + ",credit_facade_credentials" + ",active" + ",extra_wire_subject_metadata)" + " VALUES ($1, $2, $3, $4, $5::TEXT::JSONB, $6, $7)" + " ON CONFLICT(payto_uri)" + " DO UPDATE SET" + " active = TRUE" + ",credit_facade_url = EXCLUDED.credit_facade_url" + ",credit_facade_credentials = EXCLUDED.credit_facade_credentials" + ",extra_wire_subject_metadata = EXCLUDED.extra_wire_subject_metadata" + " WHERE NOT ma.active"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_account", + "", params); } diff --git a/src/backenddb/insert_category.c b/src/backenddb/insert_category.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2024, 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 @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_category.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_category (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *category_name, - const json_t *category_name_i18n, - uint64_t *category_id) +TALER_MERCHANTDB_insert_category ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t *category_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (category_name), TALER_PQ_query_param_json (category_name_i18n), GNUNET_PQ_query_param_end @@ -45,21 +43,19 @@ TALER_MERCHANTDB_insert_category (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_category", - "INSERT INTO merchant_categories" - "(merchant_serial" - ",category_name" - ",category_name_i18n" - ")" - " SELECT merchant_serial," - " $2, $3::TEXT::JSONB" - " FROM merchant_instances" - " WHERE merchant_id=$1" - " RETURNING category_serial"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_categories" + "(category_name" + ",category_name_i18n" + ")" + " VALUES ($1, $2::TEXT::JSONB)" + " RETURNING category_serial"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_category", + "", params, rs); } diff --git a/src/backenddb/insert_contract_terms.c b/src/backenddb/insert_contract_terms.c @@ -19,18 +19,17 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_contract_terms.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_contract_terms (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - json_t *contract_terms, - uint64_t *order_serial) +TALER_MERCHANTDB_insert_contract_terms ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + json_t *contract_terms, + uint64_t *order_serial) { struct GNUNET_TIME_Timestamp pay_deadline; struct GNUNET_TIME_Timestamp refund_deadline; @@ -71,10 +70,12 @@ TALER_MERCHANTDB_insert_contract_terms (struct TALER_MERCHANTDB_PostgresContext fulfillment_url = json_string_value (json_object_get (contract_terms, "fulfillment_url")); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), TALER_PQ_query_param_json (contract_terms), GNUNET_PQ_query_param_auto_from_type (&h_contract_terms), @@ -90,43 +91,37 @@ TALER_MERCHANTDB_insert_contract_terms (struct TALER_MERCHANTDB_PostgresContext order_serial), GNUNET_PQ_result_spec_end }; - PREPARE (pg, - "insert_contract_terms", - "INSERT INTO merchant_contract_terms" - "(order_serial" - ",merchant_serial" - ",order_id" - ",contract_terms" - ",h_contract_terms" - ",creation_time" - ",pay_deadline" - ",refund_deadline" - ",fulfillment_url" - ",claim_token" - ",pos_key" - ",pos_algorithm)" - "SELECT" - " mo.order_serial" - ",mo.merchant_serial" - ",mo.order_id" - ",$3::TEXT::JSONB" /* contract_terms */ - ",$4" /* h_contract_terms */ - ",mo.creation_time" - ",$5" /* pay_deadline */ - ",$6" /* refund_deadline */ - ",$7" /* fulfillment_url */ - ",mo.claim_token" - ",mo.pos_key" - ",mo.pos_algorithm" - " FROM merchant_orders mo" - " WHERE order_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " RETURNING order_serial"); + + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_contract_terms" + "(order_serial" + ",order_id" + ",contract_terms" + ",h_contract_terms" + ",creation_time" + ",pay_deadline" + ",refund_deadline" + ",fulfillment_url" + ",claim_token" + ",pos_key" + ",pos_algorithm)" + "SELECT" + " mo.order_serial" + ",mo.order_id" + ",$2::TEXT::JSONB" /* contract_terms */ + ",$3" /* h_contract_terms */ + ",mo.creation_time" + ",$4" /* pay_deadline */ + ",$5" /* refund_deadline */ + ",$6" /* fulfillment_url */ + ",mo.claim_token" + ",mo.pos_key" + ",mo.pos_algorithm" + " FROM merchant_orders mo" + " WHERE order_id=$1" + " RETURNING order_serial"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_contract_terms", + "", params, rs); } diff --git a/src/backenddb/insert_deposit.c b/src/backenddb/insert_deposit.c @@ -20,23 +20,22 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_deposit.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_deposit (struct TALER_MERCHANTDB_PostgresContext *pg, - uint32_t offset, - uint64_t deposit_confirmation_serial, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_CoinSpendSignatureP *coin_sig, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee, - struct GNUNET_TIME_Absolute check_time) +TALER_MERCHANTDB_insert_deposit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint32_t offset, + uint64_t deposit_confirmation_serial, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_CoinSpendSignatureP *coin_sig, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_Amount *refund_fee, + struct GNUNET_TIME_Absolute check_time) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&deposit_confirmation_serial), @@ -58,21 +57,20 @@ TALER_MERCHANTDB_insert_deposit (struct TALER_MERCHANTDB_PostgresContext *pg, "Storing deposit for coin_pub: `%s', amount_with_fee: %s\n", TALER_B2S (coin_pub), TALER_amount2s (amount_with_fee)); - check_connection (pg); - PREPARE (pg, - "insert_deposit", - "INSERT INTO merchant_deposits" - "(deposit_confirmation_serial" - ",coin_offset" - ",coin_pub" - ",coin_sig" - ",amount_with_fee" - ",deposit_fee" - ",refund_fee" - ",settlement_retry_time" - ") VALUES ($1,$2,$3,$4,$5,$6,$7,$8)" - " ON CONFLICT DO NOTHING;"); + GNUNET_assert (NULL != pg->current_merchant_id); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_deposits" + "(deposit_confirmation_serial" + ",coin_offset" + ",coin_pub" + ",coin_sig" + ",amount_with_fee" + ",deposit_fee" + ",refund_fee" + ",settlement_retry_time" + ") VALUES ($1,$2,$3,$4,$5,$6,$7,$8)" + " ON CONFLICT DO NOTHING;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_deposit", + "", params); } diff --git a/src/backenddb/insert_deposit_confirmation.c b/src/backenddb/insert_deposit_confirmation.c @@ -19,27 +19,25 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_deposit_confirmation.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_deposit_confirmation (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - struct GNUNET_TIME_Timestamp deposit_timestamp, - const struct TALER_PrivateContractHashP * - h_contract_terms, - const char *exchange_url, - struct GNUNET_TIME_Timestamp wire_transfer_deadline, - const struct TALER_Amount *total_without_fees, - const struct TALER_Amount *wire_fee, - const struct TALER_MerchantWireHashP *h_wire, - const struct TALER_ExchangeSignatureP *exchange_sig, - const struct TALER_ExchangePublicKeyP *exchange_pub, - uint64_t *deposit_confirmation_serial_id) +TALER_MERCHANTDB_insert_deposit_confirmation ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + struct GNUNET_TIME_Timestamp deposit_timestamp, + const struct TALER_PrivateContractHashP *h_contract_terms, + const char *exchange_url, + struct GNUNET_TIME_Timestamp wire_transfer_deadline, + const struct TALER_Amount *total_without_fees, + const struct TALER_Amount *wire_fee, + const struct TALER_MerchantWireHashP *h_wire, + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_ExchangePublicKeyP *exchange_pub, + uint64_t *deposit_confirmation_serial_id) { struct GNUNET_TIME_AbsoluteNBO nbo = GNUNET_TIME_absolute_hton (wire_transfer_deadline.abs_time); @@ -47,7 +45,6 @@ TALER_MERCHANTDB_insert_deposit_confirmation (struct TALER_MERCHANTDB_PostgresCo = GNUNET_STRINGS_data_to_string_alloc (&nbo, sizeof (nbo)); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_timestamp (&deposit_timestamp), GNUNET_PQ_query_param_string (exchange_url), @@ -55,21 +52,18 @@ TALER_MERCHANTDB_insert_deposit_confirmation (struct TALER_MERCHANTDB_PostgresCo total_without_fees), TALER_PQ_query_param_amount_with_currency (pg->conn, wire_fee), - GNUNET_PQ_query_param_auto_from_type (h_wire), /* 7 */ + GNUNET_PQ_query_param_auto_from_type (h_wire), /* 6 */ GNUNET_PQ_query_param_auto_from_type (exchange_sig), GNUNET_PQ_query_param_auto_from_type (exchange_pub), GNUNET_PQ_query_param_timestamp (&wire_transfer_deadline), GNUNET_PQ_query_param_string (nbo_str), GNUNET_PQ_query_param_end }; - bool no_instance; bool no_order; bool no_account; bool no_signkey; bool conflict; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("no_instance", - &no_instance), GNUNET_PQ_result_spec_bool ("no_order", &no_order), GNUNET_PQ_result_spec_bool ("no_account", @@ -84,6 +78,9 @@ TALER_MERCHANTDB_insert_deposit_confirmation (struct TALER_MERCHANTDB_PostgresCo }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); /* no preflight check here, run in transaction by caller! */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Storing deposit confirmation for instance `%s' h_contract_terms `%s', total_without_fees: %s and wire transfer deadline in %s\n", @@ -95,19 +92,17 @@ TALER_MERCHANTDB_insert_deposit_confirmation (struct TALER_MERCHANTDB_PostgresCo wire_transfer_deadline.abs_time), true)); check_connection (pg); - PREPARE (pg, - "insert_deposit_confirmation", - "SELECT " - " out_no_instance AS no_instance" - " ,out_no_account AS no_account" - " ,out_no_order AS no_order" - " ,out_no_signkey AS no_signkey" - " ,out_conflict AS conflict" - " ,out_deposit_confirmation_serial AS deposit_confirmation_serial" - " FROM merchant_do_insert_deposit_confirmation" - " ($1, $2 ,$3, $4, $5, $6, $7, $8, $9, $10, $11);"); + TMH_PQ_prepare_anon (pg, + "SELECT " + " out_no_account AS no_account" + " ,out_no_order AS no_order" + " ,out_no_signkey AS no_signkey" + " ,out_conflict AS conflict" + " ,out_deposit_confirmation_serial AS deposit_confirmation_serial" + " FROM merchant_do_insert_deposit_confirmation" + " ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_deposit_confirmation", + "", params, rs); GNUNET_free (nbo_str); @@ -116,11 +111,6 @@ TALER_MERCHANTDB_insert_deposit_confirmation (struct TALER_MERCHANTDB_PostgresCo return qs; // FIXME: in the future, return these codes to the client and // return more specific error codes to the client from the API! - if (no_instance) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } if (no_order) { GNUNET_break (0); diff --git a/src/backenddb/insert_deposit_confirmation.sql b/src/backenddb/insert_deposit_confirmation.sql @@ -0,0 +1,154 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_insert_deposit_confirmation; +CREATE FUNCTION merchant_do_insert_deposit_confirmation ( + IN in_h_contract_terms BYTEA, + IN in_deposit_timestamp INT8, + IN in_exchange_url TEXT, + IN in_total_without_fee merchant.taler_amount_currency, + IN in_wire_fee merchant.taler_amount_currency, + IN in_h_wire BYTEA, + IN in_exchange_sig BYTEA, + IN in_exchange_pub BYTEA, + IN in_wire_transfer_deadline INT8, + IN in_notify_arg_str TEXT, + OUT out_no_order BOOL, + OUT out_no_account BOOL, + OUT out_no_signkey BOOL, + OUT out_conflict BOOL, + OUT out_deposit_confirmation_serial INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + my_order_serial INT8; + my_account_serial INT8; + my_signkey_serial INT8; + my_record RECORD; + my_bank_serial_id INT8; + my_credit_amount merchant.taler_amount_currency; +BEGIN + +out_no_order=TRUE; +out_no_account=TRUE; +out_no_signkey=TRUE; +out_conflict=FALSE; +out_deposit_confirmation_serial=0; + +SELECT account_serial + INTO my_account_serial + FROM merchant_accounts + WHERE h_wire=in_h_wire; +IF NOT FOUND +THEN + RETURN; +END IF; +out_no_account=FALSE; + +SELECT signkey_serial + INTO my_signkey_serial + FROM merchant.merchant_exchange_signing_keys + WHERE exchange_pub=in_exchange_pub + ORDER BY start_date DESC + LIMIT 1; +IF NOT FOUND +THEN + RETURN; +END IF; +out_no_signkey=FALSE; + +SELECT order_serial + INTO my_order_serial + FROM merchant_contract_terms + WHERE h_contract_terms=in_h_contract_terms; +IF NOT FOUND +THEN + RETURN; +END IF; +out_no_order=FALSE; + +SELECT deposit_confirmation_serial + ,deposit_timestamp + ,exchange_url + ,total_without_fee + ,wire_fee + ,wire_transfer_deadline + ,account_serial + INTO my_record + FROM merchant_deposit_confirmations + WHERE order_serial=my_order_serial + AND exchange_url=in_exchange_url; +IF NOT FOUND +THEN + INSERT INTO merchant_deposit_confirmations + (order_serial + ,deposit_timestamp + ,exchange_url + ,total_without_fee + ,wire_fee + ,exchange_sig + ,wire_transfer_deadline + ,signkey_serial + ,account_serial + ) VALUES ( + my_order_serial + ,in_deposit_timestamp + ,in_exchange_url + ,in_total_without_fee + ,in_wire_fee + ,in_exchange_sig + ,in_wire_transfer_deadline + ,my_signkey_serial + ,my_account_serial + ) RETURNING deposit_confirmation_serial + INTO out_deposit_confirmation_serial; +ELSE + out_deposit_confirmation_serial = my_record.deposit_confirmation_serial; + IF (in_deposit_timestamp, + in_wire_transfer_deadline, + in_wire_fee, + my_account_serial) + IS DISTINCT FROM + (my_record.deposit_timestamp, + my_record.wire_transfer_deadline, + my_record.wire_fee, + my_record.account_serial) + THEN + out_conflict = TRUE; + RETURN; + END IF; + IF ( ((in_total_without_fee).val < (my_record.total_without_fee).val) OR + ( ((in_total_without_fee).val = (my_record.total_without_fee).val) AND + ((in_total_without_fee).frac <= (my_record.total_without_fee).frac) ) ) + THEN + -- new amount smaller or did not change, do NOT update. + RETURN; + END IF; + + -- Same deposit, but total amount increased, store this! + UPDATE merchant_deposit_confirmations + SET total_without_fee = in_total_without_fee + ,exchange_sig = in_exchange_sig + ,signkey_serial = my_signkey_serial + WHERE deposit_confirmation_serial = my_record.deposit_confirmation_serial; + +END IF; + +-- Do notify on TALER_DBEVENT_MERCHANT_NEW_WIRE_DEADLINE +PERFORM pg_notify ('XBZ19D98AK2REYNX93F736A56MT14SCY2EEX7XNXQMNCQ01B121R0', + in_notify_arg_str); + +END $$; diff --git a/src/backenddb/insert_deposit_to_transfer.c b/src/backenddb/insert_deposit_to_transfer.c @@ -19,19 +19,18 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_deposit_to_transfer.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_deposit_to_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t deposit_serial, - const struct TALER_MerchantWireHashP *h_wire, - const char *exchange_url, - const struct TALER_EXCHANGE_DepositData *dd) +TALER_MERCHANTDB_insert_deposit_to_transfer ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t deposit_serial, + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + const struct TALER_EXCHANGE_DepositData *dd) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&deposit_serial), @@ -52,15 +51,15 @@ TALER_MERCHANTDB_insert_deposit_to_transfer (struct TALER_MERCHANTDB_PostgresCon GNUNET_PQ_result_spec_end }; - PREPARE (pg, - "insert_deposit_to_transfer", - "SELECT" - " out_dummy" - " FROM merchant_insert_deposit_to_transfer" - " ($1,$2,$3,$4,$5,$6,$7,$8);"); + GNUNET_assert (NULL != pg->current_merchant_id); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_dummy" + " FROM merchant_insert_deposit_to_transfer" + " ($1,$2,$3,$4,$5,$6,$7,$8);"); return GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "insert_deposit_to_transfer", + "", params, rs); } diff --git a/src/backenddb/insert_deposit_to_transfer.sql b/src/backenddb/insert_deposit_to_transfer.sql @@ -0,0 +1,136 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024, 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_insert_deposit_to_transfer; +CREATE FUNCTION merchant_insert_deposit_to_transfer ( + IN in_deposit_serial INT8, + IN in_coin_contribution merchant.taler_amount_currency, + IN in_execution_time INT8, + IN in_exchange_url TEXT, + IN in_h_wire BYTEA, + IN in_exchange_sig BYTEA, + IN in_exchange_pub BYTEA, + IN in_wtid BYTEA, + OUT out_dummy BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_signkey_serial INT8; + my_account_serial INT8; + my_decose INT8; + my_expected_credit_serial INT8; + my_wire_pending_cleared BOOL; +BEGIN + -- Just to return something (for now). + out_dummy=FALSE; + +-- Find exchange sign key +SELECT signkey_serial + INTO my_signkey_serial + FROM merchant.merchant_exchange_signing_keys + WHERE exchange_pub=in_exchange_pub + ORDER BY start_date DESC + LIMIT 1; + +IF NOT FOUND +THEN + -- Maybe 'keys' is outdated, try again in 8 hours. + UPDATE merchant_deposits + SET settlement_last_ec=2029 -- MERCHANT_EXCHANGE_SIGN_PUB_UNKNOWN + ,settlement_last_http_status=200 + ,settlement_last_detail=ENCODE(in_exchange_pub, 'hex') + ,settlement_wtid=in_wtid + ,settlement_retry_needed=TRUE + ,settlement_retry_time=(EXTRACT(epoch FROM (CURRENT_TIME + interval '8 hours')) * 1000000)::INT8 + WHERE deposit_serial=in_deposit_serial; + RETURN; +END IF; + +-- Find deposit confirmation +SELECT deposit_confirmation_serial + INTO my_decose + FROM merchant_deposits + WHERE deposit_serial=in_deposit_serial; + +-- Find merchant account +SELECT account_serial + INTO my_account_serial + FROM merchant_deposit_confirmations mdc + JOIN merchant_accounts ma + USING (account_serial) + WHERE mdc.deposit_confirmation_serial=my_decose + AND ma.h_wire=in_h_wire; + +IF NOT FOUND +THEN + -- Merchant account referenced in exchange response is unknown to us. + -- Remember fatal error and do not try again. + UPDATE merchant_deposits + SET settlement_last_ec=2558 -- MERCHANT_EXCHANGE_TRANSFERS_TARGET_ACCOUNT_UNKNOWN + ,settlement_last_http_status=200 + ,settlement_last_detail=ENCODE(in_h_wire, 'hex') + ,settlement_wtid=in_wtid + ,settlement_retry_needed=FALSE + ,settlement_coin_contribution=in_coin_contribution + ,signkey_serial=my_signkey_serial + ,settlement_exchange_sig=in_exchange_sig + WHERE deposit_serial=in_deposit_serial; + RETURN; +END IF; + + +-- Make sure wire transfer is expected. +SELECT expected_credit_serial + INTO my_expected_credit_serial + FROM merchant_expected_transfers + WHERE wtid=in_wtid + AND exchange_url=in_exchange_url + AND account_serial=my_account_serial; + +IF NOT FOUND +THEN + INSERT INTO merchant_expected_transfers + (exchange_url + ,wtid + ,account_serial + ,expected_time) + VALUES + (in_exchange_url + ,in_wtid + ,my_account_serial + ,in_execution_time) + RETURNING expected_credit_serial + INTO my_expected_credit_serial; +END IF; + +-- Finally, update merchant_deposits so we do not try again. +UPDATE merchant_deposits + SET settlement_last_ec=0 + ,settlement_last_http_status=200 + ,settlement_last_detail=NULL + ,settlement_wtid=in_wtid + ,settlement_retry_needed=FALSE + ,settlement_coin_contribution=in_coin_contribution + ,settlement_expected_credit_serial=my_expected_credit_serial + ,signkey_serial=my_signkey_serial + ,settlement_exchange_sig=in_exchange_sig + WHERE deposit_serial=in_deposit_serial; + +-- MERCHANT_WIRE_TRANSFER_EXPECTED +NOTIFY XR6849FMRD2AJFY1E2YY0GWA8GN0YT407Z66WHJB0SAKJWF8G2Q60; + +END $$; diff --git a/src/backenddb/insert_donau_instance.c b/src/backenddb/insert_donau_instance.c @@ -20,8 +20,6 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "donau/donau_service.h" #include "merchant-database/insert_donau_instance.h" @@ -29,15 +27,15 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_donau_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *donau_url, - const struct DONAU_Charity *charity, - uint64_t charity_id) +TALER_MERCHANTDB_insert_donau_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *donau_url, + const struct DONAU_Charity *charity, + uint64_t charity_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (donau_url), GNUNET_PQ_query_param_string (charity->name), - GNUNET_PQ_query_param_auto_from_type (&charity->charity_pub), GNUNET_PQ_query_param_uint64 (&charity_id), TALER_PQ_query_param_amount_with_currency (pg->conn, &charity->max_per_year), @@ -47,25 +45,26 @@ TALER_MERCHANTDB_insert_donau_instance (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_query_param_end }; + GNUNET_static_assert (sizeof (pg->current_merchant_pub) == + sizeof (charity->charity_pub) ); + GNUNET_assert (0 == + memcmp (&pg->current_merchant_pub, + &charity->charity_pub, + sizeof (pg->current_merchant_pub))); + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "insert_donau_instance", - "INSERT INTO merchant_donau_instances" - " (donau_url" - " ,charity_name" - " ,merchant_instance_serial" - " ,charity_id" - " ,charity_max_per_year" - " ,charity_receipts_to_date" - " ,current_year)" - "VALUES" - " ($1, $2," - " (SELECT merchant_serial" - " FROM merchant_instances mi" - " WHERE mi.merchant_pub = $3)," - " $4, $5, $6, $7)" - " ON CONFLICT DO NOTHING;"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_donau_instances" + " (donau_url" + " ,charity_name" + " ,charity_id" + " ,charity_max_per_year" + " ,charity_receipts_to_date" + " ,current_year)" + "VALUES" + " ($1, $2, $3, $4, $5, $6)" + " ON CONFLICT DO NOTHING;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_donau_instance", + "", params); } diff --git a/src/backenddb/insert_exchange_account.c b/src/backenddb/insert_exchange_account.c @@ -19,21 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_exchange_account.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_exchange_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MasterPublicKeyP *master_pub, - const struct TALER_FullPayto payto_uri, - const char *conversion_url, - const json_t *debit_restrictions, - const json_t *credit_restrictions, - const struct TALER_MasterSignatureP *master_sig) +TALER_MERCHANTDB_insert_exchange_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MasterPublicKeyP *master_pub, + const struct TALER_FullPayto payto_uri, + const char *conversion_url, + const json_t *debit_restrictions, + const json_t *credit_restrictions, + const struct TALER_MasterSignatureP *master_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (master_pub), diff --git a/src/backenddb/insert_exchange_keys.c b/src/backenddb/insert_exchange_keys.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_exchange_keys.h" #include "helper.h" @@ -56,9 +54,10 @@ TALER_MERCHANTDB_insert_exchange_keys ( enum GNUNET_DB_QueryStatus qs; check_connection (pg); + // FIXME: use a stored procedure instead! PREPARE (pg, "insert_exchange_keys", - "INSERT INTO merchant_exchange_keys" + "INSERT INTO merchant.merchant_exchange_keys" "(keys_json" ",first_retry" ",expiration_time" @@ -70,7 +69,7 @@ TALER_MERCHANTDB_insert_exchange_keys ( ");"); PREPARE (pg, "update_exchange_keys", - "UPDATE merchant_exchange_keys SET" + "UPDATE merchant.merchant_exchange_keys SET" /* preserve old keys if new ones failed to download */ " keys_json=COALESCE($1::TEXT::JSONB,keys_json)" ",first_retry=$2" diff --git a/src/backenddb/insert_exchange_signkey.c b/src/backenddb/insert_exchange_signkey.c @@ -19,20 +19,20 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_exchange_signkey.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_exchange_signkey (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MasterPublicKeyP *master_pub, - const struct TALER_ExchangePublicKeyP *exchange_pub, - struct GNUNET_TIME_Timestamp start_date, - struct GNUNET_TIME_Timestamp expire_date, - struct GNUNET_TIME_Timestamp end_date, - const struct TALER_MasterSignatureP *master_sig) +TALER_MERCHANTDB_insert_exchange_signkey ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MasterPublicKeyP *master_pub, + const struct TALER_ExchangePublicKeyP *exchange_pub, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp expire_date, + struct GNUNET_TIME_Timestamp end_date, + const struct TALER_MasterSignatureP *master_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (master_pub), @@ -47,7 +47,7 @@ TALER_MERCHANTDB_insert_exchange_signkey (struct TALER_MERCHANTDB_PostgresContex check_connection (pg); PREPARE (pg, "insert_exchange_signkey", - "INSERT INTO merchant_exchange_signing_keys" + "INSERT INTO merchant.merchant_exchange_signing_keys" "(master_pub" ",exchange_pub" ",start_date" diff --git a/src/backenddb/insert_instance.c b/src/backenddb/insert_instance.c @@ -20,12 +20,11 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_instance.h" #include "helper.h" + enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_insert_instance ( struct TALER_MERCHANTDB_PostgresContext *pg, @@ -37,6 +36,7 @@ TALER_MERCHANTDB_insert_instance ( { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (merchant_pub), + GNUNET_PQ_query_param_auto_from_type (merchant_priv), GNUNET_PQ_query_param_auto_from_type (&ias->auth_hash), GNUNET_PQ_query_param_auto_from_type (&ias->auth_salt), GNUNET_PQ_query_param_string (is->id), @@ -68,18 +68,13 @@ TALER_MERCHANTDB_insert_instance ( is->default_wire_transfer_rounding_interval)), GNUNET_PQ_query_param_end }; - struct GNUNET_PQ_QueryParam params_priv[] = { - GNUNET_PQ_query_param_auto_from_type (merchant_priv), - GNUNET_PQ_query_param_string (is->id), - GNUNET_PQ_query_param_end - }; - enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, "insert_instance", - "INSERT INTO merchant_instances" + "INSERT INTO merchant.merchant_instances" "(merchant_pub" + ",merchant_priv" ",auth_hash" ",auth_salt" ",merchant_id" @@ -99,22 +94,10 @@ TALER_MERCHANTDB_insert_instance ( ",validation_needed" ",default_wire_transfer_rounding_interval)" "VALUES" - "($1,$2,$3,LOWER($4),$5,$6::TEXT::JSONB,$7::TEXT::JSONB,$8,$9,$10,$11," - "$12,$13,$14,$15,$16,$17,$18,$19::time_rounder_interval)"); - PREPARE (pg, - "insert_keys", - "INSERT INTO merchant_keys" - "(merchant_priv" - ",merchant_serial)" - " SELECT $1, merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$2"); - qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_instance", - params); - if (qs <= 0) - return qs; + "($1,$2,$3,$4,LOWER($5),$6,$7::TEXT::JSONB,$8::TEXT::JSONB," + "$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19," + "$20::merchant.time_rounder_interval)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_keys", - params_priv); + "insert_instance", + params); } diff --git a/src/backenddb/insert_issued_token.c b/src/backenddb/insert_issued_token.c @@ -19,20 +19,17 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_issued_token.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_issued_token (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_PrivateContractHashP * - h_contract_terms, - const struct TALER_TokenIssuePublicKeyHashP * - h_issue_pub, - const struct TALER_BlindedTokenIssueSignature * - blind_sig) +TALER_MERCHANTDB_insert_issued_token ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, + const struct TALER_BlindedTokenIssueSignature *blind_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (h_issue_pub), @@ -51,17 +48,17 @@ TALER_MERCHANTDB_insert_issued_token (struct TALER_MERCHANTDB_PostgresContext *p }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "issued_token_insert", - "SELECT" - " out_no_family" - " ,out_existed" - " FROM merchant_do_insert_issued_token" - " ($1, $2, $3);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_no_family" + " ,out_existed" + " FROM merchant_do_insert_issued_token" + " ($1, $2, $3);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "issued_token_insert", + "", params, rs); if (qs < 0) diff --git a/src/backenddb/pg_insert_issued_token.sql b/src/backenddb/insert_issued_token.sql diff --git a/src/backenddb/insert_login_token.c b/src/backenddb/insert_login_token.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 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 @@ -19,24 +19,22 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_login_token.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_login_token (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - const struct TALER_MERCHANTDB_LoginTokenP *token, - struct GNUNET_TIME_Timestamp creation_time, - struct GNUNET_TIME_Timestamp expiration_time, - uint32_t validity_scope, - const char *description) +TALER_MERCHANTDB_insert_login_token ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + const struct TALER_MERCHANTDB_LoginTokenP *token, + struct GNUNET_TIME_Timestamp creation_time, + struct GNUNET_TIME_Timestamp expiration_time, + uint32_t validity_scope, + const char *description) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_auto_from_type (token), GNUNET_PQ_query_param_timestamp (&creation_time), GNUNET_PQ_query_param_timestamp (&expiration_time), @@ -45,21 +43,20 @@ TALER_MERCHANTDB_insert_login_token (struct TALER_MERCHANTDB_PostgresContext *pg GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_login_token", - "INSERT INTO merchant_login_tokens" - "(token" - ",creation_time" - ",expiration_time" - ",validity_scope" - ",description" - ",merchant_serial" - ")" - "SELECT $2, $3, $4, $5, $6, merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_login_tokens" + "(token" + ",creation_time" + ",expiration_time" + ",validity_scope" + ",description" + ")" + " VALUES ($1, $2, $3, $4, $5)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_login_token", + "", params); } diff --git a/src/backenddb/insert_money_pot.c b/src/backenddb/insert_money_pot.c @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_money_pot.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *name, - const char *description, - uint64_t *money_pot_id) +TALER_MERCHANTDB_insert_money_pot ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *name, + const char *description, + uint64_t *money_pot_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (name), GNUNET_PQ_query_param_string (description), GNUNET_PQ_query_param_end @@ -45,20 +43,19 @@ TALER_MERCHANTDB_insert_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_money_pot", - "INSERT INTO merchant_money_pots" - "(merchant_serial" - ",money_pot_name" - ",money_pot_description)" - " SELECT merchant_serial, $2, $3" - " FROM merchant_instances" - " WHERE merchant_id=$1" - " ON CONFLICT DO NOTHING" - " RETURNING money_pot_serial;"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_money_pots" + "(money_pot_name" + ",money_pot_description)" + " VALUES ($1, $2)" + " ON CONFLICT DO NOTHING" + " RETURNING money_pot_serial;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_money_pot", + "", params, rs); } diff --git a/src/backenddb/insert_order.c b/src/backenddb/insert_order.c @@ -20,23 +20,23 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_order.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_order (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - const char *session_id, - const struct TALER_MerchantPostDataHashP *h_post_data, - struct GNUNET_TIME_Timestamp pay_deadline, - const struct TALER_ClaimTokenP *claim_token, - const json_t *contract_terms, - const char *pos_key, - enum TALER_MerchantConfirmationAlgorithm pos_algorithm) +TALER_MERCHANTDB_insert_order ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + const char *session_id, + const struct TALER_MerchantPostDataHashP *h_post_data, + struct GNUNET_TIME_Timestamp pay_deadline, + const struct TALER_ClaimTokenP *claim_token, + const json_t *contract_terms, + const char *pos_key, + enum TALER_MerchantConfirmationAlgorithm pos_algorithm) { struct GNUNET_TIME_Timestamp now; uint32_t pos32 = (uint32_t) pos_algorithm; @@ -44,7 +44,6 @@ TALER_MERCHANTDB_insert_order (struct TALER_MERCHANTDB_PostgresContext *pg, = json_string_value (json_object_get (contract_terms, "fulfillment_url")); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_timestamp (&pay_deadline), GNUNET_PQ_query_param_auto_from_type (claim_token), @@ -69,26 +68,25 @@ TALER_MERCHANTDB_insert_order (struct TALER_MERCHANTDB_PostgresContext *pg, "inserting order: order_id: %s, instance_id: %s.\n", order_id, instance_id); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_order", - "INSERT INTO merchant_orders" - "(merchant_serial" - ",order_id" - ",pay_deadline" - ",claim_token" - ",h_post_data" - ",creation_time" - ",contract_terms" - ",pos_key" - ",pos_algorithm" - ",session_id" - ",fulfillment_url)" - " SELECT merchant_serial," - " $2, $3, $4, $5, $6, $7::TEXT::JSONB, $8, $9, $10, $11" - " FROM merchant_instances" - " WHERE merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_orders" + "(order_id" + ",pay_deadline" + ",claim_token" + ",h_post_data" + ",creation_time" + ",contract_terms" + ",pos_key" + ",pos_algorithm" + ",session_id" + ",fulfillment_url)" + " VALUES" + " ($1, $2, $3, $4, $5, $6::TEXT::JSONB, $7, $8, $9, $10)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_order", + "", params); } diff --git a/src/backenddb/insert_order_blinded_sigs.c b/src/backenddb/insert_order_blinded_sigs.c @@ -19,18 +19,18 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_pq_lib.h> #include "merchant-database/insert_order_blinded_sigs.h" #include "helper.h" +/* FIXME-Optimize: this smells like it should support the use of arrays... */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_order_blinded_sigs (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *order_id, - uint32_t i, - const struct GNUNET_HashCode *hash, - const struct GNUNET_CRYPTO_BlindedSignature *blind_sig) +TALER_MERCHANTDB_insert_order_blinded_sigs ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *order_id, + uint32_t i, + const struct GNUNET_HashCode *hash, + const struct GNUNET_CRYPTO_BlindedSignature *blind_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (order_id), @@ -40,20 +40,19 @@ TALER_MERCHANTDB_insert_order_blinded_sigs (struct TALER_MERCHANTDB_PostgresCont GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "insert_blinded_sigs", - "INSERT INTO merchant_order_token_blinded_sigs" - " (order_serial" - " ,token_index" - " ,token_hash" - " ,token_blinded_signature" - ")" - " SELECT order_serial, $2, $3, $4" - " FROM merchant_contract_terms" - " WHERE order_id = $1"); - + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_order_token_blinded_sigs" + " (order_serial" + " ,token_index" + " ,token_hash" + " ,token_blinded_signature" + ")" + " SELECT order_serial, $2, $3, $4" + " FROM merchant_contract_terms" + " WHERE order_id = $1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_blinded_sigs", + "", params); } diff --git a/src/backenddb/insert_order_lock.c b/src/backenddb/insert_order_lock.c @@ -19,22 +19,21 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_order_lock.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_order_lock (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - const char *product_id, - uint64_t quantity, - uint32_t quantity_frac) +TALER_MERCHANTDB_insert_order_lock ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + const char *product_id, + uint64_t quantity, + uint32_t quantity_frac) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_uint64 (&quantity), @@ -42,56 +41,53 @@ TALER_MERCHANTDB_insert_order_lock (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_order_lock", - "WITH tmp AS" - " (SELECT " - " product_serial" - " ,merchant_serial" - " ,total_stock" - " ,total_stock_frac" - " ,total_sold" - " ,total_sold_frac" - " ,total_lost" - " ,total_lost_frac" - " ,allow_fractional_quantity" - " FROM merchant_inventory" - " WHERE product_id=$3" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))" - " INSERT INTO merchant_order_locks" - " (product_serial" - " ,total_locked" - " ,total_locked_frac" - " ,order_serial)" - " SELECT tmp.product_serial, $4::INT8, $5::INT4, order_serial" - " FROM merchant_orders" - " JOIN tmp USING(merchant_serial)" - " WHERE order_id=$2" - " AND (tmp.allow_fractional_quantity OR $5 = 0)" - " AND (tmp.total_stock = 9223372036854775807" - " OR (" - " (tmp.total_stock::NUMERIC * 1000000" - " + tmp.total_stock_frac::NUMERIC)" - " - (tmp.total_sold::NUMERIC * 1000000" - " + tmp.total_sold_frac::NUMERIC)" - " - (tmp.total_lost::NUMERIC * 1000000" - " + tmp.total_lost_frac::NUMERIC)" - " >= " - " (($4::NUMERIC * 1000000) + $5::NUMERIC)" - " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " FROM merchant_inventory_locks mil" - " WHERE mil.product_serial = tmp.product_serial)" - " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " FROM merchant_order_locks mol" - " WHERE mol.product_serial = tmp.product_serial)" - " ))"); + TMH_PQ_prepare_anon (pg, + "WITH tmp AS" + " (SELECT " + " product_serial" + " ,total_stock" + " ,total_stock_frac" + " ,total_sold" + " ,total_sold_frac" + " ,total_lost" + " ,total_lost_frac" + " ,allow_fractional_quantity" + " FROM merchant_inventory" + " WHERE product_id=$2)" + " INSERT INTO merchant_order_locks" + " (product_serial" + " ,total_locked" + " ,total_locked_frac" + " ,order_serial)" + " SELECT tmp.product_serial, $3::INT8, $4::INT4, order_serial" + " FROM merchant_orders" + " CROSS JOIN tmp" + " WHERE order_id=$1" + " AND (tmp.allow_fractional_quantity OR $4 = 0)" + " AND (tmp.total_stock = 9223372036854775807" + " OR (" + " (tmp.total_stock::NUMERIC * 1000000" + " + tmp.total_stock_frac::NUMERIC)" + " - (tmp.total_sold::NUMERIC * 1000000" + " + tmp.total_sold_frac::NUMERIC)" + " - (tmp.total_lost::NUMERIC * 1000000" + " + tmp.total_lost_frac::NUMERIC)" + " >= " + " (($3::NUMERIC * 1000000) + $4::NUMERIC)" + " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " FROM merchant_inventory_locks mil" + " WHERE mil.product_serial = tmp.product_serial)" + " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " FROM merchant_order_locks mol" + " WHERE mol.product_serial = tmp.product_serial)" + " ))"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_order_lock", + "", params); } diff --git a/src/backenddb/insert_otp.c b/src/backenddb/insert_otp.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_otp.h" #include "helper.h" @@ -36,14 +34,14 @@ * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_otp (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *otp_id, - const struct TALER_MERCHANTDB_OtpDeviceDetails *td) +TALER_MERCHANTDB_insert_otp ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *otp_id, + const struct TALER_MERCHANTDB_OtpDeviceDetails *td) { uint32_t pos32 = (uint32_t) td->otp_algorithm; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (otp_id), GNUNET_PQ_query_param_string (td->otp_description), GNUNET_PQ_query_param_string (td->otp_key), @@ -52,22 +50,20 @@ TALER_MERCHANTDB_insert_otp (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_otp", - "INSERT INTO merchant_otp_devices" - "(merchant_serial" - ",otp_id" - ",otp_description" - ",otp_key" - ",otp_algorithm" - ",otp_ctr" - ")" - " SELECT merchant_serial," - " $2, $3, $4, $5, $6" - " FROM merchant_instances" - " WHERE merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_otp_devices" + "(otp_id" + ",otp_description" + ",otp_key" + ",otp_algorithm" + ",otp_ctr" + ")" + " VALUES ($1, $2, $3, $4, $5)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_otp", + "", params); } diff --git a/src/backenddb/insert_pending_webhook.c b/src/backenddb/insert_pending_webhook.c @@ -19,23 +19,23 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_pending_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_pending_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t webhook_serial, - const char *url, - const char *http_method, - const char *header, - const char *body) +TALER_MERCHANTDB_insert_pending_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t webhook_serial, + const char *url, + const char *http_method, + const char *header, + const char *body) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_uint64 (&pg->current_merchant_serial), GNUNET_PQ_query_param_uint64 (&webhook_serial), GNUNET_PQ_query_param_string (url), GNUNET_PQ_query_param_string (http_method), @@ -47,10 +47,14 @@ TALER_MERCHANTDB_insert_pending_webhook (struct TALER_MERCHANTDB_PostgresContext : GNUNET_PQ_query_param_string (body), GNUNET_PQ_query_param_end }; + + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); PREPARE (pg, "insert_pending_webhook", - "INSERT INTO merchant_pending_webhooks" + "INSERT INTO merchant.merchant_pending_webhooks" "(merchant_serial" ",webhook_serial" ",url" @@ -58,11 +62,7 @@ TALER_MERCHANTDB_insert_pending_webhook (struct TALER_MERCHANTDB_PostgresContext ",header" ",body" ")" - " SELECT mi.merchant_serial," - " $2, $3, $4, $5, $6" - " FROM merchant_instances mi" - " WHERE mi.merchant_id=$1"); - + " VALUES ($1, $2, $3, $4, $5, $6)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_pending_webhook", params); diff --git a/src/backenddb/insert_product.c b/src/backenddb/insert_product.c @@ -20,56 +20,53 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_product.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_product (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd, - size_t num_cats, - const uint64_t *cats, - bool *no_instance, - bool *conflict, - ssize_t *no_cat, - bool *no_group, - bool *no_pot) +TALER_MERCHANTDB_insert_product ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *conflict, + ssize_t *no_cat, + bool *no_group, + bool *no_pot) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_string (pd->description), - TALER_PQ_query_param_json (pd->description_i18n), /* $4 */ + TALER_PQ_query_param_json (pd->description_i18n), /* $3 */ GNUNET_PQ_query_param_string (pd->unit), GNUNET_PQ_query_param_string (pd->image), - TALER_PQ_query_param_json (pd->taxes), /* $7 */ + TALER_PQ_query_param_json (pd->taxes), /* $6 */ TALER_PQ_query_param_array_amount_with_currency ( pd->price_array_length, pd->price_array, - pg->conn), /* $8 */ - GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */ - GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $10 */ + pg->conn), /* $7 */ + GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $8 */ + GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $9 */ GNUNET_PQ_query_param_bool (pd->allow_fractional_quantity), GNUNET_PQ_query_param_uint32 (&pd->fractional_precision_level), - TALER_PQ_query_param_json (pd->address), /* $13 */ + TALER_PQ_query_param_json (pd->address), /* $12 */ GNUNET_PQ_query_param_timestamp (&pd->next_restock), GNUNET_PQ_query_param_uint32 (&pd->minimum_age), GNUNET_PQ_query_param_array_uint64 (num_cats, cats, - pg->conn), /* $16 */ + pg->conn), /* $15 */ GNUNET_PQ_query_param_string (pd->product_name), (0 == pd->product_group_id) ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&pd->product_group_id), /* $18 */ + : GNUNET_PQ_query_param_uint64 (&pd->product_group_id), /* $17 */ (0 == pd->money_pot_id) ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), /* $19 */ - GNUNET_PQ_query_param_bool (pd->price_is_net), /* $20 */ + : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), /* $18 */ + GNUNET_PQ_query_param_bool (pd->price_is_net), /* $19 */ GNUNET_PQ_query_param_end }; uint64_t ncat; @@ -77,8 +74,6 @@ TALER_MERCHANTDB_insert_product (struct TALER_MERCHANTDB_PostgresContext *pg, struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_bool ("conflict", conflict), - GNUNET_PQ_result_spec_bool ("no_instance", - no_instance), GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_uint64 ("no_cat", &ncat), @@ -91,21 +86,22 @@ TALER_MERCHANTDB_insert_product (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_product", - "SELECT" - " out_conflict AS conflict" - ",out_no_instance AS no_instance" - ",out_no_cat AS no_cat" - ",out_no_group AS no_group" - ",out_no_pot AS no_pot" - " FROM merchant_do_insert_product" - "($1, $2, $3, $4::TEXT::JSONB, $5, $6, $7::TEXT::JSONB, $8" - ",$9, $10, $11, $12, $13::TEXT::JSONB, $14, $15, $16" - ",$17, $18, $19, $20);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_conflict AS conflict" + ",out_no_cat AS no_cat" + ",out_no_group AS no_group" + ",out_no_pot AS no_pot" + " FROM merchant_do_insert_product" + "($1, $2, $3::TEXT::JSONB, $4, $5, $6::TEXT::JSONB, $7" + ",$8, $9, $10, $11, $12::TEXT::JSONB, $13, $14, $15" + ",$16, $17, $18, $19);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_product", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/insert_product.sql b/src/backenddb/insert_product.sql @@ -0,0 +1,223 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024, 2025 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_insert_product; +CREATE FUNCTION merchant_do_insert_product ( + IN in_product_id TEXT, + IN in_description TEXT, + IN in_description_i18n JSONB, -- $3 + IN in_unit TEXT, + IN in_image TEXT, + IN in_taxes JSONB, -- $6 + IN ina_price_list merchant.taler_amount_currency[], + IN in_total_stock INT8, -- $8 + IN in_total_stock_frac INT4, --$9 + IN in_allow_fractional_quantity BOOL, + IN in_fractional_precision_level INT4, + IN in_address JSONB, -- $12 + IN in_next_restock INT8, + IN in_minimum_age INT4, + IN ina_categories INT8[], -- $15 + IN in_product_name TEXT, + IN in_product_group_id INT8, -- NULL for default + IN in_money_pot_id INT8, -- NULL for none + IN in_price_is_net BOOL, -- $19 + OUT out_conflict BOOL, + OUT out_no_cat INT8, + OUT out_no_group BOOL, + OUT out_no_pot BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_product_serial INT8; + i INT8; + ini_cat INT8; +BEGIN + +out_no_group = FALSE; +out_no_pot = FALSE; + +IF in_product_group_id IS NOT NULL +THEN + PERFORM FROM merchant_product_groups + WHERE product_group_serial=in_product_group_id; + IF NOT FOUND + THEN + out_no_group=TRUE; + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; + END IF; +END IF; + +IF in_money_pot_id IS NOT NULL +THEN + PERFORM FROM merchant_money_pots + WHERE money_pot_serial=in_money_pot_id; + IF NOT FOUND + THEN + out_no_pot=TRUE; + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; + END IF; +END IF; + +INSERT INTO merchant_inventory + (product_id + ,product_name + ,description + ,description_i18n + ,unit + ,image + ,image_hash + ,taxes + ,price_array + ,total_stock + ,total_stock_frac + ,allow_fractional_quantity + ,fractional_precision_level + ,address + ,next_restock + ,minimum_age + ,product_group_serial + ,money_pot_serial + ,price_is_net +) VALUES ( + in_product_id + ,in_product_name + ,in_description + ,in_description_i18n + ,in_unit + ,in_image + ,CASE + WHEN (in_image IS NULL) OR (in_image = '') + THEN NULL + ELSE encode(public.digest(convert_to(in_image, 'UTF8'), + 'sha256'), + 'hex') + END + ,in_taxes + ,ina_price_list + ,in_total_stock + ,in_total_stock_frac + ,in_allow_fractional_quantity + ,in_fractional_precision_level + ,in_address + ,in_next_restock + ,in_minimum_age + ,in_product_group_id + ,in_money_pot_id + ,in_price_is_net + ) +ON CONFLICT (product_id) DO NOTHING + RETURNING product_serial + INTO my_product_serial; + + +IF NOT FOUND +THEN + -- Check for idempotency + SELECT product_serial + INTO my_product_serial + FROM merchant_inventory + WHERE product_id=in_product_id + AND product_name=in_product_name + AND description=in_description + AND description_i18n=in_description_i18n + AND unit=in_unit + AND image=in_image + AND taxes=in_taxes + AND to_jsonb(COALESCE(price_array, ARRAY[]::merchant.taler_amount_currency[])) + = to_jsonb(COALESCE(ina_price_list, ARRAY[]::merchant.taler_amount_currency[])) -- FIXME: wild. Why so complicated? + AND total_stock=in_total_stock + AND total_stock_frac=in_total_stock_frac + AND allow_fractional_quantity=in_allow_fractional_quantity + AND fractional_precision_level=in_fractional_precision_level + AND address=in_address + AND next_restock=in_next_restock + AND minimum_age=in_minimum_age + AND product_group_serial IS NOT DISTINCT FROM in_product_group_id + AND money_pot_serial IS NOT DISTINCT FROM in_money_pot_id + AND price_is_net=in_price_is_net; + IF NOT FOUND + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + + -- Check categories match as well + FOR i IN 1..COALESCE(array_length(ina_categories,1),0) + LOOP + ini_cat=ina_categories[i]; + + PERFORM + FROM merchant_product_categories + WHERE product_serial=my_product_serial + AND category_serial=ini_cat; + IF NOT FOUND + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + END LOOP; + + -- Also check there are no additional categories + -- in either set. + SELECT COUNT(*) + INTO i + FROM merchant_product_categories + WHERE product_serial=my_product_serial; + IF i != COALESCE(array_length(ina_categories,1),0) + THEN + out_conflict=TRUE; + out_no_cat=NULL; + RETURN; + END IF; + + -- Is idempotent! + out_conflict=FALSE; + out_no_cat=NULL; + RETURN; +END IF; +out_conflict=FALSE; + + +-- Add categories +FOR i IN 1..COALESCE(array_length(ina_categories,1),0) +LOOP + ini_cat=ina_categories[i]; + + INSERT INTO merchant_product_categories + (product_serial + ,category_serial) + VALUES + (my_product_serial + ,ini_cat) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_no_cat=i; + RETURN; + END IF; +END LOOP; + +-- Success! +out_no_cat=NULL; +END $$; diff --git a/src/backenddb/insert_product_group.c b/src/backenddb/insert_product_group.c @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_product_group.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_product_group (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *name, - const char *description, - uint64_t *product_group_id) +TALER_MERCHANTDB_insert_product_group ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *name, + const char *description, + uint64_t *product_group_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (name), GNUNET_PQ_query_param_string (description), GNUNET_PQ_query_param_end @@ -45,21 +43,20 @@ TALER_MERCHANTDB_insert_product_group (struct TALER_MERCHANTDB_PostgresContext * GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_product_group", - "INSERT INTO merchant_product_groups" - "(merchant_serial" - ",product_group_name" - ",product_group_description)" - " SELECT merchant_serial, $2, $3" - " FROM merchant_instances" - " WHERE merchant_id=$1" - " ON CONFLICT DO NOTHING" - " RETURNING product_group_serial"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_product_groups" + "(product_group_name" + ",product_group_description)" + " VALUES ($1, $2)" + " ON CONFLICT DO NOTHING" + " RETURNING product_group_serial"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_product_group", + "", params, rs); } diff --git a/src/backenddb/insert_refund_proof.c b/src/backenddb/insert_refund_proof.c @@ -19,17 +19,17 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_refund_proof.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_refund_proof (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t refund_serial, - const struct TALER_ExchangeSignatureP *exchange_sig, - const struct TALER_ExchangePublicKeyP *exchange_pub) +TALER_MERCHANTDB_insert_refund_proof ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t refund_serial, + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_ExchangePublicKeyP *exchange_pub) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&refund_serial), @@ -38,20 +38,19 @@ TALER_MERCHANTDB_insert_refund_proof (struct TALER_MERCHANTDB_PostgresContext *p GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "insert_refund_proof", - "INSERT INTO merchant_refund_proofs" - "(refund_serial" - ",exchange_sig" - ",signkey_serial)" - "SELECT $1, $2, signkey_serial" - " FROM merchant_exchange_signing_keys" - " WHERE exchange_pub=$3" - " ORDER BY start_date DESC" - " LIMIT 1"); - + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_refund_proofs" + "(refund_serial" + ",exchange_sig" + ",signkey_serial)" + "SELECT $1, $2, signkey_serial" + " FROM merchant.merchant_exchange_signing_keys" + " WHERE exchange_pub=$3" + " ORDER BY start_date DESC" + " LIMIT 1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_refund_proof", + "", params); } diff --git a/src/backenddb/insert_report.c b/src/backenddb/insert_report.c @@ -43,7 +43,6 @@ TALER_MERCHANTDB_insert_report ( struct TALER_MERCHANT_ReportToken report_token; struct GNUNET_TIME_Timestamp start; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (report_program_section), GNUNET_PQ_query_param_string (report_description), GNUNET_PQ_query_param_string (mime_type), @@ -61,6 +60,9 @@ TALER_MERCHANTDB_insert_report ( GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, &report_token, sizeof (report_token)); @@ -73,27 +75,22 @@ TALER_MERCHANTDB_insert_report ( frequency_shift))); check_connection (pg); - PREPARE (pg, - "insert_report", - "INSERT INTO merchant_reports" - "(merchant_serial" - ",report_program_section" - ",report_description" - ",mime_type" - ",report_token" - ",data_source" - ",target_address" - ",frequency" - ",frequency_shift" - ",next_transmission)" - " SELECT merchant_serial, $2, $3, $4, $5," - " $6, $7, $8, $9, $10" - " FROM merchant_instances" - " WHERE merchant_id=$1" - " ON CONFLICT DO NOTHING;"); - + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_reports" + "(report_program_section" + ",report_description" + ",mime_type" + ",report_token" + ",data_source" + ",target_address" + ",frequency" + ",frequency_shift" + ",next_transmission)" + " VALUES ($1, $2, $3, $4," + " $5, $6, $7, $8, $9)" + " ON CONFLICT DO NOTHING;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_report", + "", params, rs); } diff --git a/src/backenddb/insert_spent_token.c b/src/backenddb/insert_spent_token.c @@ -19,20 +19,19 @@ * @author Christian Blättler */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_spent_token.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_spent_token (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_PrivateContractHashP *h_contract_terms, - const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, - const struct TALER_TokenUsePublicKeyP *use_pub, - const struct TALER_TokenUseSignatureP *use_sig, - const struct TALER_TokenIssueSignature *issue_sig) +TALER_MERCHANTDB_insert_spent_token ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, + const struct TALER_TokenUsePublicKeyP *use_pub, + const struct TALER_TokenUseSignatureP *use_sig, + const struct TALER_TokenIssueSignature *issue_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (h_contract_terms), @@ -53,24 +52,25 @@ TALER_MERCHANTDB_insert_spent_token (struct TALER_MERCHANTDB_PostgresContext *pg }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Storing token spent with key %s\n", GNUNET_h2s (&h_issue_pub->hash)); - PREPARE (pg, - "spent_token_insert", - "SELECT" - " out_no_family" - " ,out_conflict" - " FROM merchant_do_insert_spent_token" - "($1, $2, $3, $4, $5);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_no_family" + " ,out_conflict" + " FROM merchant_do_insert_spent_token" + "($1, $2, $3, $4, $5);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "spent_token_insert", + "", params, rs); if (qs < 0) return qs; + // FIXME: return specific errors to caller if (no_fam) { GNUNET_break (0); diff --git a/src/backenddb/pg_insert_spent_token.sql b/src/backenddb/insert_spent_token.sql diff --git a/src/backenddb/insert_template.c b/src/backenddb/insert_template.c @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_template.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_template (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *template_id, - uint64_t otp_serial_id, - const struct TALER_MERCHANTDB_TemplateDetails *td) +TALER_MERCHANTDB_insert_template ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *template_id, + uint64_t otp_serial_id, + const struct TALER_MERCHANTDB_TemplateDetails *td) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (template_id), GNUNET_PQ_query_param_string (td->template_description), (0 == otp_serial_id) @@ -48,23 +46,22 @@ TALER_MERCHANTDB_insert_template (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_template", - "INSERT INTO merchant_template" - "(merchant_serial" - ",template_id" - ",template_description" - ",otp_device_id" - ",template_contract" - ",editable_defaults" - ")" - " SELECT merchant_serial," - " $2, $3, $4, $5::TEXT::JSONB, $6::TEXT::JSONB" - " FROM merchant_instances" - " WHERE merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_template" + "(template_id" + ",template_description" + ",otp_device_id" + ",template_contract" + ",editable_defaults" + ")" + " VALUES" + " ($1, $2, $3, $4::TEXT::JSONB, $5::TEXT::JSONB)"); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_template", + "", params); GNUNET_PQ_cleanup_query_params_closures (params); return qs; diff --git a/src/backenddb/insert_token_family.c b/src/backenddb/insert_token_family.c @@ -19,18 +19,17 @@ * @author Christian Blättler */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_token_family.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_token_family (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *token_family_slug, - const struct TALER_MERCHANTDB_TokenFamilyDetails *details) +TALER_MERCHANTDB_insert_token_family ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *token_family_slug, + const struct TALER_MERCHANTDB_TokenFamilyDetails *details) { const char *kind; @@ -46,37 +45,35 @@ TALER_MERCHANTDB_insert_token_family (struct TALER_MERCHANTDB_PostgresContext *p GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_token_family", - "INSERT INTO merchant_token_families" - "(merchant_serial" - ",slug" - ",name" - ",description" - ",description_i18n" - ",extra_data" - ",valid_after" - ",valid_before" - ",duration" - ",validity_granularity" - ",start_offset" - ",kind)" - " SELECT merchant_serial, $2, $3, $4, $5::TEXT::JSONB," - " $6::TEXT::JSONB, $7, $8, $9, $10, $11, $12" - " FROM merchant_instances" - " WHERE merchant_id=$1" - " ON CONFLICT DO NOTHING;"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_token_families" + "(slug" + ",name" + ",description" + ",description_i18n" + ",extra_data" + ",valid_after" + ",valid_before" + ",duration" + ",validity_granularity" + ",start_offset" + ",kind)" + " VALUES ($1, $2, $3, $4::TEXT::JSONB," + " $5::TEXT::JSONB, $6, $7, $8, $9, $10, $11)" + " ON CONFLICT DO NOTHING;"); { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (token_family_slug), GNUNET_PQ_query_param_string (details->name), GNUNET_PQ_query_param_string (details->description), TALER_PQ_query_param_json (details->description_i18n), NULL == details->extra_data - ? GNUNET_PQ_query_param_null () - : TALER_PQ_query_param_json (details->extra_data), + ? GNUNET_PQ_query_param_null () + : TALER_PQ_query_param_json (details->extra_data), GNUNET_PQ_query_param_timestamp (&details->valid_after), GNUNET_PQ_query_param_timestamp (&details->valid_before), GNUNET_PQ_query_param_relative_time (&details->duration), @@ -87,7 +84,7 @@ TALER_MERCHANTDB_insert_token_family (struct TALER_MERCHANTDB_PostgresContext *p }; return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_token_family", + "", params); } } diff --git a/src/backenddb/insert_token_family_key.c b/src/backenddb/insert_token_family_key.c @@ -21,22 +21,21 @@ #include "platform.h" #include <gnunet/gnunet_common.h> #include <gnunet/gnunet_pq_lib.h> -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_token_family_key.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_token_family_key (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id, - const char *token_family_slug, - const struct TALER_TokenIssuePublicKey *pub, - const struct TALER_TokenIssuePrivateKey *priv, - struct GNUNET_TIME_Timestamp key_expires, - struct GNUNET_TIME_Timestamp valid_after, - struct GNUNET_TIME_Timestamp valid_before) +TALER_MERCHANTDB_insert_token_family_key ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id, + const char *token_family_slug, + const struct TALER_TokenIssuePublicKey *pub, + const struct TALER_TokenIssuePrivateKey *priv, + struct GNUNET_TIME_Timestamp key_expires, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before) { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); @@ -89,25 +88,23 @@ TALER_MERCHANTDB_insert_token_family_key (struct TALER_MERCHANTDB_PostgresContex valid_after.abs_time)); GNUNET_assert (! GNUNET_TIME_absolute_is_zero ( valid_before.abs_time)); - PREPARE (pg, - "token_family_key_insert", - "INSERT INTO merchant_token_family_keys " - "(token_family_serial" - ",pub" - ",h_pub" - ",priv" - ",private_key_created_at" - ",private_key_deleted_at" - ",signature_validity_start" - ",signature_validity_end" - ",cipher)" - " SELECT token_family_serial, $2, $3, $4, $5, $6, $7, $8, $9" - " FROM merchant_token_families" - " WHERE (slug = $1)" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$10)"); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (merchant_id, + pg->current_merchant_id)); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_token_family_keys " + "(token_family_serial" + ",pub" + ",h_pub" + ",priv" + ",private_key_created_at" + ",private_key_deleted_at" + ",signature_validity_start" + ",signature_validity_end" + ",cipher)" + " SELECT token_family_serial, $2, $3, $4, $5, $6, $7, $8, $9" + " FROM merchant_token_families" + " WHERE (slug = $1)"); { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (token_family_slug), @@ -119,13 +116,12 @@ TALER_MERCHANTDB_insert_token_family_key (struct TALER_MERCHANTDB_PostgresContex GNUNET_PQ_query_param_timestamp (&valid_after), GNUNET_PQ_query_param_timestamp (&valid_before), GNUNET_PQ_query_param_string (cipher), - GNUNET_PQ_query_param_string (merchant_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "token_family_key_insert", + "", params); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Insert into MTFK %s with valid [%llu,%llu] got %d\n", diff --git a/src/backenddb/insert_transfer.c b/src/backenddb/insert_transfer.c @@ -19,28 +19,25 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_transfer.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *credit_amount, - struct TALER_FullPayto payto_uri, - uint64_t bank_serial_id, - bool *no_instance, - bool *no_account, - bool *conflict) +TALER_MERCHANTDB_insert_transfer ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_Amount *credit_amount, + struct TALER_FullPayto payto_uri, + uint64_t bank_serial_id, + bool *no_account, + bool *conflict) { struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_auto_from_type (wtid), TALER_PQ_query_param_amount_with_currency (pg->conn, @@ -53,8 +50,6 @@ TALER_MERCHANTDB_insert_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("no_instance", - no_instance), GNUNET_PQ_result_spec_bool ("no_account", no_account), GNUNET_PQ_result_spec_bool ("conflict", @@ -62,17 +57,18 @@ TALER_MERCHANTDB_insert_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_transfer", - "SELECT " - " out_no_instance AS no_instance" - " ,out_no_account AS no_account" - " ,out_conflict AS conflict" - " FROM merchant_do_insert_transfer" - " ($1, $2, $3, $4, $5, $6, $7);"); + TMH_PQ_prepare_anon (pg, + "SELECT " + " out_no_account AS no_account" + " ,out_conflict AS conflict" + " FROM merchant_do_insert_transfer" + " ($1, $2, $3, $4, $5, $6);"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_transfer", + "", params, rs); } diff --git a/src/backenddb/insert_transfer.sql b/src/backenddb/insert_transfer.sql @@ -0,0 +1,105 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_insert_transfer; +CREATE FUNCTION merchant_do_insert_transfer ( + IN in_exchange_url TEXT, + IN in_wtid BYTEA, + IN in_credit_amount merchant.taler_amount_currency, + IN in_credited_account_payto TEXT, + IN in_bank_serial_id INT8, -- can be NULL if unknown + IN in_execution_time INT8, + OUT out_no_account BOOL, + OUT out_conflict BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_account_serial INT8; + my_record RECORD; + my_bank_serial_id INT8; + my_credit_amount merchant.taler_amount_currency; +BEGIN + +out_conflict=FALSE; + +SELECT account_serial + INTO my_account_serial + FROM merchant_accounts + WHERE REGEXP_REPLACE(payto_uri, + '\\?.*','') + =REGEXP_REPLACE(in_credited_account_payto, + '\\?.*',''); +IF NOT FOUND +THEN + out_no_account=TRUE; + RETURN; +END IF; +out_no_account=FALSE; + +SELECT bank_serial_id + ,credit_amount + INTO my_record + FROM merchant_transfers + WHERE wtid=in_wtid + AND account_serial=my_account_serial + AND exchange_url=in_exchange_url; +IF NOT FOUND +THEN + INSERT INTO merchant_transfers + (exchange_url + ,wtid + ,credit_amount + ,account_serial + ,bank_serial_id + ,execution_time + ) VALUES + (in_exchange_url + ,in_wtid + ,in_credit_amount + ,my_account_serial + ,in_bank_serial_id + ,in_execution_time); + -- Do notify on TALER_DBEVENT_MERCHANT_WIRE_TRANSFER_CONFIRMED + NOTIFY XJ5N652FA4TBS2WXGY3S1FMPMQYTD8KAZA9B7HW9JWJ4PZ2DB852G; + RETURN; +END IF; + +my_bank_serial_id = my_record.bank_serial_id; +my_credit_amount = my_record.credit_amount; + +IF ( (in_credit_amount.val != my_credit_amount.val) OR + (in_credit_amount.frac != my_credit_amount.frac) OR + (in_credit_amount.curr != my_credit_amount.curr) ) +THEN + out_conflict = TRUE; -- amounts differ, not OK! + RETURN; +END IF; + +IF ( (my_bank_serial_id IS NULL) AND + (in_bank_serial_id IS NOT NULL) ) +THEN + -- We learned the bank_bank_serial_id, update that + UPDATE merchant_transfers + SET bank_serial_id=in_bank_serial_id + WHERE wtid=in_wtid + AND account_serial=my_account_serial + AND exchange_url=in_exchange_url; + RETURN; +END IF; + +-- idempotent request, success. + +END $$; diff --git a/src/backenddb/insert_transfer_details.c b/src/backenddb/insert_transfer_details.c @@ -19,9 +19,8 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> +#include <taler/taler_dbevents.h> #include "merchant-database/start.h" #include "merchant-database/insert_transfer_details.h" #include "helper.h" @@ -34,12 +33,13 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *exchange_url, - struct TALER_FullPayto payto_uri, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_EXCHANGE_TransferData *td) +TALER_MERCHANTDB_insert_transfer_details ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *exchange_url, + struct TALER_FullPayto payto_uri, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_EXCHANGE_TransferData *td) { unsigned int len = td->details_length; struct TALER_Amount coin_values[GNUNET_NZL (len)]; @@ -49,6 +49,10 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex enum GNUNET_DB_QueryStatus qs; bool duplicate; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + for (unsigned int i = 0; i<len; i++) { const struct TALER_TrackTransferDetails *tdd = &td->details[i]; @@ -60,19 +64,8 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex } check_connection (pg); - PREPARE (pg, - "insert_transfer_details", - "SELECT" - " out_no_instance" - ",out_no_account" - ",out_no_exchange" - ",out_duplicate" - ",out_conflict" - ",out_order_id" - ",out_merchant_pub" - " FROM merchant_do_insert_transfer_details" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13);"); - + // FIXME: why do we retry here, but not most other SQL statements? + // We need a slightly more consistent strategy... for (unsigned int retries = 0; retries < MAX_RETRIES; retries++) @@ -87,7 +80,7 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_uint64 (&pg->current_merchant_serial), GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_string (payto_uri.full_payto), GNUNET_PQ_query_param_auto_from_type (wtid), @@ -116,15 +109,11 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex pg->conn), GNUNET_PQ_query_param_end }; - bool no_instance; bool no_account; bool no_exchange; bool conflict; char *order_id = NULL; - struct TALER_MerchantPublicKeyP merchant_pub; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("out_no_instance", - &no_instance), GNUNET_PQ_result_spec_bool ("out_no_account", &no_account), GNUNET_PQ_result_spec_bool ("out_no_exchange", @@ -137,15 +126,20 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex GNUNET_PQ_result_spec_string ("out_order_id", &order_id), NULL), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_auto_from_type ("out_merchant_pub", - &merchant_pub), - NULL), GNUNET_PQ_result_spec_end }; + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_no_account" + ",out_no_exchange" + ",out_duplicate" + ",out_conflict" + ",out_order_id" + " 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, - "insert_transfer_details", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); @@ -165,7 +159,7 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex struct TMH_OrderPayEventP pay_eh = { .header.size = htons (sizeof (pay_eh)), .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), - .merchant_pub = merchant_pub + .merchant_pub = pg->current_merchant_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -181,8 +175,7 @@ TALER_MERCHANTDB_insert_transfer_details (struct TALER_MERCHANTDB_PostgresContex GNUNET_free (order_id); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Transfer details inserted: %s%s%s%s%s\n", - no_instance ? "no instance " : "", + "Transfer details inserted: %s%s%s%s\n", no_account ? "no account " : "", no_exchange ? "no exchange ": "", duplicate ? "duplicate ": "", diff --git a/src/backenddb/insert_transfer_details.sql b/src/backenddb/insert_transfer_details.sql @@ -0,0 +1,252 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024, 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_insert_transfer_details; +CREATE FUNCTION merchant_do_insert_transfer_details ( + IN in_merchant_serial INT8, + IN in_exchange_url TEXT, + IN in_payto_uri TEXT, + IN in_wtid BYTEA, + IN in_execution_time INT8, + IN in_exchange_pub BYTEA, + IN in_exchange_sig BYTEA, + IN in_total_amount merchant.taler_amount_currency, + IN in_wire_fee merchant.taler_amount_currency, + IN ina_coin_values merchant.taler_amount_currency[], + IN ina_deposit_fees merchant.taler_amount_currency[], + IN ina_coin_pubs BYTEA[], + IN ina_contract_terms BYTEA[], + OUT out_no_account BOOL, + OUT out_no_exchange BOOL, + OUT out_duplicate BOOL, + OUT out_conflict BOOL, + OUT out_order_id TEXT) +LANGUAGE plpgsql +AS $$ +DECLARE + my_signkey_serial INT8; + my_expected_credit_serial INT8; + my_affected_orders RECORD; + my_decose INT8; + my_order_id TEXT; + i INT8; + curs CURSOR (arg_coin_pub BYTEA) FOR + SELECT mcon.deposit_confirmation_serial, + mcon.order_serial + FROM merchant_deposits dep + JOIN merchant_deposit_confirmations mcon + USING (deposit_confirmation_serial) + WHERE dep.coin_pub=arg_coin_pub; + ini_coin_pub BYTEA; + ini_contract_term BYTEA; + ini_coin_value merchant.taler_amount_currency; + ini_deposit_fee merchant.taler_amount_currency; +BEGIN + +-- Determine account that was credited. +SELECT expected_credit_serial + INTO my_expected_credit_serial + FROM merchant_expected_transfers + WHERE exchange_url=in_exchange_url + AND wtid=in_wtid + AND account_serial= + (SELECT account_serial + FROM merchant_accounts + WHERE payto_uri=in_payto_uri); + +IF NOT FOUND +THEN + out_no_account=TRUE; + out_no_exchange=FALSE; + out_duplicate=FALSE; + out_conflict=FALSE; + RETURN; +END IF; +out_no_account=FALSE; + +-- Find exchange sign key +SELECT signkey_serial + INTO my_signkey_serial + FROM merchant.merchant_exchange_signing_keys + WHERE exchange_pub=in_exchange_pub + ORDER BY start_date DESC + LIMIT 1; + +IF NOT FOUND +THEN + out_no_exchange=TRUE; + out_conflict=FALSE; + out_duplicate=FALSE; + RETURN; +END IF; +out_no_exchange=FALSE; + +-- Add signature first, check for idempotent request +INSERT INTO merchant_transfer_signatures + (expected_credit_serial + ,signkey_serial + ,credit_amount + ,wire_fee + ,execution_time + ,exchange_sig) + VALUES + (my_expected_credit_serial + ,my_signkey_serial + ,in_total_amount + ,in_wire_fee + ,in_execution_time + ,in_exchange_sig) + ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + PERFORM 1 + FROM merchant_transfer_signatures + WHERE expected_credit_serial=my_expected_credit_serial + AND signkey_serial=my_signkey_serial + AND credit_amount=in_total_amount + AND wire_fee=in_wire_fee + AND execution_time=in_execution_time + AND exchange_sig=in_exchange_sig; + IF FOUND + THEN + -- duplicate case + out_duplicate=TRUE; + out_conflict=FALSE; + RETURN; + END IF; + -- conflict case + out_duplicate=FALSE; + out_conflict=TRUE; + RETURN; +END IF; + +out_duplicate=FALSE; +out_conflict=FALSE; + + +FOR i IN 1..array_length(ina_coin_pubs,1) +LOOP + ini_coin_value=ina_coin_values[i]; + ini_deposit_fee=ina_deposit_fees[i]; + ini_coin_pub=ina_coin_pubs[i]; + ini_contract_term=ina_contract_terms[i]; + + INSERT INTO merchant_expected_transfer_to_coin + (deposit_serial + ,expected_credit_serial + ,offset_in_exchange_list + ,exchange_deposit_value + ,exchange_deposit_fee) + SELECT + dep.deposit_serial + ,my_expected_credit_serial + ,i + ,ini_coin_value + ,ini_deposit_fee + FROM merchant_deposits dep + JOIN merchant_deposit_confirmations dcon + USING (deposit_confirmation_serial) + JOIN merchant_contract_terms cterm + USING (order_serial) + WHERE dep.coin_pub=ini_coin_pub + AND cterm.h_contract_terms=ini_contract_term; + + RAISE NOTICE 'iterating over affected orders'; + OPEN curs (arg_coin_pub:=ini_coin_pub); + LOOP + FETCH NEXT FROM curs INTO my_affected_orders; + EXIT WHEN NOT FOUND; + + RAISE NOTICE 'checking affected order for completion'; + + my_decose=my_affected_orders.deposit_confirmation_serial; + + PERFORM FROM merchant_deposits md + WHERE md.deposit_confirmation_serial=my_decose + AND (settlement_retry_needed + OR (settlement_wtid IS NULL) ); + IF NOT FOUND + THEN + -- must be all done, clear flag + UPDATE merchant_deposit_confirmations + SET wire_pending=FALSE + WHERE (deposit_confirmation_serial=my_decose); + + IF FOUND + THEN + -- Also update contract terms, if all (other) associated + -- deposit_confirmations are also done. + + RAISE NOTICE 'checking affected contract for completion'; + PERFORM FROM merchant_deposit_confirmations mdc + WHERE mdc.wire_pending + AND mdc.order_serial=my_affected_orders.order_serial; + IF NOT FOUND + THEN + + UPDATE merchant_contract_terms + SET wired=TRUE + WHERE (order_serial=my_affected_orders.order_serial); + + -- Select merchant_serial and order_id for webhook + SELECT order_id + INTO my_order_id + FROM merchant_contract_terms + WHERE order_serial=my_affected_orders.order_serial; + out_order_id = my_order_id; + -- Insert pending webhook if it exists + INSERT INTO merchant.merchant_pending_webhooks + (merchant_serial + ,webhook_serial + ,url + ,http_method + ,header + ,body) + SELECT in_merchant_serial + ,mw.webhook_serial + ,mw.url + ,mw.http_method + ,merchant.replace_placeholder( + merchant.replace_placeholder(mw.header_template, + 'order_id', + my_order_id), + 'wtid', + encode(in_wtid, 'hex') + )::TEXT + ,merchant.replace_placeholder( + merchant.replace_placeholder(mw.body_template, + 'order_id', + my_order_id), + 'wtid', + encode(in_wtid, 'hex') + )::TEXT + FROM merchant_webhook mw + WHERE mw.event_type = 'order_settled'; + IF FOUND + THEN + NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; + END IF; -- found pending order_settled webhooks to deliver + END IF; -- no more merchant_deposits waiting for wire_pending + END IF; -- did clear wire_pending flag for deposit confirmation + END IF; -- no more merchant_deposits wait for settlement + + END LOOP; -- END curs LOOP + CLOSE curs; +END LOOP; -- END FOR loop + +END $$; diff --git a/src/backenddb/insert_unclaim_signature.c b/src/backenddb/insert_unclaim_signature.c @@ -19,13 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> #include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_unclaim_signature.h" #include "helper.h" - +/** + * Compute the notification code for the given + * @a order_id and @a merchant_pub. + * + * @param order_id order to notify for + * @param merchant_pub merchant to notify for + * @return notification code, to be freed by caller + */ static char * get_notify_str (const char *order_id, const struct TALER_MerchantPublicKeyP *merchant_pub) @@ -50,18 +56,16 @@ enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_insert_unclaim_signature ( struct TALER_MERCHANTDB_PostgresContext *pg, const char *instance_id, - const struct TALER_MerchantPublicKeyP *merchant_pub, const char *order_id, const struct GNUNET_CRYPTO_EddsaPublicKey *nonce, const struct GNUNET_HashCode *h_contract, const struct GNUNET_CRYPTO_EddsaSignature *nsig) { - char *nonce_str = GNUNET_STRINGS_data_to_string_alloc (nonce, - sizeof (*nonce)); + char nonce_str[sizeof (*nonce) * 2]; + char *end; char *notify_str = get_notify_str (order_id, - merchant_pub); + &pg->current_merchant_pub); struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_string (nonce_str), GNUNET_PQ_query_param_string (notify_str), @@ -77,22 +81,30 @@ TALER_MERCHANTDB_insert_unclaim_signature ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_unclaim_signature", - "SELECT" - " out_found" - " FROM merchant_do_insert_unclaim_signature" - "($1, $2, $3, $4, $5, $6);"); + end = GNUNET_STRINGS_data_to_string (nonce, + sizeof (*nonce), + nonce_str, + sizeof (nonce_str)); + GNUNET_assert (NULL != end); + *end = '\0'; + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_found" + " FROM merchant_do_insert_unclaim_signature" + "($1, $2, $3, $4, $5);"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "insert_unclaim_signature", + "", params, rs); - GNUNET_free (nonce_str); GNUNET_free (notify_str); if (qs <= 0) return qs; + /* FIXME: not a precise set of possible errors... (see stored proc!) */ return found ? GNUNET_DB_STATUS_SUCCESS_ONE_RESULT : GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; diff --git a/src/backenddb/insert_unclaim_signature.sql b/src/backenddb/insert_unclaim_signature.sql @@ -0,0 +1,73 @@ +-- +-- 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_insert_unclaim_signature; +CREATE FUNCTION merchant_do_insert_unclaim_signature ( + IN in_order_id TEXT, + IN in_nonce_str TEXT, + IN in_notify_str TEXT, + IN in_h_contract_terms BYTEA, + IN in_nonce_sig BYTEA, + OUT out_found BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_expiration_time INT8; +BEGIN + + SELECT pay_deadline + INTO my_expiration_time + FROM merchant_contract_terms + WHERE order_id=in_order_id + AND contract_terms->>'nonce' = in_nonce_str; + + IF NOT FOUND + THEN + -- FIXME: distinguish better between + -- different "not found" cases (contract, unclaim sig exists) + out_found = FALSE; + RETURN; + END IF; + + INSERT INTO merchant_unclaim_signatures + (h_contract_terms + ,unclaim_sig + ,expiration_time) + VALUES + (in_h_contract_terms + ,in_nonce_sig + ,my_expiration_time) + ON CONFLICT DO NOTHING; + + IF FOUND + THEN + out_found = TRUE; + + -- order status change notification + EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify_str); + + RETURN; + END IF; + + PERFORM FROM merchant_unclaim_signatures + WHERE h_contract_terms = in_h_contract_terms + AND unclaim_sig = in_nonce_sig; + out_found = FOUND; + +END $$; diff --git a/src/backenddb/insert_unit.c b/src/backenddb/insert_unit.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025 Taler Systems SA + Copyright (C) 2025, 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 @@ -19,23 +19,20 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_unit.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_unit (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_MERCHANTDB_UnitDetails *ud, - bool *no_instance, - bool *conflict, - uint64_t *unit_serial) +TALER_MERCHANTDB_insert_unit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const struct TALER_MERCHANTDB_UnitDetails *ud, + bool *conflict, + uint64_t *unit_serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (ud->unit), GNUNET_PQ_query_param_string (ud->unit_name_long), GNUNET_PQ_query_param_string (ud->unit_name_short), @@ -48,8 +45,6 @@ TALER_MERCHANTDB_insert_unit (struct TALER_MERCHANTDB_PostgresContext *pg, }; bool unit_serial_present = true; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("no_instance", - no_instance), GNUNET_PQ_result_spec_bool ("conflict", conflict), GNUNET_PQ_result_spec_allow_null ( @@ -60,20 +55,19 @@ TALER_MERCHANTDB_insert_unit (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; - *no_instance = false; *conflict = false; - + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_unit", - "SELECT" - " out_no_instance AS no_instance" - " ,out_conflict AS conflict" - " ,out_unit_serial AS unit_serial" - " FROM merchant_do_insert_unit" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_conflict AS conflict" + " ,out_unit_serial AS unit_serial" + " FROM merchant_do_insert_unit" + " ($1,$2,$3,$4,$5,$6,$7,$8);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_unit", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/insert_unit.sql b/src/backenddb/insert_unit.sql @@ -0,0 +1,81 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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 insert_unit.sql +-- @brief SQL for inserting units +-- @author Bohdan Potuzhnyi + +DROP FUNCTION IF EXISTS merchant_do_insert_unit; +CREATE FUNCTION merchant_do_insert_unit ( + IN in_unit TEXT, + IN in_unit_name_long TEXT, + IN in_unit_name_short TEXT, + IN in_unit_name_long_i18n BYTEA, + IN in_unit_name_short_i18n BYTEA, + IN in_unit_allow_fraction BOOL, + IN in_unit_precision_level INT4, + IN in_unit_active BOOL, + OUT out_conflict BOOL, + OUT out_unit_serial INT8) + LANGUAGE plpgsql +AS $$ +BEGIN + -- Reject attempts to shadow builtin identifiers. + IF EXISTS ( + SELECT 1 + FROM merchant.merchant_builtin_units bu + WHERE bu.unit = in_unit + ) THEN + out_conflict := TRUE; + out_unit_serial := NULL; + RETURN; + END IF; + + INSERT INTO merchant_custom_units ( + unit, + unit_name_long, + unit_name_short, + unit_name_long_i18n, + unit_name_short_i18n, + unit_allow_fraction, + unit_precision_level, + unit_active) + VALUES ( + in_unit, + in_unit_name_long, + in_unit_name_short, + in_unit_name_long_i18n, + in_unit_name_short_i18n, + in_unit_allow_fraction, + in_unit_precision_level, + in_unit_active) + ON CONFLICT (merchant_serial, unit) + DO NOTHING + RETURNING unit_serial + INTO out_unit_serial; + + IF FOUND THEN + out_conflict := FALSE; + RETURN; + END IF; + + -- Conflict: custom unit already exists. + SELECT unit_serial + INTO out_unit_serial + FROM merchant_custom_units + WHERE unit = in_unit; + + out_conflict := TRUE; +END $$; diff --git a/src/backenddb/insert_webhook.c b/src/backenddb/insert_webhook.c @@ -19,20 +19,19 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/insert_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *webhook_id, - const struct TALER_MERCHANTDB_WebhookDetails *wb) +TALER_MERCHANTDB_insert_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *webhook_id, + const struct TALER_MERCHANTDB_WebhookDetails *wb) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (webhook_id), GNUNET_PQ_query_param_string (wb->event_type), GNUNET_PQ_query_param_string (wb->url), @@ -46,24 +45,22 @@ TALER_MERCHANTDB_insert_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "insert_webhook", - "INSERT INTO merchant_webhook" - "(merchant_serial" - ",webhook_id" - ",event_type" - ",url" - ",http_method" - ",header_template" - ",body_template" - ")" - " SELECT merchant_serial," - " $2, $3, $4, $5, $6, $7" - " FROM merchant_instances" - " WHERE merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_webhook" + "(webhook_id" + ",event_type" + ",url" + ",http_method" + ",header_template" + ",body_template" + ")" + " VALUES ($1, $2, $3, $4, $5, $6)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "insert_webhook", + "", params); } diff --git a/src/backenddb/lock_product.c b/src/backenddb/lock_product.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lock_product.h" #include "helper.h" @@ -37,7 +35,6 @@ TALER_MERCHANTDB_lock_product ( struct GNUNET_TIME_Timestamp expiration_time) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_auto_from_type (uuid), GNUNET_PQ_query_param_uint64 (&quantity), @@ -46,58 +43,52 @@ TALER_MERCHANTDB_lock_product ( GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lock_product", - "WITH ps AS" - " (SELECT product_serial" - " FROM merchant_inventory" - " WHERE product_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))" - ",tmp AS" - " (SELECT" - " mi.product_serial" - " ,mi.total_stock" - " ,mi.total_stock_frac" - " ,mi.total_sold" - " ,mi.total_sold_frac" - " ,mi.total_lost" - " ,mi.total_lost_frac" - " ,mi.allow_fractional_quantity" - " FROM merchant_inventory mi" - " JOIN ps USING (product_serial))" - "INSERT INTO merchant_inventory_locks" - "(product_serial" - ",lock_uuid" - ",total_locked" - ",total_locked_frac" - ",expiration)" - " SELECT tmp.product_serial, $3, $4::INT8, $5::INT4, $6" - " FROM tmp" - " WHERE (tmp.allow_fractional_quantity OR $5 = 0)" - " AND (tmp.total_stock = 9223372036854775807" - " OR (" - " (tmp.total_stock::NUMERIC * 1000000" - " + tmp.total_stock_frac::NUMERIC)" - " - (tmp.total_sold::NUMERIC * 1000000" - " + tmp.total_sold_frac::NUMERIC)" - " - (tmp.total_lost::NUMERIC * 1000000" - " + tmp.total_lost_frac::NUMERIC)" - " >= " - " (($4::NUMERIC * 1000000) + $5::NUMERIC)" - " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " FROM merchant_inventory_locks mil" - " WHERE mil.product_serial = tmp.product_serial)" - " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " FROM merchant_order_locks mol" - " WHERE mol.product_serial = tmp.product_serial)" - " ))"); + TMH_PQ_prepare_anon (pg, + "WITH tmp AS" + " (SELECT" + " mi.product_serial" + " ,mi.total_stock" + " ,mi.total_stock_frac" + " ,mi.total_sold" + " ,mi.total_sold_frac" + " ,mi.total_lost" + " ,mi.total_lost_frac" + " ,mi.allow_fractional_quantity" + " FROM merchant_inventory mi" + " WHERE mi.product_id=$1)" + "INSERT INTO merchant_inventory_locks" + "(product_serial" + ",lock_uuid" + ",total_locked" + ",total_locked_frac" + ",expiration)" + " SELECT tmp.product_serial, $2, $3::INT8, $4::INT4, $5" + " FROM tmp" + " WHERE (tmp.allow_fractional_quantity OR $4 = 0)" + " AND (tmp.total_stock = 9223372036854775807" + " OR (" + " (tmp.total_stock::NUMERIC * 1000000" + " + tmp.total_stock_frac::NUMERIC)" + " - (tmp.total_sold::NUMERIC * 1000000" + " + tmp.total_sold_frac::NUMERIC)" + " - (tmp.total_lost::NUMERIC * 1000000" + " + tmp.total_lost_frac::NUMERIC)" + " >= " + " (($3::NUMERIC * 1000000) + $4::NUMERIC)" + " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " FROM merchant_inventory_locks mil" + " WHERE mil.product_serial = tmp.product_serial)" + " + (SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " FROM merchant_order_locks mol" + " WHERE mol.product_serial = tmp.product_serial)" + " ))"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "lock_product", + "", params); } diff --git a/src/backenddb/lookup_account.c b/src/backenddb/lookup_account.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 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 @@ -19,20 +19,19 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_account.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - struct TALER_FullPayto payto_uri, - uint64_t *account_serial) +TALER_MERCHANTDB_lookup_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + struct TALER_FullPayto payto_uri, + uint64_t *account_serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (payto_uri.full_payto), GNUNET_PQ_query_param_end }; @@ -42,20 +41,18 @@ TALER_MERCHANTDB_lookup_account (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_account", - "SELECT" - " account_serial" - " FROM merchant_accounts" - " WHERE REGEXP_REPLACE(payto_uri,'\\?.*','')" - " =REGEXP_REPLACE($2,'\\?.*','')" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " account_serial" + " FROM merchant_accounts" + " WHERE REGEXP_REPLACE(payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($1,'\\?.*','')"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_account", + "", params, rs); } diff --git a/src/backenddb/lookup_all_products.c b/src/backenddb/lookup_all_products.c @@ -19,12 +19,11 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_all_products.h" #include "helper.h" + /** * Context used for TALER_MERCHANTDB_lookup_all_products(). */ @@ -174,53 +173,51 @@ TALER_MERCHANTDB_lookup_all_products ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_all_products", - "SELECT" - " description" - ",description_i18n::TEXT" - ",product_name" - ",unit" - ",price_array" - ",taxes::TEXT" - ",total_stock" - ",total_stock_frac" - ",allow_fractional_quantity" - ",fractional_precision_level" - ",total_sold" - ",total_sold_frac" - ",total_lost" - ",total_lost_frac" - ",image" - ",minv.address::TEXT" - ",next_restock" - ",minimum_age" - ",product_id" - ",product_serial" - ",t.category_array AS categories" - ",product_group_serial" - ",money_pot_serial" - ",price_is_net" - " FROM merchant_inventory minv" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - ",LATERAL (" - " SELECT ARRAY (" - " SELECT mpc.category_serial" - " FROM merchant_product_categories mpc" - " WHERE mpc.product_serial = minv.product_serial" - " ) AS category_array" - " ) t" - " WHERE inst.merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " description" + ",description_i18n::TEXT" + ",product_name" + ",unit" + ",price_array" + ",taxes::TEXT" + ",total_stock" + ",total_stock_frac" + ",allow_fractional_quantity" + ",fractional_precision_level" + ",total_sold" + ",total_sold_frac" + ",total_lost" + ",total_lost_frac" + ",image" + ",minv.address::TEXT" + ",next_restock" + ",minimum_age" + ",product_id" + ",product_serial" + ",t.category_array AS categories" + ",product_group_serial" + ",money_pot_serial" + ",price_is_net" + " FROM merchant_inventory minv" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " ) AS category_array" + " ) t"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_all_products", + "", params, &lookup_products_cb, &plc); diff --git a/src/backenddb/lookup_all_webhooks.c b/src/backenddb/lookup_all_webhooks.c @@ -0,0 +1,166 @@ +/* + This file is part of TALER + Copyright (C) 2023 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 src/backenddb/lookup_all_webhooks.c + * @brief Implementation of the lookup_all_webhooks function for Postgres + * @author Iván Ávalos + */ +#include "platform.h" +#include <taler/taler_pq_lib.h> +#include "merchant-database/lookup_all_webhooks.h" +#include "helper.h" + +/** + * Context used for lookup_all_webhooks_cb(). + */ +struct LookupAllWebhookContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_AllWebhooksCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about webhook. + * + * @param[in,out] cls of type `struct LookupAllWebhookContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_all_webhooks_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupAllWebhookContext *pwlc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + uint64_t webhook_pending_serial; + struct GNUNET_TIME_Absolute next_attempt; + uint32_t retries; + char *url; + char *http_method; + char *header = NULL; + char *body = NULL; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("out_webhook_pending_serial", + &webhook_pending_serial), + GNUNET_PQ_result_spec_absolute_time ("out_next_attempt", + &next_attempt), + GNUNET_PQ_result_spec_uint32 ("out_retries", + &retries), + GNUNET_PQ_result_spec_string ("out_url", + &url), + GNUNET_PQ_result_spec_string ("out_http_method", + &http_method), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("out_header", + &header), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("out_body", + &body), + NULL), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + pwlc->extract_failed = true; + return; + } + pwlc->cb (pwlc->cb_cls, + webhook_pending_serial, + next_attempt, + retries, + url, + http_method, + header, + body); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_lookup_all_webhooks ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t min_row, + uint32_t max_results, + TALER_MERCHANTDB_AllWebhooksCallback cb, + void *cb_cls) +{ + struct LookupAllWebhookContext pwlc = { + .cb = cb, + .cb_cls = cb_cls, + .extract_failed = false, + }; + uint64_t max_results64 = max_results; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&pg->current_merchant_serial), + GNUNET_PQ_query_param_uint64 (&min_row), + GNUNET_PQ_query_param_uint64 (&max_results64), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + check_connection (pg); + PREPARE (pg, + "lookup_all_webhooks", + " SELECT" + " webhook_pending_serial AS out_webhook_pending_serial" + " ,next_attempt AS out_next_attempt" + " ,retries AS out_retries" + " ,url AS out_url" + " ,http_method AS out_http_method" + " ,header AS out_header" + " ,body AS out_body" + " FROM merchant.merchant_pending_webhooks" + " WHERE merchant_serial = $1" + " AND webhook_pending_serial > $2" + " ORDER BY webhook_pending_serial ASC" + " LIMIT $3"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_all_webhooks", + params, + &lookup_all_webhooks_cb, + &pwlc); + if (pwlc.extract_failed) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/backenddb/lookup_categories.c b/src/backenddb/lookup_categories.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_categories.h" #include "helper.h" @@ -101,10 +99,11 @@ lookup_categories_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_categories (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - TALER_MERCHANTDB_CategoriesCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_categories ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + TALER_MERCHANTDB_CategoriesCallback cb, + void *cb_cls) { struct LookupCategoryContext tlc = { .cb = cb, @@ -113,30 +112,28 @@ TALER_MERCHANTDB_lookup_categories (struct TALER_MERCHANTDB_PostgresContext *pg, .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_categories", - "SELECT" - " mc.category_serial" - ",mc.category_name" - ",mc.category_name_i18n::TEXT" - ",COALESCE(COUNT(mpc.product_serial),0)" - " AS product_count" - " FROM merchant_categories mc" - " LEFT JOIN merchant_product_categories mpc" - " USING (category_serial)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " GROUP BY mc.category_serial" - " ORDER BY mc.category_serial;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mc.category_serial" + ",mc.category_name" + ",mc.category_name_i18n::TEXT" + ",COALESCE(COUNT(mpc.product_serial),0)" + " AS product_count" + " FROM merchant_categories mc" + " LEFT JOIN merchant_product_categories mpc" + " USING (category_serial)" + " GROUP BY mc.category_serial" + " ORDER BY mc.category_serial;"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_categories", + "", params, &lookup_categories_cb, &tlc); diff --git a/src/backenddb/lookup_categories_by_ids.c b/src/backenddb/lookup_categories_by_ids.c @@ -19,8 +19,6 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_categories_by_ids.h" #include "helper.h" @@ -107,7 +105,6 @@ TALER_MERCHANTDB_lookup_categories_by_ids (struct TALER_MERCHANTDB_PostgresConte .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_array_uint64 (num_category_ids, category_ids, pg->conn), @@ -115,26 +112,25 @@ TALER_MERCHANTDB_lookup_categories_by_ids (struct TALER_MERCHANTDB_PostgresConte }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_categories_by_ids", - "SELECT" - " mc.category_serial" - ",mc.category_name" - ",mc.category_name_i18n::TEXT" - ",COALESCE(COUNT(mpc.product_serial),0)" - " AS product_count" - " FROM merchant_categories mc" - " LEFT JOIN merchant_product_categories mpc" - " USING (category_serial)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " AND mc.category_serial = ANY ($2)" - " GROUP BY mc.category_serial" - " ORDER BY mc.category_serial;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mc.category_serial" + ",mc.category_name" + ",mc.category_name_i18n::TEXT" + ",COALESCE(COUNT(mpc.product_serial),0)" + " AS product_count" + " FROM merchant_categories mc" + " LEFT JOIN merchant_product_categories mpc" + " USING (category_serial)" + " WHERE mc.category_serial = ANY ($1)" + " GROUP BY mc.category_serial" + " ORDER BY mc.category_serial;"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_categories_by_ids", + "", params, &lookup_categories_cb, &tlc); diff --git a/src/backenddb/lookup_contract_terms.c b/src/backenddb/lookup_contract_terms.c @@ -20,25 +20,23 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_contract_terms.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_contract_terms (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - json_t **contract_terms, - uint64_t *order_serial, - struct TALER_ClaimTokenP *claim_token) +TALER_MERCHANTDB_lookup_contract_terms ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + json_t **contract_terms, + uint64_t *order_serial, + struct TALER_ClaimTokenP *claim_token) { enum GNUNET_DB_QueryStatus qs; struct TALER_ClaimTokenP ct; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; @@ -53,25 +51,23 @@ TALER_MERCHANTDB_lookup_contract_terms (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_contract_terms", - "SELECT" - " contract_terms::TEXT" - ",order_serial" - ",claim_token" - " FROM merchant_contract_terms" - " WHERE order_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " contract_terms::TEXT" + ",order_serial" + ",claim_token" + " FROM merchant_contract_terms" + " WHERE order_id=$1"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_contract_terms", + "", params, (NULL != contract_terms) - ? rs - : &rs[1]); + ? rs + : &rs[1]); if (NULL != claim_token) *claim_token = ct; return qs; diff --git a/src/backenddb/lookup_contract_terms2.c b/src/backenddb/lookup_contract_terms2.c @@ -19,27 +19,26 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_contract_terms2.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_contract_terms2 (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - json_t **contract_terms, - uint64_t *order_serial, - bool *paid, - struct TALER_ClaimTokenP *claim_token, - char **pos_key, - enum TALER_MerchantConfirmationAlgorithm *pos_algorithm) +TALER_MERCHANTDB_lookup_contract_terms2 ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + json_t **contract_terms, + uint64_t *order_serial, + bool *paid, + struct TALER_ClaimTokenP *claim_token, + char **pos_key, + enum TALER_MerchantConfirmationAlgorithm *pos_algorithm) { enum GNUNET_DB_QueryStatus qs; struct TALER_ClaimTokenP ct; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; @@ -65,24 +64,22 @@ TALER_MERCHANTDB_lookup_contract_terms2 (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_contract_terms2", - "SELECT" - " contract_terms::TEXT" - ",order_serial" - ",claim_token" - ",paid" - ",pos_key" - ",pos_algorithm" - " FROM merchant_contract_terms" - " WHERE order_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " contract_terms::TEXT" + ",order_serial" + ",claim_token" + ",paid" + ",pos_key" + ",pos_algorithm" + " FROM merchant_contract_terms" + " WHERE order_id=$1"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_contract_terms2", + "", params, (NULL != contract_terms) ? rs diff --git a/src/backenddb/lookup_contract_terms3.c b/src/backenddb/lookup_contract_terms3.c @@ -21,32 +21,30 @@ */ #include "platform.h" #include <sys/types.h> -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_contract_terms3.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_contract_terms3 (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - const char *session_id, - json_t **contract_terms, - uint64_t *order_serial, - bool *paid, - bool *wired, - bool *session_matches, - struct TALER_ClaimTokenP *claim_token, - int16_t *choice_index) +TALER_MERCHANTDB_lookup_contract_terms3 ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + const char *session_id, + json_t **contract_terms, + uint64_t *order_serial, + bool *paid, + bool *wired, + bool *session_matches, + struct TALER_ClaimTokenP *claim_token, + int16_t *choice_index) { enum GNUNET_DB_QueryStatus qs; struct TALER_ClaimTokenP ct; uint16_t ci = 0; bool choice_index_null = false; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), NULL == session_id ? GNUNET_PQ_query_param_null () @@ -76,35 +74,30 @@ TALER_MERCHANTDB_lookup_contract_terms3 (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); *session_matches = false; check_connection (pg); - PREPARE (pg, - "lookup_contract_terms3", - "SELECT" - " contract_terms::TEXT" - ",order_serial" - ",claim_token" - ",paid" - ",wired" - ",(session_id=$3) AS session_matches" - ",choice_index" - " FROM merchant_contract_terms" - " WHERE order_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " contract_terms::TEXT" + ",order_serial" + ",claim_token" + ",paid" + ",wired" + ",(session_id=$2) AS session_matches" + ",choice_index" + " FROM merchant_contract_terms" + " WHERE order_id=$1"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_contract_terms3", + "", params, (NULL != contract_terms) - ? rs - : &rs[1]); + ? rs + : &rs[1]); if (NULL != claim_token) *claim_token = ct; - if (! choice_index_null) - *choice_index = (int16_t) ci; - else - *choice_index = -1; + *choice_index = (choice_index_null) ? -1 : (int16_t) ci; return qs; } diff --git a/src/backenddb/lookup_custom_units_by_names.c b/src/backenddb/lookup_custom_units_by_names.c @@ -89,12 +89,13 @@ lookup_units_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_custom_units_by_names (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *const *units, - size_t num_units, - TALER_MERCHANTDB_UnitsCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_custom_units_by_names ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *const *units, + size_t num_units, + TALER_MERCHANTDB_UnitsCallback cb, + void *cb_cls) { struct LookupUnitsContext luc = { .cb = cb, @@ -102,7 +103,6 @@ TALER_MERCHANTDB_lookup_custom_units_by_names (struct TALER_MERCHANTDB_PostgresC .extract_failed = false }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_array_ptrs_string (num_units, (const char **) units, pg->conn), @@ -110,28 +110,26 @@ TALER_MERCHANTDB_lookup_custom_units_by_names (struct TALER_MERCHANTDB_PostgresC }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_custom_units_by_names", - "WITH mi AS (" - " SELECT merchant_serial FROM merchant_instances WHERE merchant_id=$1" - ")" - "SELECT cu.unit_serial" - " ,cu.unit" - " ,cu.unit_name_long" - " ,cu.unit_name_short" - " ,cu.unit_name_long_i18n" - " ,cu.unit_name_short_i18n" - " ,cu.unit_allow_fraction" - " ,cu.unit_precision_level" - " ,cu.unit_active" - " ,FALSE AS unit_builtin" - " FROM merchant_custom_units cu" - " JOIN mi ON cu.merchant_serial = mi.merchant_serial" - " WHERE cu.unit = ANY ($2)" - " ORDER BY cu.unit"); + TMH_PQ_prepare_anon (pg, + "SELECT cu.unit_serial" + " ,cu.unit" + " ,cu.unit_name_long" + " ,cu.unit_name_short" + " ,cu.unit_name_long_i18n" + " ,cu.unit_name_short_i18n" + " ,cu.unit_allow_fraction" + " ,cu.unit_precision_level" + " ,cu.unit_active" + " ,FALSE AS unit_builtin" + " FROM merchant_custom_units cu" + " WHERE cu.unit = ANY ($1)" + " ORDER BY cu.unit"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_custom_units_by_names", + "", params, &lookup_units_cb, &luc); diff --git a/src/backenddb/lookup_deposits.c b/src/backenddb/lookup_deposits.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_deposits.h" #include "helper.h" @@ -118,7 +116,6 @@ TALER_MERCHANTDB_lookup_deposits ( void *cb_cls) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_end }; @@ -129,32 +126,30 @@ TALER_MERCHANTDB_lookup_deposits ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); /* no preflight check here, run in its own transaction by the caller! */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Finding deposits for h_contract_terms '%s'\n", GNUNET_h2s (&h_contract_terms->hash)); check_connection (pg); - PREPARE (pg, - "lookup_deposits", - "SELECT" - " dcom.exchange_url" - ",dep.coin_pub" - ",dep.amount_with_fee" - ",dep.deposit_fee" - ",dep.refund_fee" - " FROM merchant_deposits dep" - " JOIN merchant_deposit_confirmations dcom" - " USING (deposit_confirmation_serial)" - " WHERE dcom.order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " dcom.exchange_url" + ",dep.coin_pub" + ",dep.amount_with_fee" + ",dep.deposit_fee" + ",dep.refund_fee" + " FROM merchant_deposits dep" + " JOIN merchant_deposit_confirmations dcom" + " USING (deposit_confirmation_serial)" + " WHERE dcom.order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_deposits", + "", params, &lookup_deposits_cb, &ldc); diff --git a/src/backenddb/lookup_deposits_by_contract_and_coin.c b/src/backenddb/lookup_deposits_by_contract_and_coin.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_deposits_by_contract_and_coin.h" #include "helper.h" @@ -245,7 +243,6 @@ TALER_MERCHANTDB_lookup_deposits_by_contract_and_coin ( void *cb_cls) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_auto_from_type (coin_pub), GNUNET_PQ_query_param_end @@ -257,70 +254,63 @@ TALER_MERCHANTDB_lookup_deposits_by_contract_and_coin ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); /* no preflight check here, run in transaction by caller! */ TALER_LOG_DEBUG ("Looking for refund of h_contract_terms %s at `%s'\n", GNUNET_h2s (&h_contract_terms->hash), instance_id); check_connection (pg); - PREPARE (pg, - "lookup_refunds_by_coin_and_contract", - "SELECT" - " refund_amount" - " FROM merchant_refunds" - /* Join to filter by refunds that actually - did work, not only those we approved */ - " JOIN merchant_refund_proofs" - " USING (refund_serial)" - " WHERE coin_pub=$3" - " AND order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " refund_amount" + " FROM merchant_refunds" + /* Join to filter by refunds that actually + did work, not only those we approved */ + " JOIN merchant_refund_proofs" + " USING (refund_serial)" + " WHERE coin_pub=$2" + " AND order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_refunds_by_coin_and_contract", + "", params, &lookup_refunds_cb, &ldcc); if (0 > qs) return qs; - PREPARE (pg, - "lookup_deposits_by_contract_and_coin", - "SELECT" - " mcon.exchange_url" - ",dep.amount_with_fee" - ",dep.deposit_fee" - ",dep.refund_fee" - ",mcon.wire_fee" - ",acc.h_wire" - ",mcon.deposit_timestamp" - ",mct.refund_deadline" - ",mcon.exchange_sig" - ",msig.exchange_pub" - " FROM merchant_contract_terms mct" - " JOIN merchant_deposit_confirmations mcon" - " USING (order_serial)" - " JOIN merchant_deposits dep" - " USING (deposit_confirmation_serial)" - " JOIN merchant_exchange_signing_keys msig" - " ON (mcon.signkey_serial=msig.signkey_serial)" - " JOIN merchant_accounts acc" - " USING (account_serial)" - " WHERE h_contract_terms=$2" - " AND dep.coin_pub=$3" - " AND mct.merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mcon.exchange_url" + ",dep.amount_with_fee" + ",dep.deposit_fee" + ",dep.refund_fee" + ",mcon.wire_fee" + ",acc.h_wire" + ",mcon.deposit_timestamp" + ",mct.refund_deadline" + ",mcon.exchange_sig" + ",msig.exchange_pub" + " FROM merchant_contract_terms mct" + " JOIN merchant_deposit_confirmations mcon" + " USING (order_serial)" + " JOIN merchant_deposits dep" + " USING (deposit_confirmation_serial)" + " JOIN merchant.merchant_exchange_signing_keys msig" + " ON (mcon.signkey_serial=msig.signkey_serial)" + " JOIN merchant_accounts acc" + " USING (account_serial)" + " WHERE h_contract_terms=$1" + " AND dep.coin_pub=$2"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_deposits_by_contract_and_coin", + "", params, &lookup_deposits_by_contract_and_coin_cb, &ldcc); diff --git a/src/backenddb/lookup_deposits_by_order.c b/src/backenddb/lookup_deposits_by_order.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_deposits_by_order.h" #include "helper.h" @@ -119,10 +117,11 @@ lookup_deposits_by_order_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_deposits_by_order (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t order_serial, - TALER_MERCHANTDB_DepositedCoinsCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_deposits_by_order ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t order_serial, + TALER_MERCHANTDB_DepositedCoinsCallback cb, + void *cb_cls) { struct LookupDepositsByOrderContext ldoc = { .pg = pg, @@ -135,30 +134,28 @@ TALER_MERCHANTDB_lookup_deposits_by_order (struct TALER_MERCHANTDB_PostgresConte }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "lookup_deposits_by_order", - "SELECT" - " dep.deposit_serial" - ",mcon.exchange_url" - ",acc.h_wire" - ",mcon.deposit_timestamp" - ",dep.amount_with_fee" - ",dep.deposit_fee" - ",dep.coin_pub" - " FROM merchant_deposits dep" - " JOIN merchant_deposit_confirmations mcon" - " USING(deposit_confirmation_serial)" - " JOIN merchant_accounts acc" - " USING (account_serial)" - " WHERE mcon.order_serial=$1"); - + TMH_PQ_prepare_anon (pg, + "SELECT" + " dep.deposit_serial" + ",mcon.exchange_url" + ",acc.h_wire" + ",mcon.deposit_timestamp" + ",dep.amount_with_fee" + ",dep.deposit_fee" + ",dep.coin_pub" + " FROM merchant_deposits dep" + " JOIN merchant_deposit_confirmations mcon" + " USING(deposit_confirmation_serial)" + " JOIN merchant_accounts acc" + " USING (account_serial)" + " WHERE mcon.order_serial=$1"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_deposits_by_order", + "", params, &lookup_deposits_by_order_cb, &ldoc); - if (qs < 0) return qs; return ldoc.qs; diff --git a/src/backenddb/lookup_donau_keys.c b/src/backenddb/lookup_donau_keys.c @@ -20,18 +20,18 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "donau/donau_service.h" #include "merchant-database/lookup_donau_keys.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_donau_keys (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *donau_url, - struct GNUNET_TIME_Absolute *first_retry, - struct DONAU_Keys **keys) +TALER_MERCHANTDB_lookup_donau_keys ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *donau_url, + struct GNUNET_TIME_Absolute *first_retry, + struct DONAU_Keys **keys) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (donau_url), diff --git a/src/backenddb/lookup_expected_transfer.c b/src/backenddb/lookup_expected_transfer.c @@ -19,28 +19,26 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_expected_transfer.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_expected_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t expected_incoming_serial, - struct GNUNET_TIME_Timestamp *expected_time, - struct TALER_Amount *expected_credit_amount, - struct TALER_WireTransferIdentifierRawP *wtid, - struct TALER_FullPayto *payto_uri, - char **exchange_url, - struct GNUNET_TIME_Timestamp *execution_time, - bool *confirmed, - struct TALER_MasterPublicKeyP *master_pub) +TALER_MERCHANTDB_lookup_expected_transfer ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t expected_incoming_serial, + struct GNUNET_TIME_Timestamp *expected_time, + struct TALER_Amount *expected_credit_amount, + struct TALER_WireTransferIdentifierRawP *wtid, + struct TALER_FullPayto *payto_uri, + char **exchange_url, + struct GNUNET_TIME_Timestamp *execution_time, + bool *confirmed, + struct TALER_MasterPublicKeyP *master_pub) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&expected_incoming_serial), GNUNET_PQ_query_param_end }; @@ -68,35 +66,34 @@ TALER_MERCHANTDB_lookup_expected_transfer (struct TALER_MERCHANTDB_PostgresConte GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); *execution_time = GNUNET_TIME_UNIT_ZERO_TS; memset (expected_credit_amount, 0, sizeof (*expected_credit_amount)); check_connection (pg); - PREPARE (pg, - "lookup_expected_transfer", - "SELECT" - " met.expected_time" - " ,met.confirmed" - " ,met.expected_credit_amount" - " ,met.exchange_url" - " ,met.wtid" - " ,ma.payto_uri" - " ,mts.execution_time" - " ,esk.master_pub" - " FROM merchant_expected_transfers met" - " JOIN merchant_exchange_signing_keys esk" - " USING (signkey_serial)" - " JOIN merchant_accounts ma" - " USING (account_serial)" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - " LEFT JOIN merchant_transfer_signatures mts" - " USING (expected_credit_serial)" - " WHERE inst.merchant_id=$1" - " AND met.expected_credit_serial=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " met.expected_time" + " ,met.confirmed" + " ,met.expected_credit_amount" + " ,met.exchange_url" + " ,met.wtid" + " ,ma.payto_uri" + " ,mts.execution_time" + " ,esk.master_pub" + " FROM merchant_expected_transfers met" + " JOIN merchant.merchant_exchange_signing_keys esk" + " USING (signkey_serial)" + " JOIN merchant_accounts ma" + " USING (account_serial)" + " LEFT JOIN merchant_transfer_signatures mts" + " USING (expected_credit_serial)" + " WHERE met.expected_credit_serial=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_expected_transfer", + "", params, rs); } diff --git a/src/backenddb/lookup_expected_transfers.c b/src/backenddb/lookup_expected_transfers.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_expected_transfers.h" #include "helper.h" @@ -150,17 +148,18 @@ lookup_expected_transfers_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_expected_transfers (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - struct TALER_FullPayto payto_uri, - struct GNUNET_TIME_Timestamp before, - struct GNUNET_TIME_Timestamp after, - int64_t limit, - uint64_t offset, - enum TALER_EXCHANGE_YesNoAll confirmed, - enum TALER_EXCHANGE_YesNoAll verified, - TALER_MERCHANTDB_IncomingCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_expected_transfers ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + struct TALER_FullPayto payto_uri, + struct GNUNET_TIME_Timestamp before, + struct GNUNET_TIME_Timestamp after, + int64_t limit, + uint64_t offset, + enum TALER_EXCHANGE_YesNoAll confirmed, + enum TALER_EXCHANGE_YesNoAll verified, + TALER_MERCHANTDB_IncomingCallback cb, + void *cb_cls) { uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); bool by_time = ( (! GNUNET_TIME_absolute_is_never (before.abs_time)) || @@ -171,7 +170,6 @@ TALER_MERCHANTDB_lookup_expected_transfers (struct TALER_MERCHANTDB_PostgresCont .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_timestamp (&before), GNUNET_PQ_query_param_timestamp (&after), GNUNET_PQ_query_param_uint64 (&offset), @@ -179,7 +177,7 @@ TALER_MERCHANTDB_lookup_expected_transfers (struct TALER_MERCHANTDB_PostgresCont NULL == payto_uri.full_payto ? GNUNET_PQ_query_param_null () /* NULL: do not filter by payto URI */ : GNUNET_PQ_query_param_string (payto_uri.full_payto), - GNUNET_PQ_query_param_bool (! by_time), /* $7: filter by time? */ + GNUNET_PQ_query_param_bool (! by_time), /* $6: filter by time? */ GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == confirmed), /* filter by confirmed? */ GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_YES == confirmed), GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == verified), /* filter by verified? */ @@ -189,86 +187,83 @@ TALER_MERCHANTDB_lookup_expected_transfers (struct TALER_MERCHANTDB_PostgresCont }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_expected_transfers_asc", - "SELECT" - " met.expected_credit_amount" - ",met.wtid" - ",mac.payto_uri" - ",met.exchange_url" - ",met.expected_credit_serial" - ",mts.execution_time" - ",met.confirmed" - ",met.last_http_status" - ",met.last_ec" - ",met.last_detail" - " FROM merchant_expected_transfers met" - " JOIN merchant_accounts mac" - " USING (account_serial)" - " LEFT JOIN merchant_transfer_signatures mts" - " USING (expected_credit_serial)" - " WHERE ( $7 OR " - " (mts.execution_time IS NOT NULL AND" - " mts.execution_time < $2 AND" - " mts.execution_time >= $3) )" - " AND ( (CAST($6 AS TEXT) IS NULL) OR " - " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" - " =REGEXP_REPLACE($6,'\\?.*','')) )" - " AND ( $8 OR " - " (met.confirmed = $9) )" - " AND ( $10 OR " - " ($11 = (200=met.last_http_status) AND" - " (0=met.last_ec) ) )" - " AND merchant_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND (met.expected_credit_serial > $4)" - " ORDER BY met.expected_credit_serial ASC" - " LIMIT $5"); - PREPARE (pg, - "lookup_expected_transfers_desc", - "SELECT" - " met.expected_credit_amount" - ",met.wtid" - ",mac.payto_uri" - ",met.exchange_url" - ",met.expected_credit_serial" - ",mts.execution_time" - ",met.confirmed" - ",met.last_http_status" - ",met.last_ec" - ",met.last_detail" - " FROM merchant_expected_transfers met" - " JOIN merchant_accounts mac" - " USING (account_serial)" - " LEFT JOIN merchant_transfer_signatures mts" - " USING (expected_credit_serial)" - " WHERE ( $7 OR " - " (mts.execution_time IS NOT NULL AND" - " mts.execution_time < $2 AND" - " mts.execution_time >= $3) )" - " AND ( (CAST($6 AS TEXT) IS NULL) OR " - " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" - " =REGEXP_REPLACE($6,'\\?.*','')) )" - " AND ( $8 OR " - " (met.confirmed = $9) )" - " AND ( $10 OR " - " ($11 = (200=met.last_http_status) AND" - " (0=met.last_ec) ) )" - " AND merchant_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND (met.expected_credit_serial < $4)" - " ORDER BY met.expected_credit_serial DESC" - " LIMIT $5"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " met.expected_credit_amount" + ",met.wtid" + ",mac.payto_uri" + ",met.exchange_url" + ",met.expected_credit_serial" + ",mts.execution_time" + ",met.confirmed" + ",met.last_http_status" + ",met.last_ec" + ",met.last_detail" + " FROM merchant_expected_transfers met" + " JOIN merchant_accounts mac" + " USING (account_serial)" + " LEFT JOIN merchant_transfer_signatures mts" + " USING (expected_credit_serial)" + " WHERE ( $6 OR " + " (mts.execution_time IS NOT NULL AND" + " mts.execution_time < $1 AND" + " mts.execution_time >= $2) )" + " AND ( (CAST($5 AS TEXT) IS NULL) OR " + " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($5,'\\?.*','')) )" + " AND ( $7 OR " + " (met.confirmed = $8) )" + " AND ( $9 OR " + " ($10 = (200=met.last_http_status) AND" + " (0=met.last_ec) ) )" + " AND (met.expected_credit_serial > $3)" + " ORDER BY met.expected_credit_serial ASC" + " LIMIT $4"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " met.expected_credit_amount" + ",met.wtid" + ",mac.payto_uri" + ",met.exchange_url" + ",met.expected_credit_serial" + ",mts.execution_time" + ",met.confirmed" + ",met.last_http_status" + ",met.last_ec" + ",met.last_detail" + " FROM merchant_expected_transfers met" + " JOIN merchant_accounts mac" + " USING (account_serial)" + " LEFT JOIN merchant_transfer_signatures mts" + " USING (expected_credit_serial)" + " WHERE ( $6 OR " + " (mts.execution_time IS NOT NULL AND" + " mts.execution_time < $1 AND" + " mts.execution_time >= $2) )" + " AND ( (CAST($5 AS TEXT) IS NULL) OR " + " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($5,'\\?.*','')) )" + " AND ( $7 OR " + " (met.confirmed = $8) )" + " AND ( $9 OR " + " ($10 = (200=met.last_http_status) AND" + " (0=met.last_ec) ) )" + " AND (met.expected_credit_serial < $3)" + " ORDER BY met.expected_credit_serial DESC" + " LIMIT $4"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "lookup_expected_transfers_asc" - : "lookup_expected_transfers_desc", + "", params, &lookup_expected_transfers_cb, &ltc); diff --git a/src/backenddb/lookup_instance_auth.c b/src/backenddb/lookup_instance_auth.c @@ -19,16 +19,16 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_instance_auth.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_instance_auth (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - struct TALER_MERCHANTDB_InstanceAuthSettings *ias) +TALER_MERCHANTDB_lookup_instance_auth ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + struct TALER_MERCHANTDB_InstanceAuthSettings *ias) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), @@ -48,7 +48,7 @@ TALER_MERCHANTDB_lookup_instance_auth (struct TALER_MERCHANTDB_PostgresContext * "SELECT" " auth_hash" ",auth_salt" - " FROM merchant_instances" + " FROM merchant.merchant_instances" " WHERE merchant_id=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "lookup_instance_auth", diff --git a/src/backenddb/lookup_instances.c b/src/backenddb/lookup_instances.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022--2025 Taler Systems SA + Copyright (C) 2022--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 @@ -19,7 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_instances.h" #include "helper.h" @@ -57,7 +56,7 @@ struct LookupInstancesContext * Function to be called with the results of a SELECT statement * that has returned @a num_results results about instances. * - * @param cls of type `struct FindInstancesContext *` + * @param cls of type `struct LookupInstancesContext *` * @param result the postgres result * @param num_results the number of results in @a result */ @@ -186,6 +185,7 @@ TALER_MERCHANTDB_lookup_instances ( .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_bool (active_only), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; @@ -194,63 +194,33 @@ TALER_MERCHANTDB_lookup_instances ( PREPARE (pg, "lookup_instances", "SELECT" - " mi.merchant_serial" - ",mi.merchant_pub" - ",mi.auth_hash" - ",mi.auth_salt" - ",mi.merchant_id" - ",mi.merchant_name" - ",mi.address::TEXT" - ",mi.jurisdiction::TEXT" - ",mi.use_stefan" - ",mi.default_wire_transfer_delay" - ",mi.default_pay_delay" - ",mi.default_refund_delay" - ",mi.website" - ",mi.email" - ",mi.phone_number" - ",mi.phone_validated" - ",mi.email_validated" - ",mi.logo" - ",mi.default_wire_transfer_rounding_interval::TEXT" - ",mk.merchant_priv" - " FROM merchant_instances mi" - " LEFT JOIN merchant_keys mk" - " USING (merchant_serial)"); - PREPARE (pg, - "lookup_active_instances", - "SELECT " - " mi.merchant_serial" - ",mi.merchant_pub" - ",mi.auth_hash" - ",mi.auth_salt" - ",mi.merchant_id" - ",mi.merchant_name" - ",mi.address::TEXT" - ",mi.jurisdiction::TEXT" - ",mi.use_stefan" - ",mi.default_wire_transfer_delay" - ",mi.default_pay_delay" - ",mi.default_refund_delay" - ",mi.website" - ",mi.email" - ",mi.phone_number" - ",mi.phone_validated" - ",mi.email_validated" - ",mi.logo" - ",mi.default_wire_transfer_rounding_interval::TEXT" - ",mk.merchant_priv" - " FROM merchant_instances mi" - " JOIN merchant_keys mk" - " USING (merchant_serial)"); - qs = GNUNET_PQ_eval_prepared_multi_select ( - pg->conn, - active_only - ? "lookup_active_instances" - : "lookup_instances", - params, - &lookup_instances_cb, - &lic); + " merchant_serial" + " ,merchant_pub" + " ,auth_hash" + " ,auth_salt" + " ,merchant_priv" + " ,merchant_id" + " ,merchant_name" + " ,address::TEXT" + " ,jurisdiction::TEXT" + " ,use_stefan" + " ,phone_validated" + " ,email_validated" + " ,default_wire_transfer_delay" + " ,default_pay_delay" + " ,default_refund_delay" + " ,website" + " ,email" + " ,phone_number" + " ,logo" + " ,default_wire_transfer_rounding_interval::TEXT" + " FROM merchant.merchant_instances" + " WHERE (NOT $1 OR merchant_priv IS NOT NULL)"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_instances", + params, + &lookup_instances_cb, + &lic); if (0 > lic.qs) return lic.qs; return qs; @@ -258,11 +228,12 @@ TALER_MERCHANTDB_lookup_instances ( enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - bool active_only, - TALER_MERCHANTDB_InstanceCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + bool active_only, + TALER_MERCHANTDB_InstanceCallback cb, + void *cb_cls) { struct LookupInstancesContext lic = { .cb = cb, @@ -270,6 +241,7 @@ TALER_MERCHANTDB_lookup_instance (struct TALER_MERCHANTDB_PostgresContext *pg, .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_bool (active_only), GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_end }; @@ -279,65 +251,34 @@ TALER_MERCHANTDB_lookup_instance (struct TALER_MERCHANTDB_PostgresContext *pg, PREPARE (pg, "lookup_instance", "SELECT" - " mi.merchant_serial" - ",mi.merchant_pub" - ",mi.auth_hash" - ",mi.auth_salt" - ",mi.merchant_id" - ",mi.merchant_name" - ",mi.address::TEXT" - ",mi.jurisdiction::TEXT" - ",mi.use_stefan" - ",mi.default_wire_transfer_delay" - ",mi.default_pay_delay" - ",mi.default_refund_delay" - ",mi.website" - ",mi.email" - ",mi.phone_number" - ",mi.phone_validated" - ",mi.email_validated" - ",mi.logo" - ",mi.default_wire_transfer_rounding_interval::TEXT" - ",mk.merchant_priv" - " FROM merchant_instances mi" - " LEFT JOIN merchant_keys mk" - " USING (merchant_serial)" - " WHERE merchant_id=$1"); - PREPARE (pg, - "lookup_active_instance", - "SELECT" - " mi.merchant_serial" - ",mi.merchant_pub" - ",mi.auth_hash" - ",mi.auth_salt" - ",mi.merchant_id" - ",mi.merchant_name" - ",mi.address::TEXT" - ",mi.jurisdiction::TEXT" - ",mi.use_stefan" - ",mi.default_wire_transfer_delay" - ",mi.default_pay_delay" - ",mi.default_refund_delay" - ",mi.website" - ",mi.email" - ",mi.phone_number" - ",mi.phone_validated" - ",mi.email_validated" - ",mi.logo" - ",mi.default_wire_transfer_rounding_interval::TEXT" - ",mk.merchant_priv" - " FROM merchant_instances mi" - " JOIN merchant_keys mk" - " USING (merchant_serial)" - " WHERE merchant_id=$1"); - qs = GNUNET_PQ_eval_prepared_multi_select ( - pg->conn, - active_only - ? "lookup_active_instance" - : "lookup_instance", - params, - &lookup_instances_cb, - &lic); + " merchant_serial" + " ,merchant_pub" + " ,auth_hash" + " ,auth_salt" + " ,merchant_priv" + " ,merchant_id" + " ,merchant_name" + " ,address::TEXT" + " ,jurisdiction::TEXT" + " ,use_stefan" + " ,phone_validated" + " ,email_validated" + " ,default_wire_transfer_delay" + " ,default_pay_delay" + " ,default_refund_delay" + " ,website" + " ,email" + " ,phone_number" + " ,logo" + " ,default_wire_transfer_rounding_interval::TEXT" + " FROM merchant.merchant_instances" + " WHERE (merchant_id = $2)" + " AND (NOT $1 OR merchant_priv IS NOT NULL)"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "lookup_instance", + params, + &lookup_instances_cb, + &lic); if (0 > lic.qs) return lic.qs; return qs; diff --git a/src/backenddb/lookup_inventory_products.c b/src/backenddb/lookup_inventory_products.c @@ -19,8 +19,6 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_inventory_products.h" #include "helper.h" @@ -146,74 +144,72 @@ TALER_MERCHANTDB_lookup_inventory_products ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_inventory_products", - "SELECT" - " description" - ",description_i18n::TEXT" - ",product_name" - ",unit" - ",price_array" - ",CASE WHEN minv.total_stock = 9223372036854775807" - " THEN minv.total_stock" - " ELSE (GREATEST(0, rt.remaining_total) / 1000000)::INT8" - " END AS remaining_stock" - ",CASE WHEN minv.total_stock = 9223372036854775807" - " THEN 2147483647" - " ELSE mod(GREATEST(0, rt.remaining_total), 1000000)::INT4" - " END AS remaining_stock_frac" - ",taxes::TEXT" - ",image_hash" - ",allow_fractional_quantity" - ",fractional_precision_level" - ",product_id" - ",t.category_array AS categories" - " FROM merchant_inventory minv" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - ",LATERAL (" - " SELECT ARRAY (" - " SELECT mpc.category_serial" - " FROM merchant_product_categories mpc" - " WHERE mpc.product_serial = minv.product_serial" - " ) AS category_array" - " ) t" - ",LATERAL (" - " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " AS total_locked" - " FROM merchant_inventory_locks mil" - " WHERE mil.product_serial = minv.product_serial" - " ) il" - ",LATERAL (" - " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " AS total_locked" - " FROM merchant_order_locks mol" - " WHERE mol.product_serial = minv.product_serial" - " ) ol" - ",LATERAL (" - " SELECT" - " (minv.total_stock::NUMERIC * 1000000" - " + minv.total_stock_frac::NUMERIC)" - " - (minv.total_sold::NUMERIC * 1000000" - " + minv.total_sold_frac::NUMERIC)" - " - (minv.total_lost::NUMERIC * 1000000" - " + minv.total_lost_frac::NUMERIC)" - " - il.total_locked" - " - ol.total_locked" - " AS remaining_total" - " ) rt" - " WHERE inst.merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " description" + ",description_i18n::TEXT" + ",product_name" + ",unit" + ",price_array" + ",CASE WHEN minv.total_stock = 9223372036854775807" + " THEN minv.total_stock" + " ELSE (GREATEST(0, rt.remaining_total) / 1000000)::INT8" + " END AS remaining_stock" + ",CASE WHEN minv.total_stock = 9223372036854775807" + " THEN 2147483647" + " ELSE mod(GREATEST(0, rt.remaining_total), 1000000)::INT4" + " END AS remaining_stock_frac" + ",taxes::TEXT" + ",image_hash" + ",allow_fractional_quantity" + ",fractional_precision_level" + ",product_id" + ",t.category_array AS categories" + " FROM merchant_inventory minv" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " ) AS category_array" + " ) t" + ",LATERAL (" + " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " AS total_locked" + " FROM merchant_inventory_locks mil" + " WHERE mil.product_serial = minv.product_serial" + " ) il" + ",LATERAL (" + " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " AS total_locked" + " FROM merchant_order_locks mol" + " WHERE mol.product_serial = minv.product_serial" + " ) ol" + ",LATERAL (" + " SELECT" + " (minv.total_stock::NUMERIC * 1000000" + " + minv.total_stock_frac::NUMERIC)" + " - (minv.total_sold::NUMERIC * 1000000" + " + minv.total_sold_frac::NUMERIC)" + " - (minv.total_lost::NUMERIC * 1000000" + " + minv.total_lost_frac::NUMERIC)" + " - il.total_locked" + " - ol.total_locked" + " AS remaining_total" + " ) rt"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_inventory_products", + "", params, &lookup_inventory_products_cb, &plc); diff --git a/src/backenddb/lookup_inventory_products_filtered.c b/src/backenddb/lookup_inventory_products_filtered.c @@ -19,8 +19,6 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_inventory_products_filtered.h" #include "helper.h" @@ -143,7 +141,6 @@ TALER_MERCHANTDB_lookup_inventory_products_filtered ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), (0 == num_product_ids) ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_array_ptrs_string ( @@ -159,81 +156,80 @@ TALER_MERCHANTDB_lookup_inventory_products_filtered ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_inventory_products_filtered", - "SELECT" - " description" - ",description_i18n::TEXT" - ",product_name" - ",unit" - ",price_array" - ",CASE WHEN minv.total_stock = 9223372036854775807" - " THEN minv.total_stock" - " ELSE (GREATEST(0, rt.remaining_total) / 1000000)::INT8" - " END AS remaining_stock" - ",CASE WHEN minv.total_stock = 9223372036854775807" - " THEN 2147483647" - " ELSE mod(GREATEST(0, rt.remaining_total), 1000000)::INT4" - " END AS remaining_stock_frac" - ",taxes::TEXT" - ",image_hash" - ",allow_fractional_quantity" - ",fractional_precision_level" - ",product_id" - ",t.category_array AS categories" - " FROM merchant_inventory minv" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - ",LATERAL (" - " SELECT ARRAY (" - " SELECT mpc.category_serial" - " FROM merchant_product_categories mpc" - " WHERE mpc.product_serial = minv.product_serial" - " ) AS category_array" - " ) t" - ",LATERAL (" - " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " AS total_locked" - " FROM merchant_inventory_locks mil" - " WHERE mil.product_serial = minv.product_serial" - " ) il" - ",LATERAL (" - " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" - " + total_locked_frac::NUMERIC), 0)" - " AS total_locked" - " FROM merchant_order_locks mol" - " WHERE mol.product_serial = minv.product_serial" - " ) ol" - ",LATERAL (" - " SELECT" - " (minv.total_stock::NUMERIC * 1000000" - " + minv.total_stock_frac::NUMERIC)" - " - (minv.total_sold::NUMERIC * 1000000" - " + minv.total_sold_frac::NUMERIC)" - " - (minv.total_lost::NUMERIC * 1000000" - " + minv.total_lost_frac::NUMERIC)" - " - il.total_locked" - " - ol.total_locked" - " AS remaining_total" - " ) rt" - " WHERE inst.merchant_id=$1" - " AND (" - " (COALESCE (array_length ($2::TEXT[], 1), 0) > 0" - " AND minv.product_id = ANY ($2::TEXT[]))" - " OR" - " (COALESCE (array_length ($3::BIGINT[], 1), 0) > 0" - " AND EXISTS (" - " SELECT 1" - " FROM merchant_product_categories mpc" - " WHERE mpc.product_serial = minv.product_serial" - " AND mpc.category_serial = ANY ($3::BIGINT[])" - " ))" - " )"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " description" + ",description_i18n::TEXT" + ",product_name" + ",unit" + ",price_array" + ",CASE WHEN minv.total_stock = 9223372036854775807" + " THEN minv.total_stock" + " ELSE (GREATEST(0, rt.remaining_total) / 1000000)::INT8" + " END AS remaining_stock" + ",CASE WHEN minv.total_stock = 9223372036854775807" + " THEN 2147483647" + " ELSE mod(GREATEST(0, rt.remaining_total), 1000000)::INT4" + " END AS remaining_stock_frac" + ",taxes::TEXT" + ",image_hash" + ",allow_fractional_quantity" + ",fractional_precision_level" + ",product_id" + ",t.category_array AS categories" + " FROM merchant_inventory minv" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " ) AS category_array" + " ) t" + ",LATERAL (" + " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " AS total_locked" + " FROM merchant_inventory_locks mil" + " WHERE mil.product_serial = minv.product_serial" + " ) il" + ",LATERAL (" + " SELECT COALESCE(SUM(total_locked::NUMERIC * 1000000" + " + total_locked_frac::NUMERIC), 0)" + " AS total_locked" + " FROM merchant_order_locks mol" + " WHERE mol.product_serial = minv.product_serial" + " ) ol" + ",LATERAL (" + " SELECT" + " (minv.total_stock::NUMERIC * 1000000" + " + minv.total_stock_frac::NUMERIC)" + " - (minv.total_sold::NUMERIC * 1000000" + " + minv.total_sold_frac::NUMERIC)" + " - (minv.total_lost::NUMERIC * 1000000" + " + minv.total_lost_frac::NUMERIC)" + " - il.total_locked" + " - ol.total_locked" + " AS remaining_total" + " ) rt" + " WHERE (" + " (COALESCE (array_length ($1::TEXT[], 1), 0) > 0" + " AND minv.product_id = ANY ($1::TEXT[]))" + " OR" + " (COALESCE (array_length ($2::BIGINT[], 1), 0) > 0" + " AND EXISTS (" + " SELECT 1" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = minv.product_serial" + " AND mpc.category_serial = ANY ($2::BIGINT[])" + " ))" + " )"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_inventory_products_filtered", + "", params, &lookup_inventory_products_cb, &plc); diff --git a/src/backenddb/lookup_login_tokens.c b/src/backenddb/lookup_login_tokens.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_login_tokens.h" #include "helper.h" @@ -107,12 +105,13 @@ lookup_login_tokens_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_login_tokens (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t offset, - int64_t limit, - TALER_MERCHANTDB_LoginTokensCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_login_tokens ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t offset, + int64_t limit, + TALER_MERCHANTDB_LoginTokensCallback cb, + void *cb_cls) { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); @@ -123,7 +122,6 @@ TALER_MERCHANTDB_lookup_login_tokens (struct TALER_MERCHANTDB_PostgresContext *p .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_timestamp (&now), GNUNET_PQ_query_param_uint64 (&offset), GNUNET_PQ_query_param_uint64 (&plimit), @@ -131,46 +129,45 @@ TALER_MERCHANTDB_lookup_login_tokens (struct TALER_MERCHANTDB_PostgresContext *p }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_login_tokens_asc", - "SELECT" - " token" - ",serial" - ",creation_time" - ",expiration_time" - ",validity_scope" - ",description" - " FROM merchant_login_tokens" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND expiration_time > $2" - " AND serial > $3" - " ORDER BY serial ASC" - " LIMIT $4"); - PREPARE (pg, - "lookup_login_tokens_desc", - "SELECT" - " token" - ",serial" - ",creation_time" - ",expiration_time" - ",validity_scope" - ",description" - " FROM merchant_login_tokens" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND expiration_time > $2" - " AND serial < $3" - " ORDER BY serial DESC" - " LIMIT $4"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " token" + ",serial" + ",creation_time" + ",expiration_time" + ",validity_scope" + ",description" + " FROM merchant_login_tokens" + " WHERE expiration_time > $1" + " AND serial > $2" + " ORDER BY serial ASC" + " LIMIT $3"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " token" + ",serial" + ",creation_time" + ",expiration_time" + ",validity_scope" + ",description" + " FROM merchant_login_tokens" + " WHERE expiration_time > $1" + " AND serial < $2" + " ORDER BY serial DESC" + " LIMIT $3"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "lookup_login_tokens_asc" - : "lookup_login_tokens_desc", + "", params, &lookup_login_tokens_cb, &plc); diff --git a/src/backenddb/lookup_mfa_challenge.c b/src/backenddb/lookup_mfa_challenge.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "taler/taler_merchant_util.h" #include "merchantdb_lib.h" @@ -86,8 +84,8 @@ TALER_MERCHANTDB_lookup_mfa_challenge ( " ,tc.required_address" " ,tc.tan_channel::TEXT" " ,mi.merchant_id" - " FROM tan_challenges tc" - " JOIN merchant_instances mi" + " FROM merchant.tan_challenges tc" + " JOIN merchant.merchant_instances mi" " USING (merchant_serial)" " WHERE (challenge_id = $1)" " AND (h_body = $2)" @@ -95,6 +93,8 @@ TALER_MERCHANTDB_lookup_mfa_challenge ( /* Initialize to conservative values in case qs ends up <= 0 */ *tan_channel = TALER_MERCHANT_MFA_CHANNEL_NONE; *op = TALER_MERCHANT_MFA_CO_NONE; + *instance_name = NULL; + *required_address = NULL; *retry_counter = 0; qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "lookup_mfa_challenge", diff --git a/src/backenddb/lookup_order.c b/src/backenddb/lookup_order.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_order.h" #include "helper.h" @@ -39,7 +37,6 @@ TALER_MERCHANTDB_lookup_order ( struct TALER_ClaimTokenP ct; enum GNUNET_DB_QueryStatus qs; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; @@ -53,25 +50,23 @@ TALER_MERCHANTDB_lookup_order ( GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Finding contract term, order_id: '%s', instance_id: '%s'.\n", order_id, instance_id); check_connection (pg); - PREPARE (pg, - "lookup_order", - "SELECT" - " contract_terms::TEXT" - ",claim_token" - ",h_post_data" - " FROM merchant_orders" - " WHERE merchant_orders.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_orders.order_id=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " contract_terms::TEXT" + ",claim_token" + ",h_post_data" + " FROM merchant_orders" + " WHERE order_id=$1"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_order", + "", params, rs); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) diff --git a/src/backenddb/lookup_order_by_fulfillment.c b/src/backenddb/lookup_order_by_fulfillment.c @@ -19,23 +19,21 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_order_by_fulfillment.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_order_by_fulfillment (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *fulfillment_url, - const char *session_id, - bool allow_refunded_for_repurchase, - char **order_id) +TALER_MERCHANTDB_lookup_order_by_fulfillment ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *fulfillment_url, + const char *session_id, + bool allow_refunded_for_repurchase, + char **order_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (fulfillment_url), GNUNET_PQ_query_param_string (session_id), GNUNET_PQ_query_param_bool (allow_refunded_for_repurchase), @@ -47,32 +45,30 @@ TALER_MERCHANTDB_lookup_order_by_fulfillment (struct TALER_MERCHANTDB_PostgresCo GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_order_by_fulfillment", - "SELECT" - " mct.order_id" - " FROM merchant_contract_terms mct" - " LEFT JOIN merchant_refunds mref" - " USING (order_serial)" - " WHERE fulfillment_url=$2" - " AND session_id=$3" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND ((CAST($4 AS BOOL)) OR" - " mref.refund_serial IS NULL)" - /* Theoretically, multiple paid orders - for the same fulfillment URL could - exist for this session_id -- if a - wallet was broken and did multiple - payments without repurchase detection. - So we need to limit to 1 when returning! */ - " ORDER BY order_id DESC" - " LIMIT 1;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mct.order_id" + " FROM merchant_contract_terms mct" + " LEFT JOIN merchant_refunds mref" + " USING (order_serial)" + " WHERE fulfillment_url=$1" + " AND session_id=$2" + " AND ((CAST($3 AS BOOL)) OR" + " mref.refund_serial IS NULL)" + /* Theoretically, multiple paid orders + for the same fulfillment URL could + exist for this session_id -- if a + wallet was broken and did multiple + payments without repurchase detection. + So we need to limit to 1 when returning! */ + " ORDER BY order_id DESC" + " LIMIT 1;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_order_by_fulfillment", + "", params, rs); } diff --git a/src/backenddb/lookup_order_charity.c b/src/backenddb/lookup_order_charity.c @@ -23,7 +23,6 @@ * @author Bohdan Potuzhnyi * @author Vlada Svirsh */ - #include "platform.h" #include <taler/taler_error_codes.h> #include <taler/taler_dbevents.h> @@ -40,23 +39,18 @@ TALER_MERCHANTDB_lookup_order_charity ( const char *instance_id, const char *donau_url, uint64_t *charity_id, - struct DONAU_CharityPrivateKeyP *charity_priv, struct TALER_Amount *charity_max_per_year, struct TALER_Amount *charity_receipts_to_date, json_t **donau_keys_json, uint64_t *donau_instance_serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (donau_url), GNUNET_PQ_query_param_end }; - struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("charity_id", charity_id), - GNUNET_PQ_result_spec_auto_from_type ("merchant_priv", - charity_priv), TALER_PQ_result_spec_json ("keys_json", donau_keys_json), TALER_PQ_result_spec_amount_with_currency ("charity_max_per_year", @@ -68,28 +62,23 @@ TALER_MERCHANTDB_lookup_order_charity ( GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_donau_charity", - "SELECT" - " di.donau_instances_serial" - " ,di.charity_id" - " ,k.merchant_priv" - " ,dk.keys_json::TEXT" - " ,di.charity_max_per_year" - " ,di.charity_receipts_to_date" - " FROM merchant_donau_instances di" - " JOIN merchant_donau_keys dk" - " ON dk.donau_url = di.donau_url" - " JOIN merchant_instances mi" - " ON mi.merchant_serial = di.merchant_instance_serial" - " JOIN merchant_keys k" - " ON k.merchant_serial = mi.merchant_serial" - " WHERE mi.merchant_id = $1" - " AND di.donau_url = $2;"); - + TMH_PQ_prepare_anon (pg, + "SELECT" + " di.donau_instances_serial" + " ,di.charity_id" + " ,dk.keys_json::TEXT" + " ,di.charity_max_per_year" + " ,di.charity_receipts_to_date" + " FROM merchant_donau_instances di" + " JOIN merchant.merchant_donau_keys dk" + " ON dk.donau_url = di.donau_url" + " WHERE di.donau_url = $1;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_donau_charity", + "", params, rs); } diff --git a/src/backenddb/lookup_order_status.c b/src/backenddb/lookup_order_status.c @@ -25,47 +25,40 @@ #include "merchant-database/lookup_order_status.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_order_status (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - struct TALER_PrivateContractHashP *h_contract_terms, - bool *paid) +TALER_MERCHANTDB_lookup_order_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + struct TALER_PrivateContractHashP *h_contract_terms, + bool *paid) { - uint8_t paid8; - enum GNUNET_DB_QueryStatus qs; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_auto_from_type ("h_contract_terms", h_contract_terms), - GNUNET_PQ_result_spec_auto_from_type ("paid", - &paid8), + GNUNET_PQ_result_spec_bool ("paid", + paid), GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_order_status", - "SELECT" - " h_contract_terms" - ",paid" - " FROM merchant_contract_terms" - " WHERE merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND order_id=$2"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_order_status", - params, - rs); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - *paid = (0 != paid8); - else - *paid = false; /* just to be safe(r) */ - return qs; + TMH_PQ_prepare_anon (pg, + "SELECT" + " h_contract_terms" + ",paid" + " FROM merchant_contract_terms" + " WHERE order_id=$1"); + *paid = false; + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); } diff --git a/src/backenddb/lookup_order_status_by_serial.c b/src/backenddb/lookup_order_status_by_serial.c @@ -19,22 +19,21 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_order_status_by_serial.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_order_status_by_serial (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t order_serial, - char **order_id, - struct TALER_PrivateContractHashP *h_contract_terms, - bool *paid) +TALER_MERCHANTDB_lookup_order_status_by_serial ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t order_serial, + char **order_id, + struct TALER_PrivateContractHashP *h_contract_terms, + bool *paid) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&order_serial), GNUNET_PQ_query_param_end }; @@ -48,22 +47,20 @@ TALER_MERCHANTDB_lookup_order_status_by_serial (struct TALER_MERCHANTDB_Postgres GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); *paid = false; /* just to be safe(r) */ - PREPARE (pg, - "lookup_order_status_by_serial", - "SELECT" - " h_contract_terms" - ",order_id" - ",paid" - " FROM merchant_contract_terms" - " WHERE merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND order_serial=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " h_contract_terms" + ",order_id" + ",paid" + " FROM merchant_contract_terms" + " WHERE order_serial=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_order_status_by_serial", + "", params, rs); } diff --git a/src/backenddb/lookup_order_summary.c b/src/backenddb/lookup_order_summary.c @@ -19,21 +19,20 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_order_summary.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_order_summary (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - struct GNUNET_TIME_Timestamp *timestamp, - uint64_t *order_serial) +TALER_MERCHANTDB_lookup_order_summary ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + struct GNUNET_TIME_Timestamp *timestamp, + uint64_t *order_serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; @@ -45,30 +44,24 @@ TALER_MERCHANTDB_lookup_order_summary (struct TALER_MERCHANTDB_PostgresContext * GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_order_summary", - "(SELECT" - " creation_time" - ",order_serial" - " FROM merchant_contract_terms" - " WHERE merchant_contract_terms.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_contract_terms.order_id=$2)" - "UNION" - "(SELECT" - " creation_time" - ",order_serial" - " FROM merchant_orders" - " WHERE merchant_orders.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND merchant_orders.order_id=$2)"); + TMH_PQ_prepare_anon (pg, + "(SELECT" + " creation_time" + ",order_serial" + " FROM merchant_contract_terms" + " WHERE order_id=$1)" + "UNION" + "(SELECT" + " creation_time" + ",order_serial" + " FROM merchant_orders" + " WHERE order_id=$1)"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_order_summary", + "", params, rs); } diff --git a/src/backenddb/lookup_orders.c b/src/backenddb/lookup_orders.c @@ -20,8 +20,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_orders.h" #include "helper.h" @@ -111,18 +109,17 @@ TALER_MERCHANTDB_lookup_orders ( uint64_t limit = (of->delta > 0) ? of->delta : -of->delta; struct GNUNET_PQ_QueryParam params[] = { /* $1 */ - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&limit), GNUNET_PQ_query_param_uint64 (&of->start_row), GNUNET_PQ_query_param_timestamp (&of->date), GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_ALL == of->paid)), - /* $6 */ + /* $5 */ GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_YES == of->paid)), GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_ALL == of->refunded)), GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_YES == of->refunded)), GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_ALL == of->wired)), GNUNET_PQ_query_param_bool ((TALER_EXCHANGE_YNA_YES == of->wired)), - /* $11 */ + /* $10 */ GNUNET_PQ_query_param_bool (NULL == of->session_id), NULL == of->session_id ? GNUNET_PQ_query_param_null () @@ -137,151 +134,132 @@ TALER_MERCHANTDB_lookup_orders ( GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; - char stmt[128]; + GNUNET_assert (NULL != pg->current_merchant_id); + 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", of->paid, of->refunded, of->wired); - GNUNET_snprintf (stmt, - sizeof (stmt), - "lookup_orders_%s", - (of->delta > 0) ? "inc" : "dec"); - PREPARE (pg, - "lookup_orders_dec", - "(SELECT" - " order_id" - ",order_serial" - ",creation_time" - " FROM merchant_orders" - " WHERE merchant_orders.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND" - " order_serial < $3" - " AND" - " creation_time < $4" - " AND ($5 OR " - " NOT CAST($6 AS BOOL))" /* unclaimed orders are never paid */ - " AND ($7 OR " - " NOT CAST($8 AS BOOL))"/* unclaimed orders are never refunded */ - " AND ($9 OR " - " NOT CAST($10 AS BOOL))" /* unclaimed orders are never wired */ - " AND" - " order_serial NOT IN" - " (SELECT order_serial" - " FROM merchant_contract_terms)" /* only select unclaimed orders */ - " AND ($11 OR " - " ($12 = session_id))" - " AND ($13 OR " - " ($14 = fulfillment_url))" - " AND ( ($15::TEXT IS NULL) OR " - " (LOWER(contract_terms ->> 'summary') LIKE LOWER($15)) )" - " ORDER BY order_serial DESC" - " LIMIT $2)" - "UNION " /* union ensures elements are distinct! */ - "(SELECT" - " order_id" - ",order_serial" - ",creation_time" - " FROM merchant_contract_terms" - " WHERE merchant_contract_terms.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND" - " order_serial < $3" - " AND" - " creation_time < $4" - " AND ($5 OR " - " (CAST($6 AS BOOL) = paid))" - " AND ($7 OR " - " (CAST($8 AS BOOL) = (order_serial IN" - " (SELECT order_serial " - " FROM merchant_refunds))))" - " AND ($9 OR" - " (CAST($10 AS BOOL) = wired))" - " AND ($11 OR " - " ($12 = session_id))" - " AND ($13 OR " - " ($14 = fulfillment_url))" - " AND ( ($15::TEXT IS NULL) OR " - " (LOWER(contract_terms ->> 'summary') LIKE LOWER($15)) )" - " ORDER BY order_serial DESC" - " LIMIT $2)" - " ORDER BY order_serial DESC" - " LIMIT $2"); - - PREPARE (pg, - "lookup_orders_inc", - "(SELECT" - " order_id" - ",order_serial" - ",creation_time" - " FROM merchant_orders" - " WHERE merchant_orders.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND" - " order_serial > $3" - " AND" - " creation_time > $4" - " AND ($5 OR " - " NOT CAST($6 AS BOOL))" /* unclaimed orders are never paid */ - " AND ($7 OR " - " NOT CAST($8 AS BOOL))"/* unclaimed orders are never refunded */ - " AND ($9 OR " - " NOT CAST($10 AS BOOL))" /* unclaimed orders are never wired */ - " AND" - " (order_serial NOT IN" - " (SELECT order_serial" - " FROM merchant_contract_terms))" /* only select unclaimed orders */ - " AND ($11 OR " - " ($12 = session_id))" - " AND ($13 OR " - " ($14 = fulfillment_url))" - " AND ( ($15::TEXT IS NULL) OR " - " (LOWER(contract_terms ->> 'summary') LIKE LOWER($15)) )" - " ORDER BY order_serial ASC" - " LIMIT $2)" - "UNION " /* union ensures elements are distinct! */ - "(SELECT" - " order_id" - ",order_serial" - ",creation_time" - " FROM merchant_contract_terms" - " WHERE merchant_contract_terms.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND" - " order_serial > $3" - " AND" - " creation_time > $4" - " AND ($5 OR " - " (CAST($6 AS BOOL) = paid))" - " AND ($7 OR " - " (CAST($8 AS BOOL) = (order_serial IN" - " (SELECT order_serial " - " FROM merchant_refunds))))" - " AND ($9 OR" - " (CAST($10 AS BOOL) = wired))" - " AND ($11 OR " - " ($12 = session_id))" - " AND ($13 OR " - " ($14 = fulfillment_url))" - " AND ( ($15::TEXT IS NULL) OR " - " (LOWER(contract_terms ->> 'summary') LIKE LOWER($15)) )" - " ORDER BY order_serial ASC" - " LIMIT $2)" - " ORDER BY order_serial ASC" - " LIMIT $2"); + if (of->delta > 0) + { + TMH_PQ_prepare_anon (pg, + "(SELECT" + " order_id" + ",order_serial" + ",creation_time" + " FROM merchant_orders" + " WHERE order_serial > $2" + " AND" + " creation_time > $3" + " AND ($4 OR " + " NOT CAST($5 AS BOOL))" /* unclaimed orders are never paid */ + " AND ($6 OR " + " NOT CAST($7 AS BOOL))"/* unclaimed orders are never refunded */ + " AND ($8 OR " + " NOT CAST($9 AS BOOL))" /* unclaimed orders are never wired */ + " AND" + " (order_serial NOT IN" + " (SELECT order_serial" + " FROM merchant_contract_terms))" /* only select unclaimed orders */ + " AND ($10 OR " + " ($11 = session_id))" + " AND ($12 OR " + " ($13 = fulfillment_url))" + " AND ( ($14::TEXT IS NULL) OR " + " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " ORDER BY order_serial ASC" + " LIMIT $1)" + "UNION " /* union ensures elements are distinct! */ + "(SELECT" + " order_id" + ",order_serial" + ",creation_time" + " FROM merchant_contract_terms" + " WHERE order_serial > $2" + " AND" + " creation_time > $3" + " AND ($4 OR " + " (CAST($5 AS BOOL) = paid))" + " AND ($6 OR " + " (CAST($7 AS BOOL) = (order_serial IN" + " (SELECT order_serial " + " FROM merchant_refunds))))" + " AND ($8 OR" + " (CAST($9 AS BOOL) = wired))" + " AND ($10 OR " + " ($11 = session_id))" + " AND ($12 OR " + " ($13 = fulfillment_url))" + " AND ( ($14::TEXT IS NULL) OR " + " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " ORDER BY order_serial ASC" + " LIMIT $1)" + " ORDER BY order_serial ASC" + " LIMIT $1"); + } + else + { + TMH_PQ_prepare_anon (pg, + "(SELECT" + " order_id" + ",order_serial" + ",creation_time" + " FROM merchant_orders" + " WHERE order_serial < $2" + " AND" + " creation_time < $3" + " AND ($4 OR " + " NOT CAST($5 AS BOOL))" /* unclaimed orders are never paid */ + " AND ($6 OR " + " NOT CAST($7 AS BOOL))"/* unclaimed orders are never refunded */ + " AND ($8 OR " + " NOT CAST($9 AS BOOL))" /* unclaimed orders are never wired */ + " AND" + " order_serial NOT IN" + " (SELECT order_serial" + " FROM merchant_contract_terms)" /* only select unclaimed orders */ + " AND ($10 OR " + " ($11 = session_id))" + " AND ($12 OR " + " ($13 = fulfillment_url))" + " AND ( ($14::TEXT IS NULL) OR " + " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " ORDER BY order_serial DESC" + " LIMIT $1)" + "UNION " /* union ensures elements are distinct! */ + "(SELECT" + " order_id" + ",order_serial" + ",creation_time" + " FROM merchant_contract_terms" + " WHERE order_serial < $2" + " AND" + " creation_time < $3" + " AND ($4 OR " + " (CAST($5 AS BOOL) = paid))" + " AND ($6 OR " + " (CAST($7 AS BOOL) = (order_serial IN" + " (SELECT order_serial " + " FROM merchant_refunds))))" + " AND ($8 OR" + " (CAST($9 AS BOOL) = wired))" + " AND ($10 OR " + " ($11 = session_id))" + " AND ($12 OR " + " ($13 = fulfillment_url))" + " AND ( ($14::TEXT IS NULL) OR " + " (LOWER(contract_terms ->> 'summary') LIKE LOWER($14)) )" + " ORDER BY order_serial DESC" + " LIMIT $1)" + " ORDER BY order_serial DESC" + " LIMIT $1"); + } qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - stmt, + "", params, &lookup_orders_cb, &plc); diff --git a/src/backenddb/lookup_otp_devices.c b/src/backenddb/lookup_otp_devices.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_otp_devices.h" #include "helper.h" @@ -106,23 +104,21 @@ TALER_MERCHANTDB_lookup_otp_devices ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_otp_devices", - "SELECT" - " otp_id" - ",otp_description" - " FROM merchant_otp_devices" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " otp_id" + ",otp_description" + " FROM merchant_otp_devices"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_otp_devices", + "", params, &lookup_otp_devices_cb, &tlc); diff --git a/src/backenddb/lookup_pending_deposits.c b/src/backenddb/lookup_pending_deposits.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_pending_deposits.h" #include "helper.h" @@ -82,25 +80,25 @@ lookup_deposits_cb (void *cls, struct TALER_Amount deposit_fee; struct TALER_CoinSpendPublicKeyP coin_pub; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_uint64 ("deposit_serial", + GNUNET_PQ_result_spec_uint64 ("out_deposit_serial", &deposit_serial), - GNUNET_PQ_result_spec_auto_from_type ("h_contract_terms", + GNUNET_PQ_result_spec_auto_from_type ("out_h_contract_terms", &h_contract_terms), - GNUNET_PQ_result_spec_auto_from_type ("merchant_priv", + GNUNET_PQ_result_spec_auto_from_type ("out_merchant_priv", &merchant_priv), - GNUNET_PQ_result_spec_string ("merchant_id", + GNUNET_PQ_result_spec_string ("out_merchant_id", &instance_id), - GNUNET_PQ_result_spec_absolute_time ("wire_transfer_deadline", + GNUNET_PQ_result_spec_absolute_time ("out_wire_transfer_deadline", &wire_deadline), - GNUNET_PQ_result_spec_absolute_time ("retry_time", + GNUNET_PQ_result_spec_absolute_time ("out_retry_time", &retry_time), - GNUNET_PQ_result_spec_auto_from_type ("h_wire", + GNUNET_PQ_result_spec_auto_from_type ("out_h_wire", &h_wire), - TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", + TALER_PQ_result_spec_amount_with_currency ("out_amount_with_fee", &amount_with_fee), - TALER_PQ_result_spec_amount_with_currency ("deposit_fee", + TALER_PQ_result_spec_amount_with_currency ("out_deposit_fee", &deposit_fee), - GNUNET_PQ_result_spec_auto_from_type ("coin_pub", + GNUNET_PQ_result_spec_auto_from_type ("out_coin_pub", &coin_pub), GNUNET_PQ_result_spec_end }; @@ -159,35 +157,17 @@ TALER_MERCHANTDB_lookup_pending_deposits ( PREPARE (pg, "lookup_pending_deposits", "SELECT" - " md.deposit_serial" - ",mct.h_contract_terms" - ",mk.merchant_priv" - ",mi.merchant_id" - ",mdc.wire_transfer_deadline" - ",md.settlement_retry_time AS retry_time" - ",ma.h_wire" - ",md.amount_with_fee" - ",md.deposit_fee" - ",md.coin_pub" - " FROM merchant_deposits md" - " JOIN merchant_deposit_confirmations mdc" - " USING (deposit_confirmation_serial)" - " JOIN merchant_contract_terms mct" - " ON (mct.order_serial=mdc.order_serial)" - " JOIN merchant_accounts ma" - " ON (mdc.account_serial=ma.account_serial)" - " LEFT JOIN merchant_kyc kyc" - " ON (mdc.account_serial=kyc.account_serial)" - " JOIN merchant_instances mi" - " ON (mct.merchant_serial=mi.merchant_serial)" - " JOIN merchant_keys mk" - " ON (mi.merchant_serial=mk.merchant_serial)" - " WHERE (mdc.exchange_url=$1)" - " AND md.settlement_retry_needed" - " AND ($4 OR (md.settlement_retry_time < $2))" - " AND ( (kyc.kyc_ok IS NULL) OR kyc.kyc_ok)" - " ORDER BY md.settlement_retry_time ASC" - " LIMIT $3"); + " out_deposit_serial" + " ,out_h_contract_terms" + " ,out_merchant_priv" + " ,out_merchant_id" + " ,out_wire_transfer_deadline" + " ,out_retry_time" + " ,out_h_wire" + " ,out_amount_with_fee" + " ,out_deposit_fee" + " ,out_coin_pub" + " FROM merchant.lookup_pending_deposits($1, $2, $3, $4)"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "lookup_pending_deposits", params, diff --git a/src/backenddb/lookup_pending_deposits.sql b/src/backenddb/lookup_pending_deposits.sql @@ -0,0 +1,108 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.lookup_pending_deposits(TEXT, INT8, INT8, BOOLEAN); +CREATE FUNCTION merchant.lookup_pending_deposits( + IN p_exchange_url TEXT, + IN p_now INT8, + IN p_limit INT8, + IN p_allow_future BOOLEAN) +RETURNS TABLE( + out_deposit_serial INT8, + out_h_contract_terms BYTEA, + out_merchant_priv BYTEA, + out_merchant_id TEXT, + out_wire_transfer_deadline INT8, + out_retry_time INT8, + out_h_wire BYTEA, + out_amount_with_fee merchant.taler_amount_currency, + out_deposit_fee merchant.taler_amount_currency, + out_coin_pub BYTEA) +LANGUAGE plpgsql +AS $FN$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; + remaining INT8 := p_limit; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + ,merchant_priv + FROM merchant.merchant_instances + LOOP + EXIT WHEN remaining <= 0; + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + FOR inner_rec IN + EXECUTE format( + 'SELECT' + ' md.deposit_serial AS deposit_serial' + ' ,mct.h_contract_terms AS h_contract_terms' + ' ,mdc.wire_transfer_deadline AS wire_transfer_deadline' + ' ,md.settlement_retry_time AS retry_time' + ' ,ma.h_wire AS h_wire' + ' ,md.amount_with_fee AS amount_with_fee' + ' ,md.deposit_fee AS deposit_fee' + ' ,md.coin_pub AS coin_pub' + ' FROM %I.merchant_deposits md' + ' JOIN %I.merchant_deposit_confirmations mdc' + ' USING (deposit_confirmation_serial)' + ' JOIN %I.merchant_contract_terms mct' + ' ON (mct.order_serial=mdc.order_serial)' + ' JOIN %I.merchant_accounts ma' + ' ON (mdc.account_serial=ma.account_serial)' + ' LEFT JOIN %I.merchant_kyc kyc' + ' ON (mdc.account_serial=kyc.account_serial)' + ' WHERE (mdc.exchange_url=$1)' + ' AND md.settlement_retry_needed' + ' AND ($4 OR (md.settlement_retry_time < $2))' + ' AND ( (kyc.kyc_ok IS NULL) OR kyc.kyc_ok)' + ' ORDER BY md.settlement_retry_time ASC' + ' LIMIT $3', + s, s, s, s, s) + USING p_exchange_url + ,p_now + ,remaining + ,p_allow_future + LOOP + out_deposit_serial := inner_rec.deposit_serial; + out_h_contract_terms := inner_rec.h_contract_terms; + out_merchant_priv := rec.merchant_priv; + out_merchant_id := rec.merchant_id; + out_wire_transfer_deadline := inner_rec.wire_transfer_deadline; + out_retry_time := inner_rec.retry_time; + out_h_wire := inner_rec.h_wire; + out_amount_with_fee := inner_rec.amount_with_fee; + out_deposit_fee := inner_rec.deposit_fee; + out_coin_pub := inner_rec.coin_pub; + RETURN NEXT; + remaining := remaining - 1; + EXIT WHEN remaining <= 0; + END LOOP; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; +END +$FN$; +COMMENT ON FUNCTION merchant.lookup_pending_deposits(TEXT, INT8, INT8, BOOLEAN) + IS 'Returns up to p_limit pending-settlement deposit rows for the given exchange' + ' across all instance schemas, with merchant_priv read from' + ' merchant.merchant_instances (NULL if absent).'; diff --git a/src/backenddb/lookup_pending_webhooks.c b/src/backenddb/lookup_pending_webhooks.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_pending_webhooks.h" #include "helper.h" @@ -127,35 +125,31 @@ TALER_MERCHANTDB_lookup_pending_webhooks ( .extract_failed = false, }; struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); - struct GNUNET_PQ_QueryParam params_null[] = { + struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_absolute_time (&now), GNUNET_PQ_query_param_end }; - enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, "lookup_pending_webhooks", "SELECT" - " webhook_pending_serial" - ",next_attempt" - ",retries" - ",url" - ",http_method" - ",header" - ",body" - " FROM merchant_pending_webhooks" + " webhook_pending_serial" + " ,next_attempt" + " ,retries" + " ,url" + " ,http_method" + " ,header" + " ,body" + " FROM merchant.merchant_pending_webhooks" " WHERE next_attempt <= $1" - " ORDER BY next_attempt ASC" - ); - + " ORDER BY next_attempt ASC"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "lookup_pending_webhooks", - params_null, + params, &lookup_pending_webhooks_cb, &pwlc); - if (pwlc.extract_failed) return GNUNET_DB_STATUS_HARD_ERROR; return qs; @@ -163,9 +157,10 @@ TALER_MERCHANTDB_lookup_pending_webhooks ( enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_future_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - TALER_MERCHANTDB_PendingWebhooksCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_future_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + TALER_MERCHANTDB_PendingWebhooksCallback cb, + void *cb_cls) { struct LookupPendingWebhookContext pwlc = { .cb = cb, @@ -175,84 +170,27 @@ TALER_MERCHANTDB_lookup_future_webhook (struct TALER_MERCHANTDB_PostgresContext struct GNUNET_PQ_QueryParam params_null[] = { GNUNET_PQ_query_param_end }; - enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, "lookup_future_webhook", "SELECT" - " webhook_pending_serial" - ",next_attempt" - ",retries" - ",url" - ",http_method" - ",header" - ",body" - " FROM merchant_pending_webhooks" - " ORDER BY next_attempt ASC LIMIT 1" - ); - + " webhook_pending_serial" + " ,next_attempt" + " ,retries" + " ,url" + " ,http_method" + " ,header" + " ,body" + " FROM merchant.merchant_pending_webhooks" + " ORDER BY next_attempt ASC" + " LIMIT 1"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "lookup_future_webhook", params_null, &lookup_pending_webhooks_cb, &pwlc); - - if (pwlc.extract_failed) - return GNUNET_DB_STATUS_HARD_ERROR; - return qs; -} - - -enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_all_webhooks (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t min_row, - uint32_t max_results, - TALER_MERCHANTDB_PendingWebhooksCallback cb, - void *cb_cls) -{ - struct LookupPendingWebhookContext pwlc = { - .cb = cb, - .cb_cls = cb_cls, - .extract_failed = false, - }; - uint64_t max_results64 = max_results; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), - GNUNET_PQ_query_param_uint64 (&min_row), - GNUNET_PQ_query_param_uint64 (&max_results64), - GNUNET_PQ_query_param_end - }; - - enum GNUNET_DB_QueryStatus qs; - - check_connection (pg); - PREPARE (pg, - "lookup_all_webhooks", - " SELECT" - " webhook_pending_serial" - ",next_attempt" - ",retries" - ",url" - ",http_method" - ",header" - ",body" - " FROM merchant_pending_webhooks" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND webhook_pending_serial > $2" - " ORDER BY webhook_pending_serial" - " ASC LIMIT $3"); - - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_all_webhooks", - params, - &lookup_pending_webhooks_cb, - &pwlc); - if (pwlc.extract_failed) return GNUNET_DB_STATUS_HARD_ERROR; return qs; diff --git a/src/backenddb/lookup_product.c b/src/backenddb/lookup_product.c @@ -19,66 +19,63 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_product.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_product (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *product_id, - struct TALER_MERCHANTDB_ProductDetails *pd, - size_t *num_categories, - uint64_t **categories) +TALER_MERCHANTDB_lookup_product ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *product_id, + struct TALER_MERCHANTDB_ProductDetails *pd, + size_t *num_categories, + uint64_t **categories) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (product_id), GNUNET_PQ_query_param_end }; - PREPARE (pg, - "lookup_product", - "SELECT" - " mi.description" - ",mi.description_i18n::TEXT" - ",mi.product_name" - ",mi.unit" - ",mi.price_array" - ",mi.taxes::TEXT" - ",mi.total_stock" - ",mi.total_stock_frac" - ",mi.allow_fractional_quantity" - ",mi.fractional_precision_level" - ",mi.total_sold" - ",mi.total_sold_frac" - ",mi.total_lost" - ",mi.total_lost_frac" - ",mi.image" - ",mi.address::TEXT" - ",mi.next_restock" - ",mi.minimum_age" - ",mi.product_group_serial" - ",mi.money_pot_serial" - ",mi.price_is_net" - ",t.category_array AS categories" - " FROM merchant_inventory mi" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - ",LATERAL (" - " SELECT ARRAY (" - " SELECT mpc.category_serial" - " FROM merchant_product_categories mpc" - " WHERE mpc.product_serial = mi.product_serial" - " ) AS category_array" - " ) t" - " WHERE inst.merchant_id=$1" - " AND mi.product_id=$2" - ); - if (NULL == pd) + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mi.description" + ",mi.description_i18n::TEXT" + ",mi.product_name" + ",mi.unit" + ",mi.price_array" + ",mi.taxes::TEXT" + ",mi.total_stock" + ",mi.total_stock_frac" + ",mi.allow_fractional_quantity" + ",mi.fractional_precision_level" + ",mi.total_sold" + ",mi.total_sold_frac" + ",mi.total_lost" + ",mi.total_lost_frac" + ",mi.image" + ",mi.address::TEXT" + ",mi.next_restock" + ",mi.minimum_age" + ",mi.product_group_serial" + ",mi.money_pot_serial" + ",mi.price_is_net" + ",t.category_array AS categories" + " FROM merchant_inventory mi" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT mpc.category_serial" + " FROM merchant_product_categories mpc" + " WHERE mpc.product_serial = mi.product_serial" + " ) AS category_array" + " ) t" + " WHERE mi.product_id=$1" + ); + if (NULL == pd) // FIXME: is this case needed? For now yes: delete-private-products-PRODUCT_ID uses it! { struct GNUNET_PQ_ResultSpec rs_null[] = { GNUNET_PQ_result_spec_end @@ -86,7 +83,7 @@ TALER_MERCHANTDB_lookup_product (struct TALER_MERCHANTDB_PostgresContext *pg, check_connection (pg); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_product", + "", params, rs_null); } @@ -164,7 +161,7 @@ TALER_MERCHANTDB_lookup_product (struct TALER_MERCHANTDB_PostgresContext *pg, pd->product_group_id = 0; pd->money_pot_id = 0; qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_product", + "", params, rs); pd->product_name = my_name; diff --git a/src/backenddb/lookup_product_image.c b/src/backenddb/lookup_product_image.c @@ -18,8 +18,6 @@ * @brief Implementation of the lookup_product_image_by_hash function for Postgres * @author Bohdan Potuzhnyi */ -#include <taler/taler_error_codes.h> -#include <taler/taler_pq_lib.h> #include "merchantdb_lib.h" #include "merchant-database/lookup_product_image.h" #include "helper.h" @@ -33,35 +31,29 @@ TALER_MERCHANTDB_lookup_product_image_by_hash ( char **image) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (image_hash), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("image", + image), + GNUNET_PQ_result_spec_end + }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE ( + TMH_PQ_prepare_anon ( pg, - "lookup_product_image_by_hash", "SELECT" " mi.image" " FROM merchant_inventory mi" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - " WHERE inst.merchant_id=$1" - " AND mi.image_hash=$2"); - + " WHERE mi.image_hash=$1"); *image = NULL; - { - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("image", - image), - GNUNET_PQ_result_spec_end - }; - - return GNUNET_PQ_eval_prepared_singleton_select ( - pg->conn, - "lookup_product_image_by_hash", - params, - rs); - } + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "", + params, + rs); } diff --git a/src/backenddb/lookup_products.c b/src/backenddb/lookup_products.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_products.h" #include "helper.h" @@ -112,7 +110,6 @@ TALER_MERCHANTDB_lookup_products ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&offset), NULL == category_filter ? GNUNET_PQ_query_param_null () @@ -129,64 +126,63 @@ TALER_MERCHANTDB_lookup_products ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_products_asc", - "SELECT" - " product_id" - " ,product_serial" - " FROM merchant_inventory" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND product_serial > $2" - " AND ( ($3::TEXT IS NULL) OR" - " (product_serial IN" - " (SELECT product_serial" - " FROM merchant_product_categories" - " WHERE category_serial IN" - " (SELECT category_serial" - " FROM merchant_categories" - " WHERE category_name LIKE LOWER($3)))) )" - " AND ( (0 = $7::INT8) OR" - " (product_group_serial = $7) )" - " AND ( ($4::TEXT IS NULL) OR" - " (product_name LIKE LOWER($4)) )" - " AND ( ($5::TEXT IS NULL) OR" - " (description LIKE LOWER($5)) )" - " ORDER BY product_serial ASC" - " LIMIT $6"); - PREPARE (pg, - "lookup_products_desc", - "SELECT" - " product_id" - " ,product_serial" - " FROM merchant_inventory" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND product_serial < $2" - " AND ( ($3::TEXT IS NULL) OR" - " (product_serial IN" - " (SELECT product_serial" - " FROM merchant_product_categories" - " WHERE category_serial IN" - " (SELECT category_serial" - " FROM merchant_categories" - " WHERE category_name LIKE LOWER($3)))) )" - " AND ( (0 = $7::INT8) OR" - " (product_group_serial = $7) )" - " AND ( ($4::TEXT IS NULL) OR" - " (product_name LIKE LOWER($4)) )" - " AND ( ($5::TEXT IS NULL) OR" - " (description LIKE LOWER($5)) )" - " ORDER BY product_serial DESC" - " LIMIT $6"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " product_id" + " ,product_serial" + " FROM merchant_inventory" + " WHERE product_serial > $1" + " AND ( ($2::TEXT IS NULL) OR" + " (product_serial IN" + " (SELECT product_serial" + " FROM merchant_product_categories" + " WHERE category_serial IN" + " (SELECT category_serial" + " FROM merchant_categories" + " WHERE LOWER(category_name) LIKE LOWER($2)))) )" + " AND ( (0 = $6::INT8) OR" + " (product_group_serial = $6) )" + " AND ( ($3::TEXT IS NULL) OR" + " (product_name LIKE LOWER($3)) )" + " AND ( ($4::TEXT IS NULL) OR" + " (description LIKE LOWER($4)) )" + " ORDER BY product_serial ASC" + " LIMIT $5"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " product_id" + " ,product_serial" + " FROM merchant_inventory" + " WHERE product_serial < $1" + " AND ( ($2::TEXT IS NULL) OR" + " (product_serial IN" + " (SELECT product_serial" + " FROM merchant_product_categories" + " WHERE category_serial IN" + " (SELECT category_serial" + " FROM merchant_categories" + " WHERE LOWER(category_name) LIKE LOWER($2)))) )" + " AND ( (0 = $6::INT8) OR" + " (product_group_serial = $6) )" + " AND ( ($3::TEXT IS NULL) OR" + " (LOWER(product_name) LIKE LOWER($3)) )" + " AND ( ($4::TEXT IS NULL) OR" + " (description LIKE LOWER($4)) )" + " ORDER BY product_serial DESC" + " LIMIT $5"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "lookup_products_asc" - : "lookup_products_desc", + "", params, &lookup_products_cb, &plc); diff --git a/src/backenddb/lookup_reconciliation_details.c b/src/backenddb/lookup_reconciliation_details.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_reconciliation_details.h" #include "helper.h" @@ -139,36 +137,34 @@ TALER_MERCHANTDB_lookup_reconciliation_details ( .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&expected_incoming_serial), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_reconciliation_details", - "SELECT" - " mct.order_id" - ",SUM((etc.exchange_deposit_value).val)::INT8 AS deposit_value_sum" - ",SUM((etc.exchange_deposit_value).frac)::INT8 AS deposit_frac_sum" - ",SUM((etc.exchange_deposit_fee).val)::INT8 AS fee_value_sum" - ",SUM((etc.exchange_deposit_fee).frac)::INT8 AS fee_frac_sum" - ",(etc.exchange_deposit_value).curr AS currency" - " FROM merchant_expected_transfer_to_coin etc" - " JOIN merchant_deposits md" - " USING (deposit_serial)" - " JOIN merchant_deposit_confirmations mdc" - " USING (deposit_confirmation_serial)" - " JOIN merchant_contract_terms mct" - " USING (order_serial)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE expected_credit_serial=$2" - " AND mi.merchant_id=$1" - " GROUP BY mct.order_id, (etc.exchange_deposit_value).curr;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mct.order_id" + ",SUM((etc.exchange_deposit_value).val)::INT8 AS deposit_value_sum" + ",SUM((etc.exchange_deposit_value).frac)::INT8 AS deposit_frac_sum" + ",SUM((etc.exchange_deposit_fee).val)::INT8 AS fee_value_sum" + ",SUM((etc.exchange_deposit_fee).frac)::INT8 AS fee_frac_sum" + ",(etc.exchange_deposit_value).curr AS currency" + " FROM merchant_expected_transfer_to_coin etc" + " JOIN merchant_deposits md" + " USING (deposit_serial)" + " JOIN merchant_deposit_confirmations mdc" + " USING (deposit_confirmation_serial)" + " JOIN merchant_contract_terms mct" + " USING (order_serial)" + " WHERE expected_credit_serial=$1" + " GROUP BY mct.order_id, (etc.exchange_deposit_value).curr;"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_reconciliation_details", + "", params, &reconciliation_cb, &lic); diff --git a/src/backenddb/lookup_refund_proof.c b/src/backenddb/lookup_refund_proof.c @@ -19,17 +19,17 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_refund_proof.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_refund_proof (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t refund_serial, - struct TALER_ExchangeSignatureP *exchange_sig, - struct TALER_ExchangePublicKeyP *exchange_pub) +TALER_MERCHANTDB_lookup_refund_proof ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t refund_serial, + struct TALER_ExchangeSignatureP *exchange_sig, + struct TALER_ExchangePublicKeyP *exchange_pub) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&refund_serial), @@ -43,20 +43,19 @@ TALER_MERCHANTDB_lookup_refund_proof (struct TALER_MERCHANTDB_PostgresContext *p GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "lookup_refund_proof", - "SELECT" - " merchant_exchange_signing_keys.exchange_pub" - ",exchange_sig" - " FROM merchant_refund_proofs" - " JOIN merchant_exchange_signing_keys" - " USING (signkey_serial)" - " WHERE" - " refund_serial=$1"); - + TMH_PQ_prepare_anon (pg, + "SELECT" + " merchant.merchant_exchange_signing_keys.exchange_pub" + ",exchange_sig" + " FROM merchant_refund_proofs" + " JOIN merchant.merchant_exchange_signing_keys" + " USING (signkey_serial)" + " WHERE" + " refund_serial=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_refund_proof", + "", params, rs); } diff --git a/src/backenddb/lookup_refunds.c b/src/backenddb/lookup_refunds.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_refunds.h" #include "helper.h" @@ -98,14 +96,14 @@ lookup_refunds_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_refunds (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - TALER_MERCHANTDB_RefundCallback rc, - void *rc_cls) +TALER_MERCHANTDB_lookup_refunds ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + TALER_MERCHANTDB_RefundCallback rc, + void *rc_cls) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_end }; @@ -116,27 +114,25 @@ TALER_MERCHANTDB_lookup_refunds (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); /* no preflight check here, run in transaction by caller! */ TALER_LOG_DEBUG ("Looking for refund of h_contract_terms %s at `%s'\n", GNUNET_h2s (&h_contract_terms->hash), instance_id); check_connection (pg); - PREPARE (pg, - "lookup_refunds", - "SELECT" - " coin_pub" - ",refund_amount" - " FROM merchant_refunds" - " WHERE order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " coin_pub" + ",refund_amount" + " FROM merchant_refunds" + " WHERE order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_refunds", + "", params, &lookup_refunds_cb, &lrc); diff --git a/src/backenddb/lookup_refunds_detailed.c b/src/backenddb/lookup_refunds_detailed.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_refunds_detailed.h" #include "helper.h" @@ -131,7 +129,6 @@ TALER_MERCHANTDB_lookup_refunds_detailed ( void *rc_cls) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_end }; @@ -142,41 +139,39 @@ TALER_MERCHANTDB_lookup_refunds_detailed ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); /* no preflight check here, run in transaction by caller! */ TALER_LOG_DEBUG ("Looking for refund %s + %s\n", GNUNET_h2s (&h_contract_terms->hash), instance_id); check_connection (pg); - PREPARE (pg, - "lookup_refunds_detailed", - "SELECT" - " ref.refund_serial" - ",ref.refund_timestamp" - ",dep.coin_pub" - ",mcon.exchange_url" - ",ref.rtransaction_id" - ",ref.reason" - ",ref.refund_amount" - ",merchant_refund_proofs.exchange_sig IS NULL AS pending" - " FROM merchant_deposit_confirmations mcon" - " JOIN merchant_deposits dep" - " USING (deposit_confirmation_serial)" - " JOIN merchant_refunds ref" - " USING (order_serial, coin_pub)" - " LEFT JOIN merchant_refund_proofs" - " USING (refund_serial)" - " WHERE mcon.order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " ref.refund_serial" + ",ref.refund_timestamp" + ",dep.coin_pub" + ",mcon.exchange_url" + ",ref.rtransaction_id" + ",ref.reason" + ",ref.refund_amount" + ",merchant_refund_proofs.exchange_sig IS NULL AS pending" + " FROM merchant_deposit_confirmations mcon" + " JOIN merchant_deposits dep" + " USING (deposit_confirmation_serial)" + " JOIN merchant_refunds ref" + " USING (order_serial, coin_pub)" + " LEFT JOIN merchant_refund_proofs" + " USING (refund_serial)" + " WHERE mcon.order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_refunds_detailed", + "", params, &lookup_refunds_detailed_cb, &lrdc); diff --git a/src/backenddb/lookup_reports_pending.c b/src/backenddb/lookup_reports_pending.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_reports_pending.h" #include "helper.h" @@ -77,27 +75,27 @@ select_pending_reports_cb (void *cls, struct GNUNET_TIME_Absolute next_transmission; bool one_shot; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("merchant_id", + GNUNET_PQ_result_spec_string ("out_merchant_id", &instance_id), - GNUNET_PQ_result_spec_uint64 ("report_serial", + GNUNET_PQ_result_spec_uint64 ("out_report_serial", &report_serial), - GNUNET_PQ_result_spec_string ("report_program_section", + GNUNET_PQ_result_spec_string ("out_report_program_section", &report_program_section), - GNUNET_PQ_result_spec_string ("report_description", + GNUNET_PQ_result_spec_string ("out_report_description", &report_description), - GNUNET_PQ_result_spec_string ("mime_type", + GNUNET_PQ_result_spec_string ("out_mime_type", &mime_type), - GNUNET_PQ_result_spec_auto_from_type ("report_token", + GNUNET_PQ_result_spec_auto_from_type ("out_report_token", &report_token), - GNUNET_PQ_result_spec_string ("target_address", + GNUNET_PQ_result_spec_string ("out_target_address", &target_address), - GNUNET_PQ_result_spec_relative_time ("frequency", + GNUNET_PQ_result_spec_relative_time ("out_frequency", &frequency), - GNUNET_PQ_result_spec_relative_time ("frequency_shift", + GNUNET_PQ_result_spec_relative_time ("out_frequency_shift", &frequency_shift), - GNUNET_PQ_result_spec_absolute_time ("next_transmission", + GNUNET_PQ_result_spec_absolute_time ("out_next_transmission", &next_transmission), - GNUNET_PQ_result_spec_bool ("one_shot_hidden", + GNUNET_PQ_result_spec_bool ("out_one_shot_hidden", &one_shot), GNUNET_PQ_result_spec_end }; @@ -149,22 +147,18 @@ TALER_MERCHANTDB_lookup_reports_pending ( PREPARE (pg, "lookup_reports_pending", "SELECT" - " mi.merchant_id" - " ,mr.report_serial" - " ,mr.report_program_section" - " ,mr.report_description" - " ,mr.mime_type" - " ,mr.report_token" - " ,mr.target_address" - " ,mr.frequency" - " ,mr.frequency_shift" - " ,mr.next_transmission" - " ,mr.one_shot_hidden" - " FROM merchant_reports mr" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " ORDER BY next_transmission ASC" - " LIMIT 1;"); + " out_merchant_id" + " ,out_report_serial" + " ,out_report_program_section" + " ,out_report_description" + " ,out_mime_type" + " ,out_report_token" + " ,out_target_address" + " ,out_frequency" + " ,out_frequency_shift" + " ,out_next_transmission" + " ,out_one_shot_hidden" + " FROM merchant.lookup_reports_pending()"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, "lookup_reports_pending", diff --git a/src/backenddb/lookup_reports_pending.sql b/src/backenddb/lookup_reports_pending.sql @@ -0,0 +1,93 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.lookup_reports_pending(); +CREATE FUNCTION merchant.lookup_reports_pending() +RETURNS TABLE( + out_merchant_id TEXT, + out_report_serial INT8, + out_report_program_section TEXT, + out_report_description TEXT, + out_mime_type TEXT, + out_report_token BYTEA, + out_target_address TEXT, + out_frequency INT8, + out_frequency_shift INT8, + out_next_transmission INT8, + out_one_shot_hidden BOOLEAN) +LANGUAGE plpgsql +AS $FN$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; + xfound BOOLEAN := FALSE; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + EXECUTE format( + 'SELECT' + ' report_serial AS rs' + ' ,report_program_section AS rps' + ' ,report_description AS rd' + ' ,mime_type AS mt' + ' ,report_token AS rt' + ' ,target_address AS ta' + ' ,frequency AS f' + ' ,frequency_shift AS fs' + ' ,next_transmission AS nt' + ' ,one_shot_hidden AS osh' + ' FROM %I.merchant_reports' + ' ORDER BY next_transmission ASC LIMIT 1', s) + INTO inner_rec; + IF inner_rec IS NULL + THEN + CONTINUE; + END IF; + IF (NOT found) OR (inner_rec.nt < out_next_transmission) + THEN + out_merchant_id := rec.merchant_id; + out_report_serial := inner_rec.rs; + out_report_program_section := inner_rec.rps; + out_report_description := inner_rec.rd; + out_mime_type := inner_rec.mt; + out_report_token := inner_rec.rt; + out_target_address := inner_rec.ta; + out_frequency := inner_rec.f; + out_frequency_shift := inner_rec.fs; + out_next_transmission := inner_rec.nt; + out_one_shot_hidden := inner_rec.osh; + xfound := TRUE; + END IF; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; + IF xfound THEN + RETURN NEXT; + END IF; +END +$FN$; +COMMENT ON FUNCTION merchant.lookup_reports_pending() + IS 'Returns the single next-due report (smallest next_transmission)' + ' across all instance schemas, or no row if no reports exist.'; diff --git a/src/backenddb/lookup_spent_tokens_by_order.c b/src/backenddb/lookup_spent_tokens_by_order.c @@ -19,8 +19,6 @@ * @author Christian Blättler */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_spent_tokens_by_order.h" #include "helper.h" @@ -133,25 +131,24 @@ TALER_MERCHANTDB_lookup_spent_tokens_by_order ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "lookup_spent_tokens_by_order", - "SELECT" - " spent_token_serial" - ",h_contract_terms" - ",h_pub" - ",token_pub" - ",token_sig" - ",blind_sig" - " FROM merchant_used_tokens" - " JOIN merchant_contract_terms" - " USING (h_contract_terms)" - " JOIN merchant_token_family_keys" - " USING (token_family_key_serial)" - " WHERE order_serial=$1"); - + TMH_PQ_prepare_anon (pg, + "SELECT" + " spent_token_serial" + ",h_contract_terms" + ",h_pub" + ",token_pub" + ",token_sig" + ",blind_sig" + " FROM merchant_used_tokens" + " JOIN merchant_contract_terms" + " USING (h_contract_terms)" + " JOIN merchant_token_family_keys" + " USING (token_family_key_serial)" + " WHERE order_serial=$1"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_spent_tokens_by_order", + "", params, &lookup_spent_tokens_by_order_cb, &ctx); diff --git a/src/backenddb/lookup_statistics_amount_by_bucket.c b/src/backenddb/lookup_statistics_amount_by_bucket.c @@ -19,8 +19,6 @@ * @author Martin Schanzenbach */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_statistics_amount_by_bucket.h" #include "helper.h" @@ -186,33 +184,31 @@ TALER_MERCHANTDB_lookup_statistics_amount_by_bucket ( .pg = pg, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (slug), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_statistics_amount_by_bucket", - "SELECT" - " bmeta_serial_id" - ",description" - ",bucket_start" - ",bucket_range::TEXT" - ",merchant_statistics_bucket_end(bucket_start, bucket_range) AS bucket_end" - ",(cumulative_value,cumulative_frac,curr)::taler_amount_currency AS cumulative_amount" - " FROM merchant_statistic_bucket_amount" - " JOIN merchant_statistic_bucket_meta" - " USING (bmeta_serial_id)" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_statistic_bucket_meta.slug=$2" - " AND merchant_statistic_bucket_meta.stype='amount'"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " bmeta_serial_id" + ",description" + ",bucket_start" + ",bucket_range::TEXT" + ",merchant_statistics_bucket_end(bucket_start, bucket_range) AS bucket_end" + ",(cumulative_value,cumulative_frac,curr)::merchant.taler_amount_currency AS cumulative_amount" + " FROM merchant_statistic_bucket_amount" + " JOIN merchant_statistic_bucket_meta" + " USING (bmeta_serial_id)" + " WHERE merchant_statistic_bucket_meta.slug=$1" + " AND merchant_statistic_bucket_meta.stype='amount'"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_amount_by_bucket", + "", params, &lookup_statistics_amount_by_bucket_cb, &context); diff --git a/src/backenddb/lookup_statistics_amount_by_bucket2.c b/src/backenddb/lookup_statistics_amount_by_bucket2.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_statistics_amount_by_bucket2.h" #include "helper.h" @@ -124,7 +122,6 @@ TALER_MERCHANTDB_lookup_statistics_amount_by_bucket2 ( .pg = pg, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (slug), GNUNET_PQ_query_param_string (granularity), GNUNET_PQ_query_param_uint64 (&counter), @@ -132,29 +129,28 @@ TALER_MERCHANTDB_lookup_statistics_amount_by_bucket2 ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_statistics_amount_by_bucket2", - "SELECT" - " msba.bucket_start" - ",ARRAY_AGG (" - " ROW(msba.cumulative_value::INT8, msba.cumulative_frac::INT4, msba.curr)::taler_amount_currency" - " ) AS cumulative_amounts" - " FROM merchant_statistic_bucket_meta msbm" - " JOIN merchant_statistic_bucket_amount msba" - " USING (bmeta_serial_id)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " AND msbm.slug=$2" - " AND msba.bucket_range=$3::TEXT::statistic_range" - " AND msbm.stype='amount'" - " GROUP BY msba.bucket_start" - " ORDER BY msba.bucket_start DESC" - " LIMIT $4;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " msba.bucket_start" + ",ARRAY_AGG (" + " ROW(msba.cumulative_value::INT8, msba.cumulative_frac::INT4, msba.curr)::merchant.taler_amount_currency" + " ) AS cumulative_amounts" + " FROM merchant_statistic_bucket_meta msbm" + " JOIN merchant_statistic_bucket_amount msba" + " USING (bmeta_serial_id)" + " WHERE msbm.slug=$1" + " AND msba.bucket_range=$2::TEXT::merchant.statistic_range" + " AND msbm.stype='amount'" + " GROUP BY msba.bucket_start" + " ORDER BY msba.bucket_start DESC" + " LIMIT $3;"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_amount_by_bucket2", + "", params, &lookup_statistics_amount_by_bucket_cb2, &context); diff --git a/src/backenddb/lookup_statistics_amount_by_interval.c b/src/backenddb/lookup_statistics_amount_by_interval.c @@ -19,8 +19,6 @@ * @author Martin Schanzenbach */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_statistics_amount_by_interval.h" #include "helper.h" @@ -198,21 +196,22 @@ TALER_MERCHANTDB_lookup_statistics_amount_by_interval ( GNUNET_PQ_query_param_end }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (slug), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_statistics_amount_by_interval_description", - "SELECT description" - " FROM merchant_statistic_interval_meta" - " WHERE slug=$1 LIMIT 1"); + TMH_PQ_prepare_anon (pg, + "SELECT description" + " FROM merchant_statistic_interval_meta" + " WHERE slug=$1 LIMIT 1"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_amount_by_interval_description", + "", descParams, &lookup_statistics_amount_by_interval_desc_cb, &context); @@ -222,13 +221,12 @@ TALER_MERCHANTDB_lookup_statistics_amount_by_interval ( GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } - PREPARE (pg, - "lookup_statistics_amount_by_interval", - "SELECT *" - " FROM merchant_statistic_interval_amount_get($2,$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT *" + " FROM merchant_statistic_interval_amount_get($1)"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_amount_by_interval", + "", params, &lookup_statistics_amount_by_interval_cb, &context); diff --git a/src/backenddb/lookup_statistics_amount_by_interval.sql b/src/backenddb/lookup_statistics_amount_by_interval.sql @@ -0,0 +1,202 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_statistic_interval_amount_get; +CREATE FUNCTION merchant_statistic_interval_amount_get ( + IN in_slug TEXT +) +RETURNS SETOF merchant.merchant_statistic_interval_amount_get_return_value +LANGUAGE plpgsql +AS $$ +DECLARE + my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + my_ranges INT8[]; + my_range INT8; + my_delta_value INT8; + my_delta_frac INT8; + my_meta INT8; + my_next_max_serial INT8; + my_currency TEXT; + my_rec RECORD; + my_irec RECORD; + my_jrec RECORD; + my_i INT; + my_min_serial INT8 DEFAULT NULL; + my_rval merchant.merchant_statistic_interval_amount_get_return_value; +BEGIN + + SELECT imeta_serial_id + ,ranges + ,precisions + INTO my_rec + FROM merchant_statistic_interval_meta + WHERE slug=in_slug; + IF NOT FOUND + THEN + RETURN; + END IF; + + my_meta = my_rec.imeta_serial_id; + my_ranges = my_rec.ranges; + + FOR my_currency IN + SELECT DISTINCT delta_curr + FROM merchant_statistic_amount_event + WHERE imeta_serial_id = my_meta + LOOP + + my_rval.rvalue.val = 0; + my_rval.rvalue.frac = 0; + my_rval.rvalue.curr = my_currency; + + FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + my_range = my_ranges[my_i]; + SELECT event_delimiter + ,cumulative_value + ,cumulative_frac + INTO my_irec + FROM merchant_statistic_interval_amount + WHERE imeta_serial_id = my_meta + AND curr = my_currency + AND range = my_range; + + IF FOUND + THEN + my_min_serial = my_irec.event_delimiter; + my_rval.rvalue.val = (my_rval.rvalue).val + my_irec.cumulative_value + my_irec.cumulative_frac / 100000000; + my_rval.rvalue.frac = (my_rval.rvalue).frac + my_irec.cumulative_frac % 100000000; + IF (my_rval.rvalue).frac > 100000000 + THEN + my_rval.rvalue.frac = (my_rval.rvalue).frac - 100000000; + my_rval.rvalue.val = (my_rval.rvalue).val + 1; + END IF; + + -- Check if we have events that left the applicable range + SELECT SUM(delta_value) AS value_sum + ,SUM(delta_frac) AS frac_sum + INTO my_jrec + FROM merchant_statistic_amount_event + WHERE imeta_serial_id = my_meta + AND delta_curr = my_currency + AND slot < my_time - my_range + AND aevent_serial_id >= my_min_serial; + + IF FOUND AND my_jrec.value_sum IS NOT NULL + THEN + -- Normalize sum + my_delta_value = my_jrec.value_sum + my_jrec.frac_sum / 100000000; + my_delta_frac = my_jrec.frac_sum % 100000000; + my_rval.rvalue.val = (my_rval.rvalue).val - my_delta_value; + IF ((my_rval.rvalue).frac >= my_delta_frac) + THEN + my_rval.rvalue.frac = (my_rval.rvalue).frac - my_delta_frac; + ELSE + my_rval.rvalue.frac = 100000000 + (my_rval.rvalue).frac - my_delta_frac; + my_rval.rvalue.val = (my_rval.rvalue).val - 1; + END IF; + + -- First find out the next event delimiter value + SELECT aevent_serial_id + INTO my_next_max_serial + FROM merchant_statistic_amount_event + WHERE imeta_serial_id = my_meta + AND delta_curr = my_currency + AND slot >= my_time - my_range + AND aevent_serial_id >= my_min_serial + ORDER BY slot ASC + LIMIT 1; + IF FOUND + THEN + -- remove expired events from the sum of the current slot + UPDATE merchant_statistic_interval_amount SET + cumulative_value = cumulative_value - my_delta_value + - CASE + WHEN cumulative_frac < my_delta_frac + THEN 1 + ELSE 0 + END, + cumulative_frac = cumulative_frac - my_delta_frac + + CASE + WHEN cumulative_frac < my_delta_frac + THEN 100000000 + ELSE 0 + END, + event_delimiter = my_next_max_serial + WHERE imeta_serial_id = my_meta + AND curr = my_currency + AND range = my_range; + ELSE + -- actually, slot is now empty, remove it entirely + DELETE FROM merchant_statistic_interval_amount + WHERE imeta_serial_id = my_meta + AND curr = my_currency + AND range = my_range; + END IF; + IF (my_i < COALESCE(array_length(my_ranges,1),0)) + THEN + -- carry over all events into the next (larger) slot + UPDATE merchant_statistic_interval_amount AS msia SET + cumulative_value = cumulative_value + my_delta_value + + CASE + WHEN cumulative_frac + my_delta_frac > 100000000 + THEN 1 + ELSE 0 + END, + cumulative_frac = cumulative_frac + my_delta_frac + - CASE + WHEN cumulative_frac + my_delta_frac > 100000000 + THEN 100000000 + ELSE 0 + END, + event_delimiter = LEAST (msia.event_delimiter,my_min_serial) + WHERE imeta_serial_id = my_meta + AND range=my_ranges[my_i+1]; + IF NOT FOUND + THEN + INSERT INTO merchant_statistic_interval_amount + (imeta_serial_id + ,event_delimiter + ,range + ,curr + ,cumulative_value + ,cumulative_frac + ) VALUES ( + my_meta + ,my_min_serial + ,my_ranges[my_i+1] + ,my_currency + ,my_delta_value + ,my_delta_frac); + END IF; + ELSE + -- events are obsolete, delete them + DELETE FROM merchant_statistic_amount_event + WHERE imeta_serial_id = my_meta + AND slot < my_time - my_range; + END IF; + END IF; + + my_rval.range = my_range; + RETURN NEXT my_rval; + END IF; + END LOOP; -- over my_ranges + END LOOP; -- over my_currency +END $$; + +COMMENT ON FUNCTION merchant_statistic_interval_amount_get + IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value; multiple values are returned, one per currency and range'; diff --git a/src/backenddb/lookup_statistics_counter_by_bucket.c b/src/backenddb/lookup_statistics_counter_by_bucket.c @@ -19,8 +19,6 @@ * @author Martin Schanzenbach */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_statistics_counter_by_bucket.h" #include "helper.h" @@ -125,32 +123,30 @@ TALER_MERCHANTDB_lookup_statistics_counter_by_bucket ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (slug), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_statistics_counter_by_bucket", - "SELECT" - " description" - ",bucket_start" - ",bucket_range::TEXT" - ",merchant_statistics_bucket_end(bucket_start, bucket_range) AS bucket_end" - ",cumulative_number" - " FROM merchant_statistic_bucket_counter" - " JOIN merchant_statistic_bucket_meta" - " USING (bmeta_serial_id)" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_statistic_bucket_meta.slug=$2" - " AND merchant_statistic_bucket_meta.stype = 'number'"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " description" + ",bucket_start" + ",bucket_range::TEXT" + ",merchant_statistics_bucket_end(bucket_start, bucket_range) AS bucket_end" + ",cumulative_number" + " FROM merchant_statistic_bucket_counter" + " JOIN merchant_statistic_bucket_meta" + " USING (bmeta_serial_id)" + " WHERE merchant_statistic_bucket_meta.slug=$1" + " AND merchant_statistic_bucket_meta.stype = 'number'"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_counter_by_bucket", + "", params, &lookup_statistics_counter_by_bucket_cb, &context); diff --git a/src/backenddb/lookup_statistics_counter_by_bucket2.c b/src/backenddb/lookup_statistics_counter_by_bucket2.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_statistics_counter_by_bucket2.h" #include "helper.h" @@ -147,7 +145,6 @@ TALER_MERCHANTDB_lookup_statistics_counter_by_bucket2 ( .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (prefix), GNUNET_PQ_query_param_string (granularity), GNUNET_PQ_query_param_uint64 (&counter), @@ -155,28 +152,27 @@ TALER_MERCHANTDB_lookup_statistics_counter_by_bucket2 ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_statistics_counter_by_bucket2", - "SELECT" - " msbc.bucket_start" - ",ARRAY_AGG(msbm.slug) AS slugs" - ",ARRAY_AGG(msbc.cumulative_number) AS counters" - " FROM merchant_statistic_bucket_counter msbc" - " JOIN merchant_statistic_bucket_meta msbm" - " USING (bmeta_serial_id)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " AND msbm.slug LIKE $2" - " AND msbc.bucket_range=$3::TEXT::statistic_range" - " AND msbm.stype = 'number'" - " GROUP BY msbc.bucket_start" - " ORDER BY msbc.bucket_start DESC" - " LIMIT $4"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " msbc.bucket_start" + ",ARRAY_AGG(msbm.slug) AS slugs" + ",ARRAY_AGG(msbc.cumulative_number) AS counters" + " FROM merchant_statistic_bucket_counter msbc" + " JOIN merchant_statistic_bucket_meta msbm" + " USING (bmeta_serial_id)" + " WHERE msbm.slug LIKE $1" + " AND msbc.bucket_range=$2::TEXT::merchant.statistic_range" + " AND msbm.stype = 'number'" + " GROUP BY msbc.bucket_start" + " ORDER BY msbc.bucket_start DESC" + " LIMIT $3"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_counter_by_bucket2", + "", params, &lookup_statistics_counter_by_bucket_cb2, &context); diff --git a/src/backenddb/lookup_statistics_counter_by_interval.c b/src/backenddb/lookup_statistics_counter_by_interval.c @@ -19,8 +19,6 @@ * @author Martin Schanzenbach */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_statistics_counter_by_interval.h" #include "helper.h" @@ -164,21 +162,23 @@ TALER_MERCHANTDB_lookup_statistics_counter_by_interval ( GNUNET_PQ_query_param_end }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (slug), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_statistics_counter_by_interval_description", - "SELECT description" - " FROM merchant_statistic_interval_meta" - " WHERE slug=$1 LIMIT 1"); + TMH_PQ_prepare_anon (pg, + "SELECT description" + " FROM merchant_statistic_interval_meta" + " WHERE slug=$1" + " LIMIT 1"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_counter_by_interval_description", + "", descParams, &lookup_statistics_counter_by_interval_desc_cb, &context); @@ -188,13 +188,14 @@ TALER_MERCHANTDB_lookup_statistics_counter_by_interval ( GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } - PREPARE (pg, - "lookup_statistics_counter_by_interval", - "SELECT *" - " FROM merchant_statistic_interval_number_get($2,$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " range" + " ,rvalue" + " FROM merchant_statistic_interval_number_get($1)"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_statistics_counter_by_interval", + "", params, &lookup_statistics_counter_by_interval_cb, &context); diff --git a/src/backenddb/lookup_statistics_counter_by_interval.sql b/src/backenddb/lookup_statistics_counter_by_interval.sql @@ -0,0 +1,140 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_statistic_interval_number_get; +CREATE FUNCTION merchant_statistic_interval_number_get ( + IN in_slug TEXT +) +RETURNS SETOF merchant.merchant_statistic_interval_number_get_return_value +LANGUAGE plpgsql +AS $$ +DECLARE + my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + my_ranges INT8[]; + my_range INT8; + my_delta INT8; + my_meta INT8; + my_next_max_serial INT8; + my_rec RECORD; + my_irec RECORD; + my_i INT; + my_min_serial INT8 DEFAULT NULL; + my_rval merchant.merchant_statistic_interval_number_get_return_value; +BEGIN + SELECT imeta_serial_id + ,ranges + ,precisions + INTO my_rec + FROM merchant_statistic_interval_meta + WHERE slug=in_slug; + IF NOT FOUND + THEN + RETURN; + END IF; + my_rval.rvalue = 0; + my_ranges = my_rec.ranges; + my_meta = my_rec.imeta_serial_id; + + FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + my_range = my_ranges[my_i]; + SELECT event_delimiter + ,cumulative_number + INTO my_irec + FROM merchant_statistic_interval_counter + WHERE imeta_serial_id = my_meta + AND range = my_range; + IF FOUND + THEN + my_min_serial = my_irec.event_delimiter; + my_rval.rvalue = my_rval.rvalue + my_irec.cumulative_number; + + -- Check if we have events that left the applicable range + SELECT SUM(delta) AS delta_sum + INTO my_irec + FROM merchant_statistic_counter_event + WHERE imeta_serial_id = my_meta + AND slot < my_time - my_range + AND nevent_serial_id >= my_min_serial; + + IF FOUND AND my_irec.delta_sum IS NOT NULL + THEN + my_delta = my_irec.delta_sum; + my_rval.rvalue = my_rval.rvalue - my_delta; + + -- First find out the next event delimiter value + SELECT nevent_serial_id + INTO my_next_max_serial + FROM merchant_statistic_counter_event + WHERE imeta_serial_id = my_meta + AND slot >= my_time - my_range + AND nevent_serial_id >= my_min_serial + ORDER BY slot ASC + LIMIT 1; + + IF FOUND + THEN + -- remove expired events from the sum of the current slot + + UPDATE merchant_statistic_interval_counter + SET cumulative_number = cumulative_number - my_delta, + event_delimiter = my_next_max_serial + WHERE imeta_serial_id = my_meta + AND range = my_range; + ELSE + -- actually, slot is now empty, remove it entirely + DELETE FROM merchant_statistic_interval_counter + WHERE imeta_serial_id = my_meta + AND range = my_range; + END IF; + IF (my_i < COALESCE(array_length(my_ranges,1),0)) + THEN + -- carry over all events into the next slot + UPDATE merchant_statistic_interval_counter AS usic SET + cumulative_number = cumulative_number + my_delta, + event_delimiter = LEAST(usic.event_delimiter,my_min_serial) + WHERE imeta_serial_id = my_meta + AND range=my_ranges[my_i+1]; + IF NOT FOUND + THEN + INSERT INTO merchant_statistic_interval_counter + (imeta_serial_id + ,range + ,event_delimiter + ,cumulative_number + ) VALUES ( + my_meta + ,my_ranges[my_i+1] + ,my_min_serial + ,my_delta); + END IF; + ELSE + -- events are obsolete, delete them + DELETE FROM merchant_statistic_counter_event + WHERE imeta_serial_id = my_meta + AND slot < my_time - my_range; + END IF; + END IF; + + my_rval.range = my_range; + RETURN NEXT my_rval; + END IF; + END LOOP; +END $$; + +COMMENT ON FUNCTION merchant_statistic_interval_number_get + IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value for each range'; diff --git a/src/backenddb/lookup_template.c b/src/backenddb/lookup_template.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_template.h" #include "helper.h" @@ -37,72 +35,54 @@ * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_template (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *template_id, - struct TALER_MERCHANTDB_TemplateDetails *td) +TALER_MERCHANTDB_lookup_template ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *template_id, + struct TALER_MERCHANTDB_TemplateDetails *td) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (template_id), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("template_description", + &td->template_description), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("otp_id", + &td->otp_id), + NULL), + TALER_PQ_result_spec_json ("template_contract", + &td->template_contract), + GNUNET_PQ_result_spec_allow_null ( + TALER_PQ_result_spec_json ("editable_defaults", + &td->editable_defaults), + NULL), + GNUNET_PQ_result_spec_end + }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_template", - "SELECT" - " mt.template_description" - ",mod.otp_id" - ",mt.template_contract::TEXT" - ",mt.editable_defaults::TEXT" - " FROM merchant_template mt" - " JOIN merchant_instances mi" - " ON (mi.merchant_serial = mt.merchant_serial)" - " LEFT JOIN merchant_otp_devices mod" - " ON (mod.otp_serial = mt.otp_device_id)" - " WHERE mi.merchant_id=$1" - " AND mt.template_id=$2"); - if (NULL == td) - { - struct GNUNET_PQ_ResultSpec rs_null[] = { - GNUNET_PQ_result_spec_end - }; - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_template", - params, - rs_null); - GNUNET_PQ_cleanup_query_params_closures (params); - return qs; - } - else - { - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("template_description", - &td->template_description), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_string ("otp_id", - &td->otp_id), - NULL), - TALER_PQ_result_spec_json ("template_contract", - &td->template_contract), - GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_json ("editable_defaults", - &td->editable_defaults), - NULL), - GNUNET_PQ_result_spec_end - }; - - memset (td, - 0, - sizeof (*td)); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_template", - params, - rs); - GNUNET_PQ_cleanup_query_params_closures (params); - return qs; - } + TMH_PQ_prepare_anon (pg, + "SELECT" + " mt.template_description" + ",mod.otp_id" + ",mt.template_contract::TEXT" + ",mt.editable_defaults::TEXT" + " FROM merchant_template mt" + " LEFT JOIN merchant_otp_devices mod" + " ON (mod.otp_serial = mt.otp_device_id)" + " WHERE mt.template_id=$1"); + memset (td, + 0, + sizeof (*td)); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; } diff --git a/src/backenddb/lookup_templates.c b/src/backenddb/lookup_templates.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_templates.h" #include "helper.h" @@ -93,10 +91,11 @@ lookup_templates_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_templates (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - TALER_MERCHANTDB_TemplatesCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_templates ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + TALER_MERCHANTDB_TemplatesCallback cb, + void *cb_cls) { struct LookupTemplateContext tlc = { .cb = cb, @@ -105,23 +104,21 @@ TALER_MERCHANTDB_lookup_templates (struct TALER_MERCHANTDB_PostgresContext *pg, .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_templates", - "SELECT" - " template_id" - ",template_description" - " FROM merchant_template" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " template_id" + ",template_description" + " FROM merchant_template"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_templates", + "", params, &lookup_templates_cb, &tlc); diff --git a/src/backenddb/lookup_token_families.c b/src/backenddb/lookup_token_families.c @@ -19,8 +19,6 @@ * @author Christian Blättler */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_token_families.h" #include "helper.h" @@ -130,28 +128,26 @@ TALER_MERCHANTDB_lookup_token_families ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_token_families", - "SELECT" - " slug" - ",name" - ",description" - ",description_i18n::TEXT" - ",valid_after" - ",valid_before" - ",kind" - " FROM merchant_token_families" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " slug" + ",name" + ",description" + ",description_i18n::TEXT" + ",valid_after" + ",valid_before" + ",kind" + " FROM merchant_token_families"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_token_families", + "", params, &lookup_token_families_cb, &context); diff --git a/src/backenddb/lookup_token_family.c b/src/backenddb/lookup_token_family.c @@ -19,117 +19,100 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_token_family.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_token_family (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *token_family_slug, - struct TALER_MERCHANTDB_TokenFamilyDetails *details) +TALER_MERCHANTDB_lookup_token_family ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *token_family_slug, + struct TALER_MERCHANTDB_TokenFamilyDetails *details) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (token_family_slug), GNUNET_PQ_query_param_end }; + char *kind; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("slug", + &details->slug), + GNUNET_PQ_result_spec_string ("name", + &details->name), + GNUNET_PQ_result_spec_string ("cipher_choice", + &details->cipher_spec), + GNUNET_PQ_result_spec_string ("description", + &details->description), + TALER_PQ_result_spec_json ("description_i18n", + &details->description_i18n), + GNUNET_PQ_result_spec_allow_null ( + TALER_PQ_result_spec_json ("extra_data", + &details->extra_data), + NULL), + GNUNET_PQ_result_spec_timestamp ("valid_after", + &details->valid_after), + GNUNET_PQ_result_spec_timestamp ("valid_before", + &details->valid_before), + GNUNET_PQ_result_spec_relative_time ("duration", + &details->duration), + GNUNET_PQ_result_spec_relative_time ("validity_granularity", + &details->validity_granularity), + GNUNET_PQ_result_spec_relative_time ("start_offset", + &details->start_offset), + GNUNET_PQ_result_spec_string ("kind", + &kind), + GNUNET_PQ_result_spec_uint64 ("issued", + &details->issued), + GNUNET_PQ_result_spec_uint64 ("used", + &details->used), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; - if (NULL == details) + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + check_connection (pg); + TMH_PQ_prepare_anon (pg, + "SELECT" + " slug" + ",name" + ",cipher_choice" + ",description" + ",description_i18n::TEXT" + ",extra_data::TEXT" + ",valid_after" + ",valid_before" + ",duration" + ",validity_granularity" + ",start_offset" + ",kind" + ",issued" + ",used" + " FROM merchant_token_families" + " WHERE slug=$1"); + memset (details, + 0, + sizeof (*details)); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { - struct GNUNET_PQ_ResultSpec rs_null[] = { - GNUNET_PQ_result_spec_end - }; - - check_connection (pg); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_token_family", - params, - rs_null); - } - else - { - char *kind; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("slug", - &details->slug), - GNUNET_PQ_result_spec_string ("name", - &details->name), - GNUNET_PQ_result_spec_string ("cipher_choice", - &details->cipher_spec), - GNUNET_PQ_result_spec_string ("description", - &details->description), - TALER_PQ_result_spec_json ("description_i18n", - &details->description_i18n), - GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_json ("extra_data", - &details->extra_data), - NULL), - GNUNET_PQ_result_spec_timestamp ("valid_after", - &details->valid_after), - GNUNET_PQ_result_spec_timestamp ("valid_before", - &details->valid_before), - GNUNET_PQ_result_spec_relative_time ("duration", - &details->duration), - GNUNET_PQ_result_spec_relative_time ("validity_granularity", - &details->validity_granularity), - GNUNET_PQ_result_spec_relative_time ("start_offset", - &details->start_offset), - GNUNET_PQ_result_spec_string ("kind", - &kind), - GNUNET_PQ_result_spec_uint64 ("issued", - &details->issued), - GNUNET_PQ_result_spec_uint64 ("used", - &details->used), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - check_connection (pg); - PREPARE (pg, - "lookup_token_family", - "SELECT" - " slug" - ",name" - ",cipher_choice" - ",description" - ",description_i18n::TEXT" - ",extra_data::TEXT" - ",valid_after" - ",valid_before" - ",duration" - ",validity_granularity" - ",start_offset" - ",kind" - ",issued" - ",used" - " FROM merchant_token_families" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_token_families.slug=$2"); - memset (details, - 0, - sizeof (*details)); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_token_family", - params, - rs); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + if (0 == strcmp (kind, + "discount")) + details->kind = TALER_MERCHANTDB_TFK_Discount; + else if (0 == strcmp (kind, + "subscription")) + details->kind = TALER_MERCHANTDB_TFK_Subscription; + else { - if (0 == strcmp (kind, "discount")) - details->kind = TALER_MERCHANTDB_TFK_Discount; - else if (0 == strcmp (kind, "subscription")) - details->kind = TALER_MERCHANTDB_TFK_Subscription; - else - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; } - return qs; } + return qs; } diff --git a/src/backenddb/lookup_token_family_key.c b/src/backenddb/lookup_token_family_key.c @@ -39,151 +39,133 @@ TALER_MERCHANTDB_lookup_token_family_key ( struct TALER_MERCHANTDB_TokenFamilyKeyDetails *details) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (token_family_slug), GNUNET_PQ_query_param_timestamp (&valid_at), GNUNET_PQ_query_param_timestamp (&sign_until), GNUNET_PQ_query_param_end }; + char *kind; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_blind_sign_pub ("pub", + &details->pub.public_key), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_blind_sign_priv ("priv", + &details->priv.private_key), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_timestamp ("signature_validity_start", + &details->signature_validity_start), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_timestamp ("signature_validity_end", + &details->signature_validity_end), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_timestamp ("private_key_deleted_at", + &details->private_key_deleted_at), + NULL), + GNUNET_PQ_result_spec_string ("slug", + &details->token_family.slug), + GNUNET_PQ_result_spec_string ("name", + &details->token_family.name), + GNUNET_PQ_result_spec_string ("cipher_choice", + &details->token_family.cipher_spec), + GNUNET_PQ_result_spec_string ("description", + &details->token_family.description), + TALER_PQ_result_spec_json ("description_i18n", + &details->token_family.description_i18n), + GNUNET_PQ_result_spec_timestamp ("valid_after", + &details->token_family.valid_after), + GNUNET_PQ_result_spec_timestamp ("valid_before", + &details->token_family.valid_before), + GNUNET_PQ_result_spec_relative_time ("duration", + &details->token_family.duration), + GNUNET_PQ_result_spec_relative_time ("validity_granularity", + &details->token_family. + validity_granularity), + GNUNET_PQ_result_spec_relative_time ("start_offset", + &details->token_family.start_offset), + GNUNET_PQ_result_spec_string ("kind", + &kind), + GNUNET_PQ_result_spec_uint64 ("issued", + &details->token_family.issued), + GNUNET_PQ_result_spec_uint64 ("used", + &details->token_family.used), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_token_family_key", - "SELECT" - " mtfk.h_pub" - ",mtfk.pub" - ",mtfk.priv" - ",cipher_choice" - ",mtfk.signature_validity_start" - ",mtfk.signature_validity_end" - ",mtfk.private_key_deleted_at" - ",slug" - ",name" - ",description" - ",description_i18n::TEXT" - ",mtf.valid_after" - ",mtf.valid_before" - ",duration" - ",validity_granularity" - ",start_offset" - ",kind" - ",issued" - ",used" - " FROM merchant_token_families mtf" - " LEFT JOIN merchant_token_family_keys mtfk" - " ON ( (mtf.token_family_serial = mtfk.token_family_serial)" - " AND ($3 >= mtfk.signature_validity_start)" - " AND ($3 <= mtfk.signature_validity_end)" - " AND ($4 <= mtfk.private_key_deleted_at) )" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " AND slug=$2" - " AND ($3 >= valid_after)" - " AND ($3 <= valid_before)" - " ORDER BY mtfk.signature_validity_start ASC" - " LIMIT 1"); - - if (NULL == details) - { - struct GNUNET_PQ_ResultSpec rs_null[] = { - GNUNET_PQ_result_spec_end - }; - - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_token_family_key", - params, - rs_null); - } - + TMH_PQ_prepare_anon (pg, + "SELECT" + " h_pub" + ",pub" + ",priv" + ",cipher_choice" + ",mtfk.signature_validity_start" + ",mtfk.signature_validity_end" + ",mtfk.private_key_deleted_at" + ",slug" + ",name" + ",description" + ",description_i18n::TEXT" + ",mtf.valid_after" + ",mtf.valid_before" + ",duration" + ",validity_granularity" + ",start_offset" + ",kind" + ",issued" + ",used" + " FROM merchant_token_families mtf" + " LEFT JOIN merchant_token_family_keys mtfk" + " ON ( (mtf.token_family_serial = mtfk.token_family_serial)" + " AND ($2 >= mtfk.signature_validity_start)" + " AND ($2 <= mtfk.signature_validity_end)" + " AND ($3 <= mtfk.private_key_deleted_at) )" + " WHERE slug=$1" + " AND ($2 >= valid_after)" + " AND ($2 <= valid_before)" + " ORDER BY mtfk.signature_validity_start ASC" + " LIMIT 1"); + memset (details, + 0, + sizeof (*details)); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { - char *kind; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_blind_sign_pub ("pub", - &details->pub.public_key), - NULL), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_blind_sign_priv ("priv", - &details->priv.private_key), - NULL), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_timestamp ("signature_validity_start", - &details->signature_validity_start), - NULL), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_timestamp ("signature_validity_end", - &details->signature_validity_end), - NULL), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_timestamp ("private_key_deleted_at", - &details->private_key_deleted_at), - NULL), - GNUNET_PQ_result_spec_string ("slug", - &details->token_family.slug), - GNUNET_PQ_result_spec_string ("name", - &details->token_family.name), - GNUNET_PQ_result_spec_string ("cipher_choice", - &details->token_family.cipher_spec), - GNUNET_PQ_result_spec_string ("description", - &details->token_family.description), - TALER_PQ_result_spec_json ("description_i18n", - &details->token_family.description_i18n), - GNUNET_PQ_result_spec_timestamp ("valid_after", - &details->token_family.valid_after), - GNUNET_PQ_result_spec_timestamp ("valid_before", - &details->token_family.valid_before), - GNUNET_PQ_result_spec_relative_time ("duration", - &details->token_family.duration), - GNUNET_PQ_result_spec_relative_time ("validity_granularity", - &details->token_family. - validity_granularity), - GNUNET_PQ_result_spec_relative_time ("start_offset", - &details->token_family.start_offset), - GNUNET_PQ_result_spec_string ("kind", - &kind), - GNUNET_PQ_result_spec_uint64 ("issued", - &details->token_family.issued), - GNUNET_PQ_result_spec_uint64 ("used", - &details->token_family.used), - GNUNET_PQ_result_spec_end - }; - - memset (details, - 0, - sizeof (*details)); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_token_family_key", - params, - rs); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + if (0 == strcmp (kind, + "discount")) + { + details->token_family.kind = TALER_MERCHANTDB_TFK_Discount; + } + else if (0 == strcmp (kind, + "subscription")) + { + details->token_family.kind = TALER_MERCHANTDB_TFK_Subscription; + } + else { - if (0 == strcmp (kind, - "discount")) - { - details->token_family.kind = TALER_MERCHANTDB_TFK_Discount; - } - else if (0 == strcmp (kind, - "subscription")) - { - details->token_family.kind = TALER_MERCHANTDB_TFK_Subscription; - } - else - { - GNUNET_free (kind); - GNUNET_free (details->token_family.slug); - GNUNET_free (details->token_family.name); - GNUNET_free (details->token_family.description); - json_decref (details->token_family.description_i18n); - GNUNET_CRYPTO_blind_sign_pub_decref (details->pub.public_key); - GNUNET_CRYPTO_blind_sign_priv_decref (details->priv.private_key); - GNUNET_free (details->token_family.cipher_spec); - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } GNUNET_free (kind); + GNUNET_free (details->token_family.slug); + GNUNET_free (details->token_family.name); + GNUNET_free (details->token_family.description); + json_decref (details->token_family.description_i18n); + GNUNET_CRYPTO_blind_sign_pub_decref (details->pub.public_key); + GNUNET_CRYPTO_blind_sign_priv_decref (details->priv.private_key); + GNUNET_free (details->token_family.cipher_spec); + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; } - return qs; + GNUNET_free (kind); } + return qs; } diff --git a/src/backenddb/lookup_token_family_keys.c b/src/backenddb/lookup_token_family_keys.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_token_family_keys.h" #include "helper.h" @@ -166,7 +164,6 @@ TALER_MERCHANTDB_lookup_token_family_keys ( void *cb_cls) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (token_family_slug), GNUNET_PQ_query_param_timestamp (&start_time), GNUNET_PQ_query_param_timestamp (&end_time), @@ -179,41 +176,40 @@ TALER_MERCHANTDB_lookup_token_family_keys ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_token_family_keys", - "SELECT" - " h_pub" - ",mtfk.pub" - ",mtfk.priv" - ",cipher_choice" - ",mtfk.signature_validity_start" - ",mtfk.signature_validity_end" - ",mtfk.private_key_deleted_at" - ",slug" - ",name" - ",description" - ",description_i18n::TEXT" - ",mtf.valid_after" - ",mtf.valid_before" - ",duration" - ",validity_granularity" - ",start_offset" - ",kind" - ",issued" - ",used" - " FROM merchant_token_families mtf" - " LEFT JOIN merchant_token_family_keys mtfk" - " USING (token_family_serial)" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " AND slug=$2" - " AND COALESCE ($3 <= mtfk.signature_validity_end, TRUE)" - " AND COALESCE ($4 >= mtfk.signature_validity_start, TRUE);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " h_pub" + ",mtfk.pub" + ",mtfk.priv" + ",cipher_choice" + ",mtfk.signature_validity_start" + ",mtfk.signature_validity_end" + ",mtfk.private_key_deleted_at" + ",slug" + ",name" + ",description" + ",description_i18n::TEXT" + ",mtf.valid_after" + ",mtf.valid_before" + ",duration" + ",validity_granularity" + ",start_offset" + ",kind" + ",issued" + ",used" + " FROM merchant_token_families mtf" + " LEFT JOIN merchant_token_family_keys mtfk" + " USING (token_family_serial)" + " WHERE slug=$1" + " AND COALESCE ($2 <= mtfk.signature_validity_end, TRUE)" + " AND COALESCE ($3 >= mtfk.signature_validity_start, TRUE);"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_token_family_keys", + "", params, &lookup_token_keys_cb, &ctx); diff --git a/src/backenddb/lookup_transfer_details.c b/src/backenddb/lookup_transfer_details.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_transfer_details.h" #include "helper.h" @@ -123,30 +121,30 @@ TALER_MERCHANTDB_lookup_transfer_details ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "lookup_transfer_details", - "SELECT" - " mterm.h_contract_terms" - ",mtcoin.offset_in_exchange_list" - ",dep.coin_pub" - ",mtcoin.exchange_deposit_value" - ",mtcoin.exchange_deposit_fee" - " FROM merchant_expected_transfer_to_coin mtcoin" - " JOIN merchant_deposits dep" - " USING (deposit_serial)" - " JOIN merchant_deposit_confirmations mcon" - " USING (deposit_confirmation_serial)" - " JOIN merchant_contract_terms mterm" - " USING (order_serial)" - " JOIN merchant_expected_transfers met" - " USING (expected_credit_serial)" - " WHERE met.wtid=$2" - " AND met.exchange_url=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mterm.h_contract_terms" + ",mtcoin.offset_in_exchange_list" + ",dep.coin_pub" + ",mtcoin.exchange_deposit_value" + ",mtcoin.exchange_deposit_fee" + " FROM merchant_expected_transfer_to_coin mtcoin" + " JOIN merchant_deposits dep" + " USING (deposit_serial)" + " JOIN merchant_deposit_confirmations mcon" + " USING (deposit_confirmation_serial)" + " JOIN merchant_contract_terms mterm" + " USING (order_serial)" + " JOIN merchant_expected_transfers met" + " USING (expected_credit_serial)" + " WHERE met.wtid=$2" + " AND met.exchange_url=$1"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_transfer_details", + "", params, &lookup_transfer_details_cb, &ltdc); diff --git a/src/backenddb/lookup_transfer_details_by_order.c b/src/backenddb/lookup_transfer_details_by_order.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_transfer_details_by_order.h" #include "helper.h" @@ -201,36 +199,35 @@ TALER_MERCHANTDB_lookup_transfer_details_by_order ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "lookup_transfer_details_by_order", - "SELECT" - " md.deposit_serial" - ",mcon.exchange_url" - ",met.wtid" - ",mtc.exchange_deposit_value" - ",mtc.exchange_deposit_fee" - ",mcon.deposit_timestamp" - ",met.confirmed" - ",met.expected_credit_serial" - " FROM merchant_expected_transfer_to_coin mtc" - " JOIN merchant_deposits md" - " USING (deposit_serial)" - " JOIN merchant_deposit_confirmations mcon" - " USING (deposit_confirmation_serial)" - " JOIN merchant_expected_transfers met" - " USING (expected_credit_serial)" - " JOIN merchant_accounts acc" - " ON (acc.account_serial = met.account_serial)" - /* Check that all this is for the same instance */ - " JOIN merchant_contract_terms contracts" - " USING (merchant_serial, order_serial)" - " WHERE mcon.order_serial=$1" - " ORDER BY met.wtid"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " md.deposit_serial" + ",mcon.exchange_url" + ",met.wtid" + ",mtc.exchange_deposit_value" + ",mtc.exchange_deposit_fee" + ",mcon.deposit_timestamp" + ",met.confirmed" + ",met.expected_credit_serial" + " FROM merchant_expected_transfer_to_coin mtc" + " JOIN merchant_deposits md" + " USING (deposit_serial)" + " JOIN merchant_deposit_confirmations mcon" + " USING (deposit_confirmation_serial)" + " JOIN merchant_expected_transfers met" + " USING (expected_credit_serial)" + " JOIN merchant_accounts acc" + " ON (acc.account_serial = met.account_serial)" + " JOIN merchant_contract_terms contracts" + " USING (order_serial)" + " WHERE mcon.order_serial=$1" + " ORDER BY met.wtid"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_transfer_details_by_order", + "", params, &lookup_transfer_details_by_order_cb, &ltdo); diff --git a/src/backenddb/lookup_transfer_summary.c b/src/backenddb/lookup_transfer_summary.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_transfer_summary.h" #include "helper.h" @@ -121,28 +119,28 @@ TALER_MERCHANTDB_lookup_transfer_summary ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "lookup_transfer_summary", - "SELECT" - " mct.order_id" - ",mtc.exchange_deposit_value" - ",mtc.exchange_deposit_fee" - " FROM merchant_expected_transfers met" - " JOIN merchant_expected_transfer_to_coin mtc" - " USING (expected_credit_serial)" - " JOIN merchant_deposits dep" - " USING (deposit_serial)" - " JOIN merchant_deposit_confirmations mcon" - " USING (deposit_confirmation_serial)" - " JOIN merchant_contract_terms mct" - " USING (order_serial)" - " WHERE met.wtid=$2" - " AND met.exchange_url=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " mct.order_id" + ",mtc.exchange_deposit_value" + ",mtc.exchange_deposit_fee" + " FROM merchant_expected_transfers met" + " JOIN merchant_expected_transfer_to_coin mtc" + " USING (expected_credit_serial)" + " JOIN merchant_deposits dep" + " USING (deposit_serial)" + " JOIN merchant_deposit_confirmations mcon" + " USING (deposit_confirmation_serial)" + " JOIN merchant_contract_terms mct" + " USING (order_serial)" + " WHERE met.wtid=$2" + " AND met.exchange_url=$1"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "lookup_transfer_summary", + "", params, &lookup_transfer_summary_cb, &ltdc); diff --git a/src/backenddb/lookup_transfers.c b/src/backenddb/lookup_transfers.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_transfers.h" #include "helper.h" @@ -147,7 +145,6 @@ TALER_MERCHANTDB_lookup_transfers ( .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_timestamp (&before), GNUNET_PQ_query_param_timestamp (&after), GNUNET_PQ_query_param_uint64 (&offset), @@ -155,7 +152,7 @@ TALER_MERCHANTDB_lookup_transfers ( NULL == payto_uri.full_payto ? GNUNET_PQ_query_param_null () /* NULL: do not filter by payto URI */ : GNUNET_PQ_query_param_string (payto_uri.full_payto), - GNUNET_PQ_query_param_bool (! by_time), /* $7: filter by time? */ + GNUNET_PQ_query_param_bool (! by_time), /* $6: filter by time? */ GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == expected), /* filter by expected? */ GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_YES == expected), @@ -163,80 +160,77 @@ TALER_MERCHANTDB_lookup_transfers ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_transfers_asc", - "SELECT" - " mt.credit_amount" - ",mt.wtid" - ",mac.payto_uri" - ",mt.exchange_url" - ",mt.credit_serial" - ",mt.execution_time" - ",mt.expected" - ",met.expected_credit_serial" - " FROM merchant_transfers mt" - " JOIN merchant_accounts mac" - " USING (account_serial)" - " LEFT JOIN merchant_expected_transfers met" - " ON mt.wtid = met.wtid" - " AND mt.account_serial = met.account_serial" - " AND mt.exchange_url = met.exchange_url" - " AND mt.expected" - " WHERE ( $7 OR " - " (mt.execution_time < $2 AND" - " mt.execution_time >= $3) )" - " AND ( (CAST($6 AS TEXT) IS NULL) OR " - " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" - " =REGEXP_REPLACE($6,'\\?.*','')) )" - " AND ( $8 OR " - " (mt.expected = $9) )" - " AND merchant_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND (mt.credit_serial > $4)" - " ORDER BY mt.credit_serial ASC" - " LIMIT $5"); - PREPARE (pg, - "lookup_transfers_desc", - "SELECT" - " mt.credit_amount" - ",mt.wtid" - ",mac.payto_uri" - ",mt.exchange_url" - ",mt.credit_serial" - ",mt.execution_time" - ",mt.expected" - ",met.expected_credit_serial" - " FROM merchant_transfers mt" - " JOIN merchant_accounts mac" - " USING (account_serial)" - " LEFT JOIN merchant_expected_transfers met" - " ON mt.wtid = met.wtid" - " AND mt.account_serial = met.account_serial" - " AND mt.exchange_url = met.exchange_url" - " AND mt.expected" - " WHERE ( $7 OR " - " (mt.execution_time < $2 AND" - " mt.execution_time >= $3) )" - " AND ( (CAST($6 AS TEXT) IS NULL) OR " - " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" - " =REGEXP_REPLACE($6,'\\?.*','')) )" - " AND ( $8 OR " - " (mt.expected = $9) )" - " AND merchant_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND (mt.credit_serial < $4)" - " ORDER BY mt.credit_serial DESC" - " LIMIT $5"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " mt.credit_amount" + ",mt.wtid" + ",mac.payto_uri" + ",mt.exchange_url" + ",mt.credit_serial" + ",mt.execution_time" + ",mt.expected" + ",met.expected_credit_serial" + " FROM merchant_transfers mt" + " JOIN merchant_accounts mac" + " USING (account_serial)" + " LEFT JOIN merchant_expected_transfers met" + " ON mt.wtid = met.wtid" + " AND mt.account_serial = met.account_serial" + " AND mt.exchange_url = met.exchange_url" + " AND mt.expected" + " WHERE ( $6 OR " + " (mt.execution_time < $1 AND" + " mt.execution_time >= $2) )" + " AND ( (CAST($5 AS TEXT) IS NULL) OR " + " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($5,'\\?.*','')) )" + " AND ( $7 OR " + " (mt.expected = $8) )" + " AND (mt.credit_serial > $3)" + " ORDER BY mt.credit_serial ASC" + " LIMIT $4"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " mt.credit_amount" + ",mt.wtid" + ",mac.payto_uri" + ",mt.exchange_url" + ",mt.credit_serial" + ",mt.execution_time" + ",mt.expected" + ",met.expected_credit_serial" + " FROM merchant_transfers mt" + " JOIN merchant_accounts mac" + " USING (account_serial)" + " LEFT JOIN merchant_expected_transfers met" + " ON mt.wtid = met.wtid" + " AND mt.account_serial = met.account_serial" + " AND mt.exchange_url = met.exchange_url" + " AND mt.expected" + " WHERE ( $6 OR " + " (mt.execution_time < $1 AND" + " mt.execution_time >= $2) )" + " AND ( (CAST($5 AS TEXT) IS NULL) OR " + " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($5,'\\?.*','')) )" + " AND ( $7 OR " + " (mt.expected = $8) )" + " AND (mt.credit_serial < $3)" + " ORDER BY mt.credit_serial DESC" + " LIMIT $4"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "lookup_transfers_asc" - : "lookup_transfers_desc", + "", params, &lookup_transfers_cb, &ltc); diff --git a/src/backenddb/lookup_units.c b/src/backenddb/lookup_units.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025 Taler Systems SA + Copyright (C) 2025 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 @@ -19,8 +19,6 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_units.h" #include "helper.h" @@ -89,10 +87,11 @@ lookup_units_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_units (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - TALER_MERCHANTDB_UnitsCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_units ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + TALER_MERCHANTDB_UnitsCallback cb, + void *cb_cls) { struct LookupUnitsContext luc = { .cb = cb, @@ -100,48 +99,43 @@ TALER_MERCHANTDB_lookup_units (struct TALER_MERCHANTDB_PostgresContext *pg, .extract_failed = false }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_units", - "WITH mi AS (" - " SELECT merchant_serial FROM merchant_instances WHERE merchant_id=$1" - ")" - "SELECT cu.unit_serial" - " ,cu.unit" - " ,cu.unit_name_long" - " ,cu.unit_name_short" - " ,cu.unit_name_long_i18n" - " ,cu.unit_name_short_i18n" - " ,cu.unit_allow_fraction" - " ,cu.unit_precision_level" - " ,cu.unit_active" - " ,FALSE AS unit_builtin" - " FROM merchant_custom_units cu" - " JOIN mi ON cu.merchant_serial = mi.merchant_serial" - " UNION ALL " - "SELECT bu.unit_serial" - " ,bu.unit" - " ,bu.unit_name_long" - " ,bu.unit_name_short" - " ,bu.unit_name_long_i18n" - " ,bu.unit_name_short_i18n" - " ,COALESCE(bo.override_allow_fraction, bu.unit_allow_fraction)" - " ,COALESCE(bo.override_precision_level, bu.unit_precision_level)" - " ,COALESCE(bo.override_active, bu.unit_active)" - " ,TRUE AS unit_builtin" - " FROM merchant_builtin_units bu" - " JOIN mi ON TRUE" - " LEFT JOIN merchant_builtin_unit_overrides bo" - " ON bo.builtin_unit_serial = bu.unit_serial" - " AND bo.merchant_serial = mi.merchant_serial" - " ORDER BY unit"); + TMH_PQ_prepare_anon (pg, + "SELECT cu.unit_serial" + " ,cu.unit" + " ,cu.unit_name_long" + " ,cu.unit_name_short" + " ,cu.unit_name_long_i18n" + " ,cu.unit_name_short_i18n" + " ,cu.unit_allow_fraction" + " ,cu.unit_precision_level" + " ,cu.unit_active" + " ,FALSE AS unit_builtin" + " FROM merchant_custom_units cu" + " UNION ALL " + "SELECT bu.unit_serial" + " ,bu.unit" + " ,bu.unit_name_long" + " ,bu.unit_name_short" + " ,bu.unit_name_long_i18n" + " ,bu.unit_name_short_i18n" + " ,COALESCE(bo.override_allow_fraction, bu.unit_allow_fraction)" + " ,COALESCE(bo.override_precision_level, bu.unit_precision_level)" + " ,COALESCE(bo.override_active, bu.unit_active)" + " ,TRUE AS unit_builtin" + " FROM merchant.merchant_builtin_units bu" + " LEFT JOIN merchant_builtin_unit_overrides bo" + " ON bo.builtin_unit_serial = bu.unit_serial" + " ORDER BY unit"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_units", + "", params, &lookup_units_cb, &luc); diff --git a/src/backenddb/lookup_webhook.c b/src/backenddb/lookup_webhook.c @@ -19,47 +19,45 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *webhook_id, - struct TALER_MERCHANTDB_WebhookDetails *wb) +TALER_MERCHANTDB_lookup_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *webhook_id, + struct TALER_MERCHANTDB_WebhookDetails *wb) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (webhook_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_webhook", - "SELECT" - " event_type" - ",url" - ",http_method" - ",header_template" - ",body_template" - " FROM merchant_webhook" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_webhook.webhook_id=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " event_type" + ",url" + ",http_method" + ",header_template" + ",body_template" + " FROM merchant_webhook" + " WHERE webhook_id=$1"); - if (NULL == wb) + if (NULL == wb) // fIXME: is this case needed? { struct GNUNET_PQ_ResultSpec rs_null[] = { GNUNET_PQ_result_spec_end }; return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_webhook", + "", params, rs_null); } @@ -84,7 +82,7 @@ TALER_MERCHANTDB_lookup_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, }; return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_webhook", + "", params, rs); } diff --git a/src/backenddb/lookup_webhook_by_event.c b/src/backenddb/lookup_webhook_by_event.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_webhook_by_event.h" #include "helper.h" @@ -111,11 +109,12 @@ lookup_webhook_by_event_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_webhook_by_event (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *event_type, - TALER_MERCHANTDB_WebhookDetailCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_webhook_by_event ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *event_type, + TALER_MERCHANTDB_WebhookDetailCallback cb, + void *cb_cls) { struct LookupWebhookDetailContext wlc = { .cb = cb, @@ -124,30 +123,28 @@ TALER_MERCHANTDB_lookup_webhook_by_event (struct TALER_MERCHANTDB_PostgresContex }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (event_type), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_webhook_by_event", - "SELECT" - " webhook_serial" - ",event_type" - ",url" - ",http_method" - ",header_template" - ",body_template" - " FROM merchant_webhook" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND event_type=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " webhook_serial" + ",event_type" + ",url" + ",http_method" + ",header_template" + ",body_template" + " FROM merchant_webhook" + " WHERE event_type=$1"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_webhook_by_event", + "", params, &lookup_webhook_by_event_cb, &wlc); diff --git a/src/backenddb/lookup_webhooks.c b/src/backenddb/lookup_webhooks.c @@ -19,8 +19,6 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_webhooks.h" #include "helper.h" @@ -92,10 +90,11 @@ lookup_webhooks_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_webhooks (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - TALER_MERCHANTDB_WebhooksCallback cb, - void *cb_cls) +TALER_MERCHANTDB_lookup_webhooks ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + TALER_MERCHANTDB_WebhooksCallback cb, + void *cb_cls) { struct LookupWebhookContext wlc = { .cb = cb, @@ -104,24 +103,22 @@ TALER_MERCHANTDB_lookup_webhooks (struct TALER_MERCHANTDB_PostgresContext *pg, .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_webhooks", - "SELECT" - " webhook_id" - ",event_type" - " FROM merchant_webhook" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " webhook_id" + ",event_type" + " FROM merchant_webhook"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_webhooks", + "", params, &lookup_webhooks_cb, &wlc); diff --git a/src/backenddb/lookup_wire_fee.c b/src/backenddb/lookup_wire_fee.c @@ -19,21 +19,21 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/lookup_wire_fee.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_lookup_wire_fee (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MasterPublicKeyP *master_pub, - const char *wire_method, - struct GNUNET_TIME_Timestamp contract_date, - struct TALER_WireFeeSet *fees, - struct GNUNET_TIME_Timestamp *start_date, - struct GNUNET_TIME_Timestamp *end_date, - struct TALER_MasterSignatureP *master_sig) +TALER_MERCHANTDB_lookup_wire_fee ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MasterPublicKeyP *master_pub, + const char *wire_method, + struct GNUNET_TIME_Timestamp contract_date, + struct TALER_WireFeeSet *fees, + struct GNUNET_TIME_Timestamp *start_date, + struct GNUNET_TIME_Timestamp *end_date, + struct TALER_MasterSignatureP *master_sig) { struct GNUNET_HashCode h_wire_method; struct GNUNET_PQ_QueryParam params[] = { @@ -68,7 +68,7 @@ TALER_MERCHANTDB_lookup_wire_fee (struct TALER_MERCHANTDB_PostgresContext *pg, ",start_date" ",end_date" ",master_sig" - " FROM merchant_exchange_wire_fees" + " FROM merchant.merchant_exchange_wire_fees" " WHERE master_pub=$1" " AND h_wire_method=$2" " AND start_date <= $3" diff --git a/src/backenddb/mark_contract_paid.c b/src/backenddb/mark_contract_paid.c @@ -19,21 +19,20 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/mark_contract_paid.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_mark_contract_paid (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - const char *session_id, - int16_t choice_index) +TALER_MERCHANTDB_mark_contract_paid ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + const char *session_id, + int16_t choice_index) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_string (session_id), (choice_index >= 0) @@ -42,7 +41,6 @@ TALER_MERCHANTDB_mark_contract_paid (struct TALER_MERCHANTDB_PostgresContext *pg GNUNET_PQ_query_param_end }; struct GNUNET_PQ_QueryParam uparams[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_end }; @@ -50,72 +48,60 @@ TALER_MERCHANTDB_mark_contract_paid (struct TALER_MERCHANTDB_PostgresContext *pg /* Session ID must always be given by the caller. */ GNUNET_assert (NULL != session_id); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + /* FIXME: combine the 3 individual statements into a stored procedure! */ /* no preflight check here, run in transaction by caller! */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Marking h_contract_terms '%s' of %s as paid for session `%s'\n", GNUNET_h2s (&h_contract_terms->hash), instance_id, session_id); - PREPARE (pg, - "mark_contract_paid", - "UPDATE merchant_contract_terms SET" - " paid=TRUE" - ",session_id=$3" - ",choice_index=$4" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_contract_terms SET" + " paid=TRUE" + ",session_id=$2" + ",choice_index=$3" + " WHERE h_contract_terms=$1"); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "mark_contract_paid", + "", params); if (qs <= 0) return qs; - PREPARE (pg, - "mark_inventory_sold", - "UPDATE merchant_inventory SET" - " total_sold = total_sold" - " + order_locks.total_locked" - " + ((merchant_inventory.total_sold_frac::BIGINT" - " + order_locks.total_locked_frac::BIGINT) / 1000000)" - " ,total_sold_frac =" - " ((merchant_inventory.total_sold_frac::BIGINT" - " + order_locks.total_locked_frac::BIGINT) % 1000000)::INT4" - " FROM (SELECT total_locked,total_locked_frac,product_serial" - " FROM merchant_order_locks" - " WHERE order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))" - " ) AS order_locks" - " WHERE merchant_inventory.product_serial" - " =order_locks.product_serial"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_inventory SET" + " total_sold = total_sold" + " + order_locks.total_locked" + " + ((merchant_inventory.total_sold_frac::BIGINT" + " + order_locks.total_locked_frac::BIGINT) / 1000000)" + " ,total_sold_frac =" + " ((merchant_inventory.total_sold_frac::BIGINT" + " + order_locks.total_locked_frac::BIGINT) % 1000000)::INT4" + " FROM (SELECT total_locked,total_locked_frac,product_serial" + " FROM merchant_order_locks" + " WHERE order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)" + " ) AS order_locks" + " WHERE merchant_inventory.product_serial" + " =order_locks.product_serial"); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "mark_inventory_sold", + "", uparams); if (qs < 0) return qs; /* 0: no inventory management, that's OK! */ /* ON DELETE CASCADE deletes from merchant_order_locks */ - PREPARE (pg, - "delete_completed_order", - "WITH md AS" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1) " - "DELETE" - " FROM merchant_orders" - " WHERE order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " JOIN md USING (merchant_serial)" - " WHERE h_contract_terms=$2)"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_orders" + " WHERE order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "delete_completed_order", + "", uparams); } diff --git a/src/backenddb/mark_order_wired.c b/src/backenddb/mark_order_wired.c @@ -19,29 +19,28 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/mark_order_wired.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_mark_order_wired (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t order_serial) +TALER_MERCHANTDB_mark_order_wired ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t order_serial) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&order_serial), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "mark_order_wired", - "UPDATE merchant_contract_terms SET" - " wired=TRUE" - " WHERE order_serial=$1"); - + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_contract_terms SET" + " wired=TRUE" + " WHERE order_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "mark_order_wired", + "", params); } diff --git a/src/backenddb/meson.build b/src/backenddb/meson.build @@ -14,6 +14,7 @@ libtalermerchantdb = library( 'event_listen.c', 'event_notify.c', 'preflight.c', + 'set_instance.c', 'account_kyc_get_outdated.c', 'account_kyc_get_status.c', 'account_kyc_set_failed.c', @@ -143,6 +144,7 @@ libtalermerchantdb = library( 'lookup_transfers.c', 'lookup_webhook.c', 'lookup_webhook_by_event.c', + 'lookup_all_webhooks.c', 'lookup_webhooks.c', 'lookup_wire_fee.c', 'mark_contract_paid.c', @@ -153,6 +155,7 @@ libtalermerchantdb = library( 'select_account.c', 'select_account_by_uri.c', 'select_accounts.c', + 'select_accounts_by_instance.c', 'select_accounts_by_exchange.c', 'select_category.c', 'select_category_by_name.c', diff --git a/src/backenddb/pg.c b/src/backenddb/pg.c @@ -33,8 +33,10 @@ #include "helper.h" +// FIXME: prefix? void -check_connection (struct TALER_MERCHANTDB_PostgresContext *pg) +check_connection ( + struct TALER_MERCHANTDB_PostgresContext *pg) { if (NULL != pg->transaction_name) return; @@ -43,7 +45,8 @@ check_connection (struct TALER_MERCHANTDB_PostgresContext *pg) struct TALER_MERCHANTDB_PostgresContext * -TALER_MERCHANTDB_connect (const struct GNUNET_CONFIGURATION_Handle *cfg) +TALER_MERCHANTDB_connect ( + const struct GNUNET_CONFIGURATION_Handle *cfg) { struct TALER_MERCHANTDB_PostgresContext *pg; struct GNUNET_PQ_ExecuteStatement es[] = { @@ -82,9 +85,11 @@ TALER_MERCHANTDB_connect (const struct GNUNET_CONFIGURATION_Handle *cfg) void -TALER_MERCHANTDB_disconnect (struct TALER_MERCHANTDB_PostgresContext *pg) +TALER_MERCHANTDB_disconnect ( + struct TALER_MERCHANTDB_PostgresContext *pg) { GNUNET_PQ_disconnect (pg->conn); + GNUNET_free (pg->current_merchant_id); GNUNET_free (pg); } diff --git a/src/backenddb/pg_account_kyc_get_status.sql b/src/backenddb/pg_account_kyc_get_status.sql @@ -1,124 +0,0 @@ --- --- 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_account_kyc_get_status; -CREATE FUNCTION merchant_do_account_kyc_get_status ( - IN in_merchant_id TEXT, - IN in_now INT8, - IN in_exchange_url TEXT, -- can be NULL - IN in_h_wire BYTEA -- can be NULL -) RETURNS TABLE ( - out_h_wire BYTEA, -- never NULL - out_payto_uri TEXT, -- never NULL - out_exchange_url TEXT, - out_kyc_timestamp INT8, - out_kyc_ok BOOLEAN, - out_access_token BYTEA, - out_exchange_http_status INT4, - out_exchange_ec_code INT4, - out_aml_review BOOLEAN, - out_jaccount_limits TEXT -) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_serial INT8; - my_account_serial INT8; - my_h_wire BYTEA; - my_payto_uri TEXT; - my_kyc_record RECORD; - -BEGIN - -- Get the merchant_serial from merchant_instances - SELECT merchant_serial - INTO my_merchant_serial - FROM merchant_instances - WHERE merchant_id = in_merchant_id; - IF NOT FOUND - THEN - RETURN; - END IF; - - -- Iterate over merchant_accounts for this merchant - FOR my_account_serial, my_h_wire, my_payto_uri - IN SELECT account_serial, h_wire, payto_uri - FROM merchant_accounts - WHERE merchant_serial = my_merchant_serial - AND active - AND (in_h_wire IS NULL OR h_wire = in_h_wire) - ORDER BY account_serial ASC - LOOP - - -- Fetch KYC info for this account (can have multiple results) - FOR my_kyc_record IN - SELECT - mk.kyc_serial_id - ,mk.exchange_url - ,mk.kyc_timestamp - ,mk.kyc_ok - ,mk.access_token - ,mk.exchange_http_status - ,mk.exchange_ec_code - ,mk.aml_review - ,mk.jaccount_limits::TEXT - FROM merchant_kyc mk - WHERE mk.account_serial = my_account_serial - AND (in_exchange_url IS NULL OR mk.exchange_url = in_exchange_url) - ORDER BY mk.kyc_serial_id ASC - LOOP - -- Ask taler-merchant-kyccheck to get us an update on the status ASAP - UPDATE merchant_kyc - SET next_kyc_poll=in_now - WHERE kyc_serial_id = my_kyc_record.kyc_serial_id; - NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; -- MERCHANT_EXCHANGE_KYC_UPDATE_FORCED - RETURN QUERY - SELECT - my_h_wire, - my_payto_uri, - my_kyc_record.exchange_url, - my_kyc_record.kyc_timestamp, - my_kyc_record.kyc_ok, - my_kyc_record.access_token, - my_kyc_record.exchange_http_status, - my_kyc_record.exchange_ec_code, - my_kyc_record.aml_review, - my_kyc_record.jaccount_limits::TEXT; - END LOOP; -- loop over exchanges with KYC status for the given account - - IF NOT FOUND - THEN - -- Still return to server that we do NOT know anything - -- for the given exchange yet (but that the bank account exists) - RETURN QUERY - SELECT - my_h_wire, - my_payto_uri, - NULL::TEXT, - NULL::INT8, - NULL::BOOLEAN, - NULL::BYTEA, - NULL::INT4, - NULL::INT4, - NULL::BOOLEAN, - NULL::TEXT; - END IF; - - END LOOP; -- loop over merchant_accounts - -END $$; -COMMENT ON FUNCTION merchant_do_account_kyc_get_status - IS 'Returns the KYC status of selected exchanges and accounts, but ALSO resets the next_kyc_check time for all returned data points to the current time (in_now argument)'; diff --git a/src/backenddb/pg_account_kyc_set_failed.sql b/src/backenddb/pg_account_kyc_set_failed.sql @@ -1,100 +0,0 @@ --- --- 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_account_kyc_set_failed; - -CREATE FUNCTION merchant_do_account_kyc_set_failed ( - IN in_merchant_id TEXT, - IN in_h_wire BYTEA, - IN in_exchange_url TEXT, - IN in_timestamp INT8, - IN in_exchange_http_status INT4, - IN in_kyc_ok BOOL, - IN in_notify_str TEXT, - IN in_notify2_str TEXT, - OUT out_no_instance BOOL, - OUT out_no_account BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_account_serial INT8; -BEGIN - -out_no_instance=FALSE; -out_no_account=FALSE; - --- Which instance are we using? -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_merchant_id; - -IF NOT FOUND -THEN - out_no_instance=TRUE; - RETURN; -END IF; - -SELECT account_serial - INTO my_account_serial - FROM merchant_accounts - WHERE merchant_serial=my_merchant_id - AND h_wire=in_h_wire; - -IF NOT FOUND -THEN - out_no_account=TRUE; - RETURN; -END IF; - -UPDATE merchant_kyc - SET kyc_timestamp=in_timestamp - ,kyc_ok=in_kyc_ok - ,exchange_http_status=in_exchange_http_status - ,exchange_ec_code=0 - WHERE account_serial=my_account_serial - AND exchange_url=in_exchange_url; - -IF NOT FOUND -THEN - - INSERT INTO merchant_kyc - (kyc_timestamp - ,kyc_ok - ,account_serial - ,exchange_url - ,exchange_http_status) - VALUES - (in_timestamp - ,in_kyc_ok - ,my_account_serial - ,in_exchange_url - ,in_exchange_http_status); -END IF; - -EXECUTE FORMAT ( - 'NOTIFY %s' - ,in_notify_str); - -EXECUTE FORMAT ( - 'NOTIFY %s' - ,in_notify2_str); - - --- Success! -END $$; diff --git a/src/backenddb/pg_account_kyc_set_status.sql b/src/backenddb/pg_account_kyc_set_status.sql @@ -1,127 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024, 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_account_kyc_set_status; - -CREATE FUNCTION merchant_do_account_kyc_set_status ( - IN in_merchant_id TEXT, - IN in_h_wire BYTEA, - IN in_exchange_url TEXT, - IN in_timestamp INT8, - IN in_exchange_http_status INT4, - IN in_exchange_ec_code INT4, - IN in_access_token BYTEA, -- can be NULL - IN in_jlimits JSONB, - IN in_aml_active BOOL, - IN in_kyc_ok BOOL, - IN in_notify_str TEXT, - IN in_notify2_str TEXT, - IN in_rule_gen INT8, - IN in_next_time INT8, - IN in_kyc_backoff INT8, - OUT out_no_instance BOOL, - OUT out_no_account BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_account_serial INT8; -BEGIN - -out_no_instance=FALSE; -out_no_account=FALSE; - --- Which instance are we using? -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_merchant_id; - -IF NOT FOUND -THEN - out_no_instance=TRUE; - RETURN; -END IF; - -SELECT account_serial - INTO my_account_serial - FROM merchant_accounts - WHERE merchant_serial=my_merchant_id - AND h_wire=in_h_wire; - -IF NOT FOUND -THEN - out_no_account=TRUE; - RETURN; -END IF; - -UPDATE merchant_kyc - SET kyc_timestamp=in_timestamp - ,kyc_ok=in_kyc_ok - ,jaccount_limits=in_jlimits - ,aml_review=in_aml_active - ,exchange_http_status=in_exchange_http_status - ,exchange_ec_code=in_exchange_ec_code - ,access_token=in_access_token - ,last_rule_gen=in_rule_gen - ,next_kyc_poll=in_next_time - ,kyc_backoff=in_kyc_backoff - WHERE account_serial=my_account_serial - AND exchange_url=in_exchange_url; - -IF NOT FOUND -THEN - - INSERT INTO merchant_kyc - (kyc_timestamp - ,kyc_ok - ,account_serial - ,exchange_url - ,jaccount_limits - ,aml_review - ,exchange_http_status - ,exchange_ec_code - ,access_token - ,last_rule_gen - ,next_kyc_poll - ,kyc_backoff) - VALUES - (in_timestamp - ,in_kyc_ok - ,my_account_serial - ,in_exchange_url - ,in_jlimits - ,in_aml_active - ,in_exchange_http_status - ,in_exchange_ec_code - ,in_access_token - ,in_rule_gen - ,in_next_time - ,in_kyc_backoff); -END IF; - -EXECUTE FORMAT ( - 'NOTIFY %s' - ,in_notify_str); - -EXECUTE FORMAT ( - 'NOTIFY %s' - ,in_notify2_str); - - --- Success! -END $$; diff --git a/src/backenddb/pg_activate_account.sql b/src/backenddb/pg_activate_account.sql @@ -1,146 +0,0 @@ --- --- 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/> --- - -DROP FUNCTION IF EXISTS merchant_do_activate_account; -CREATE FUNCTION merchant_do_activate_account( - IN in_instance_name TEXT - ,IN in_h_wire BYTEA - ,IN in_salt BYTEA - ,IN in_full_payto TEXT - ,IN in_credit_facade_url TEXT -- can be NULL - ,IN in_credit_facade_credentials TEXT -- can be NULL - ,IN in_extra_wire_subject_metadata TEXT -- can be NULL - ,OUT out_h_wire BYTEA - ,OUT out_salt BYTEA - ,OUT out_not_found BOOL - ,OUT out_conflict BOOL -) -LANGUAGE plpgsql -AS $$ -DECLARE my_instance INT8; -DECLARE my_active BOOL; -DECLARE my_cfu TEXT; -DECLARE my_cfc TEXT; -DECLARE my_ewsm TEXT; -DECLARE my_h_wire BYTEA; -DECLARE my_salt BYTEA; -BEGIN - out_not_found = FALSE; - out_conflict = FALSE; - out_h_wire = in_h_wire; - out_salt = in_salt; - - SELECT merchant_serial - INTO my_instance - FROM merchant_instances - WHERE merchant_id=in_instance_name; - IF NOT FOUND - THEN - out_not_found = TRUE; - RETURN; - END IF; - - INSERT INTO merchant_accounts - AS ma - (merchant_serial - ,h_wire - ,salt - ,payto_uri - ,credit_facade_url - ,credit_facade_credentials - ,active - ,extra_wire_subject_metadata - ) VALUES ( - my_instance - ,in_h_wire - ,in_salt - ,in_full_payto - ,in_credit_facade_url - ,in_credit_facade_credentials::JSONB - ,TRUE - ,in_extra_wire_subject_metadata - ) ON CONFLICT DO NOTHING; - IF FOUND - THEN - -- Notify taler-merchant-kyccheck about the change in - -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; - RETURN; - END IF; - - SELECT h_wire - ,salt - ,active - ,credit_facade_url - ,credit_facade_credentials::TEXT - ,extra_wire_subject_metadata - INTO my_h_wire - ,my_salt - ,my_active - ,my_cfu - ,my_cfc - ,my_ewsm - FROM merchant_accounts - WHERE merchant_serial=my_instance - AND payto_uri=in_full_payto; - IF NOT FOUND - THEN - -- This should never happen (we had a conflict!) - -- Still, safe way is to return not found. - out_not_found = TRUE; - RETURN; - END IF; - - -- Check for conflict - IF (my_active AND - (ROW (my_cfu - ,my_cfc - ,my_ewsm) - IS DISTINCT FROM - ROW (in_credit_facade_url - ,in_credit_facade_credentials - ,in_extra_wire_subject_metadata))) - THEN - -- Active conflicting account, refuse! - out_conflict = TRUE; - RETURN; - END IF; - - -- Equivalent account exists, use its salt instead of the new salt - -- and just set it to active! - out_salt = my_salt; - out_h_wire = my_h_wire; - - -- Now check if existing account is already active - IF my_active - THEN - -- nothing to do - RETURN; - END IF; - - UPDATE merchant_accounts - SET active=TRUE - ,credit_facade_url=in_credit_facade_url - ,credit_facade_credentials=in_credit_facade_credentials::JSONB - ,extra_wire_subject_metadata=in_extra_wire_subject_metadata - WHERE h_wire=out_h_wire - AND merchant_serial=my_instance; - - -- Notify taler-merchant-kyccheck about the change in (active) - -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; - -END $$; diff --git a/src/backenddb/pg_base32_crockford.sql b/src/backenddb/pg_base32_crockford.sql @@ -14,13 +14,13 @@ -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -- -DROP FUNCTION IF EXISTS base32_crockford; -CREATE FUNCTION base32_crockford(data BYTEA) -RETURNS TEXT -LANGUAGE plpgsql -IMMUTABLE -STRICT -PARALLEL SAFE +DROP FUNCTION IF EXISTS merchant.base32_crockford; +CREATE FUNCTION merchant.base32_crockford(data BYTEA) + RETURNS TEXT + LANGUAGE plpgsql + IMMUTABLE + STRICT + PARALLEL SAFE AS $$ DECLARE alphabet TEXT := '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; @@ -62,5 +62,5 @@ BEGIN RETURN array_to_string(chars, ''); END; $$; -COMMENT ON FUNCTION base32_crockford(BYTEA) +COMMENT ON FUNCTION merchant.base32_crockford(BYTEA) IS 'Encodes binary data using Crockford Base32'; diff --git a/src/backenddb/pg_create_instance_schema.sql b/src/backenddb/pg_create_instance_schema.sql @@ -0,0 +1,45 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.create_instance_schema; +CREATE FUNCTION merchant.create_instance_schema( + in_merchant_serial BIGINT +) + RETURNS void + LANGUAGE plpgsql + AS $$ +DECLARE + s TEXT := 'merchant_instance_' || in_merchant_serial::TEXT; + r RECORD; +BEGIN + EXECUTE format('CREATE SCHEMA %I', s); + + -- Call fixup functions to initialize all tables + FOR r IN + SELECT migration_name + FROM merchant.instance_fixups + ORDER BY version ASC + LOOP + EXECUTE FORMAT ('CALL merchant.%I(%L)', r.migration_name, s); + END LOOP; + EXECUTE FORMAT ('CALL merchant.sync_instance_procedures(%s)', in_merchant_serial); +END $$; + +COMMENT ON FUNCTION merchant.create_instance_schema(BIGINT) + IS 'Constructs the per-instance schema merchant_instance_<merchant_serial>' + ' with all per-instance tables, stored procedures and triggers. Called from' + ' merchant_instances_after_insert_trigger() whenever a new instance row' + ' is inserted into merchant.merchant_instances.'; diff --git a/src/backenddb/pg_create_instance_trigger.sql b/src/backenddb/pg_create_instance_trigger.sql @@ -0,0 +1,58 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.merchant_instances_after_insert_trigger() CASCADE; +CREATE FUNCTION merchant.merchant_instances_after_insert_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + PERFORM merchant.create_instance_schema(NEW.merchant_serial); + RETURN NEW; +END $$; +COMMENT ON FUNCTION merchant.merchant_instances_after_insert_trigger() + IS 'Builds the per-instance schema merchant_instance_<merchant_serial>' + ' for the newly inserted row.'; + +DROP TRIGGER IF EXISTS merchant_instances_on_insert + ON merchant.merchant_instances; +CREATE TRIGGER merchant_instances_on_insert + AFTER INSERT + ON merchant.merchant_instances + FOR EACH ROW + EXECUTE FUNCTION merchant.merchant_instances_after_insert_trigger(); + + +DROP FUNCTION IF EXISTS merchant.merchant_instances_after_delete_trigger() CASCADE; +CREATE FUNCTION merchant.merchant_instances_after_delete_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + EXECUTE format('DROP SCHEMA IF EXISTS %I CASCADE', + 'merchant_instance_' || OLD.merchant_serial::TEXT); + RETURN NULL; +END $$; +COMMENT ON FUNCTION merchant.merchant_instances_after_delete_trigger() + IS 'Drops the per-instance schema merchant_instance_<merchant_serial>' + ' when its row in merchant.merchant_instances is removed.'; + +DROP TRIGGER IF EXISTS merchant_instances_on_delete + ON merchant.merchant_instances; +CREATE TRIGGER merchant_instances_on_delete + AFTER DELETE ON merchant.merchant_instances + FOR EACH ROW + EXECUTE FUNCTION merchant.merchant_instances_after_delete_trigger(); diff --git a/src/backenddb/pg_do_handle_category_changes.sql b/src/backenddb/pg_do_handle_category_changes.sql @@ -33,24 +33,23 @@ BEGIN url, http_method, body_template - FROM merchant.merchant_webhook + 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, + resolved_body := merchant.replace_placeholder(resolved_body, + 'webhook_type', + 'category_added'); + resolved_body := merchant.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); + resolved_body := merchant.replace_placeholder(resolved_body, + 'category_name', + NEW.category_name); + resolved_body := merchant.replace_placeholder(resolved_body, + 'merchant_serial', + my_merchant_serial::TEXT); -- Insert into pending webhooks for this webhook INSERT INTO merchant.merchant_pending_webhooks @@ -75,28 +74,27 @@ BEGIN url, http_method, body_template - FROM merchant.merchant_webhook + 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, + resolved_body := merchant.replace_placeholder(resolved_body, 'webhook_type', 'category_updated'); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'category_serial', NEW.category_serial::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_category_name', OLD.category_name); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'category_name', NEW.category_name); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'category_name_i18n', NEW.category_name_i18n::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_category_name_i18n', OLD.category_name_i18n::TEXT); @@ -123,19 +121,18 @@ BEGIN url, http_method, body_template - FROM merchant.merchant_webhook + 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, + resolved_body := merchant.replace_placeholder(resolved_body, 'webhook_type', 'category_deleted'); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'category_serial', OLD.category_serial::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'category_name', OLD.category_name); diff --git a/src/backenddb/pg_do_handle_inventory_changes.sql b/src/backenddb/pg_do_handle_inventory_changes.sql @@ -18,12 +18,9 @@ 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 @@ -33,58 +30,57 @@ BEGIN url, http_method, body_template - FROM merchant.merchant_webhook + 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, + resolved_body := merchant.replace_placeholder(resolved_body, 'webhook_type', 'inventory_added'); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'product_serial', NEW.product_serial::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'product_id', NEW.product_id); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'description', NEW.description); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'description_i18n', NEW.description_i18n::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'unit', NEW.unit); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'image', NEW.image); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'taxes', NEW.taxes::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'price', NEW.price_array[1]::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'unit_price', NEW.price_array::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_stock', NEW.total_stock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_sold', NEW.total_sold::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_lost', NEW.total_lost::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'address', NEW.address::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'next_restock', NEW.next_restock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'minimum_age', NEW.minimum_age::TEXT); @@ -111,97 +107,96 @@ BEGIN url, http_method, body_template - FROM merchant.merchant_webhook + 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, + resolved_body := merchant.replace_placeholder(resolved_body, 'webhook_type', 'inventory_updated'); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'product_serial', NEW.product_serial::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'product_id', NEW.product_id); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_description', OLD.description); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'description', NEW.description); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_description_i18n', OLD.description_i18n::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'description_i18n', NEW.description_i18n::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_unit', OLD.unit); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'unit', NEW.unit); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_image', OLD.image); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'image', NEW.image); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_taxes', OLD.taxes::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'taxes', NEW.taxes::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_price', OLD.price_array[1]::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_unit_price', OLD.price_array::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'price', NEW.price_array[1]::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'unit_price', NEW.price_array::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_total_stock', OLD.total_stock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_stock', NEW.total_stock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_total_sold', OLD.total_sold::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_sold', NEW.total_sold::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_total_lost', OLD.total_lost::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_lost', NEW.total_lost::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_address', OLD.address::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'address', NEW.address::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_next_restock', OLD.next_restock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'next_restock', NEW.next_restock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'old_minimum_age', OLD.minimum_age::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'minimum_age', NEW.minimum_age::TEXT); @@ -228,58 +223,57 @@ BEGIN url, http_method, body_template - FROM merchant.merchant_webhook + 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, + resolved_body := merchant.replace_placeholder(resolved_body, 'webhook_type', 'inventory_deleted'); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'product_serial', OLD.product_serial::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'product_id', OLD.product_id); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'description', OLD.description); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'description_i18n', OLD.description_i18n::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'unit', OLD.unit); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'image', OLD.image); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'taxes', OLD.taxes::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'price', OLD.price_array[1]::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'unit_price', OLD.price_array::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_stock', OLD.total_stock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_sold', OLD.total_sold::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'total_lost', OLD.total_lost::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'address', OLD.address::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'next_restock', OLD.next_restock::TEXT); - resolved_body := replace_placeholder(resolved_body, + resolved_body := merchant.replace_placeholder(resolved_body, 'minimum_age', OLD.minimum_age::TEXT); diff --git a/src/backenddb/pg_fixup_instance_schema.sql b/src/backenddb/pg_fixup_instance_schema.sql @@ -0,0 +1,48 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.fixup_instance_schema(BIGINT); +CREATE FUNCTION merchant.fixup_instance_schema( + in_min_version BIGINT +) + RETURNS void + LANGUAGE plpgsql +AS $$ +DECLARE + rec RECORD; + r RECORD; + my_schema_name TEXT; +BEGIN + FOR rec IN + SELECT merchant_serial + FROM merchant.merchant_instances + LOOP + my_schema_name := 'merchant_instance_' || rec.merchant_serial::TEXT; + -- Call fixup functions + FOR r IN + SELECT migration_name + FROM merchant.instance_fixups + WHERE version >= in_min_version + ORDER BY version ASC + LOOP + EXECUTE format('CALL %I(%I)', r.migration_name, my_schema_name); + END LOOP; + END LOOP; +END $$; + +COMMENT ON FUNCTION merchant.create_instance_schema(BIGINT) + IS 'Updates all schema to the given version'; + diff --git a/src/backenddb/pg_inactivate_account.sql b/src/backenddb/pg_inactivate_account.sql @@ -1,49 +0,0 @@ --- --- 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/> --- - -DROP FUNCTION IF EXISTS merchant_do_inactivate_account; -CREATE FUNCTION merchant_do_inactivate_account ( - IN in_instance_name TEXT - ,IN in_h_wire BYTEA - ,OUT out_found BOOL -) -LANGUAGE plpgsql -AS $$ -DECLARE - my_instance INT8; -BEGIN - SELECT merchant_serial - INTO my_instance - FROM merchant_instances - WHERE merchant_id=in_instance_name; - IF NOT FOUND - THEN - out_found = FALSE; - RETURN; - END IF; - UPDATE merchant_accounts - SET active=FALSE - WHERE h_wire=in_h_wire - AND merchant_serial=my_instance; - out_found = FOUND; - IF out_found - THEN - -- Notify taler-merchant-kyccheck about the change in - -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; - END IF; - -END $$; diff --git a/src/backenddb/pg_increment_money_pots.sql b/src/backenddb/pg_increment_money_pots.sql @@ -1,109 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2025 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/> --- - -SET search_path TO merchant; - -DROP FUNCTION IF EXISTS merchant_do_increment_money_pots; -CREATE FUNCTION merchant_do_increment_money_pots ( - IN in_instance_id TEXT, - IN ina_money_pots_ids INT8[], - IN ina_increments taler_amount_currency[], - OUT out_not_found BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - i INT; - ini_current_pot_id INT8; - ini_current_increment taler_amount_currency; - my_totals taler_amount_currency[]; - currency_found BOOL; - j INT; -BEGIN - -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - -IF NOT FOUND -THEN - out_not_found = TRUE; - RETURN; -END IF; - -out_not_found = FALSE; - -IF ( COALESCE(array_length(ina_money_pots_ids, 1), 0) != - COALESCE(array_length(ina_increments, 1), 0) ) -THEN - RAISE EXCEPTION 'Array lengths must match'; -END IF; - -FOR i IN 1..COALESCE(array_length(ina_money_pots_ids, 1),0) - LOOP - ini_current_pot_id = ina_money_pots_ids[i]; - ini_current_increment = ina_increments[i]; - - SELECT pot_totals - INTO my_totals - FROM merchant_money_pots - WHERE money_pot_serial = ini_current_pot_id - AND merchant_serial = my_merchant_id; - - IF NOT FOUND - THEN - -- If pot does not exist, we just ignore the entire - -- requested increment, but update the return value. - -- (We may have other pots to update, so we continue - -- to iterate!). - out_not_found = TRUE; - ELSE - -- Check if currency exists in pot_totals and update - currency_found = FALSE; - - FOR j IN 1..COALESCE(array_length(my_totals, 1), 0) - LOOP - IF (my_totals[j]).curr = (ini_current_increment).curr - THEN - my_totals[j].frac - = my_totals[j].frac + ini_current_increment.frac; - my_totals[j].val - = my_totals[j].val + ini_current_increment.val; - IF my_totals[j].frac >= 100000000 - THEN - my_totals[j].frac = my_totals[j].frac - 100000000; - my_totals[j].val = my_totals[j].val + 1; - END IF; - currency_found = TRUE; - EXIT; -- break out of loop - END IF; - END LOOP; - - IF NOT currency_found - THEN - my_totals = array_append(my_totals, ini_current_increment); - END IF; - - UPDATE merchant_money_pots - SET pot_totals = my_totals - WHERE money_pot_serial = ini_current_pot_id - AND merchant_serial = my_merchant_id; - - END IF; - END LOOP; - -END $$; diff --git a/src/backenddb/pg_insert_deposit_confirmation.sql b/src/backenddb/pg_insert_deposit_confirmation.sql @@ -1,171 +0,0 @@ --- --- 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/> --- - -DROP FUNCTION IF EXISTS merchant_do_insert_deposit_confirmation; -CREATE FUNCTION merchant_do_insert_deposit_confirmation ( - IN in_instance_id TEXT, - IN in_h_contract_terms BYTEA, - IN in_deposit_timestamp INT8, - IN in_exchange_url TEXT, - IN in_total_without_fee taler_amount_currency, - IN in_wire_fee taler_amount_currency, - IN in_h_wire BYTEA, - IN in_exchange_sig BYTEA, - IN in_exchange_pub BYTEA, - IN in_wire_transfer_deadline INT8, - IN in_notify_arg_str TEXT, - OUT out_no_instance BOOL, - OUT out_no_order BOOL, - OUT out_no_account BOOL, - OUT out_no_signkey BOOL, - OUT out_conflict BOOL, - OUT out_deposit_confirmation_serial INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_order_serial INT8; - my_account_serial INT8; - my_signkey_serial INT8; - my_record RECORD; - my_bank_serial_id INT8; - my_credit_amount taler_amount_currency; -BEGIN - -out_no_instance=TRUE; -out_no_order=TRUE; -out_no_account=TRUE; -out_no_signkey=TRUE; -out_conflict=FALSE; -out_deposit_confirmation_serial=0; - --- Which instance are we using? -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; -IF NOT FOUND -THEN - RETURN; -END IF; -out_no_instance=FALSE; - -SELECT account_serial - INTO my_account_serial - FROM merchant_accounts - WHERE merchant_serial=my_merchant_id - AND h_wire=in_h_wire; -IF NOT FOUND -THEN - RETURN; -END IF; -out_no_account=FALSE; - -SELECT signkey_serial - INTO my_signkey_serial - FROM merchant_exchange_signing_keys - WHERE exchange_pub=in_exchange_pub - ORDER BY start_date DESC - LIMIT 1; -IF NOT FOUND -THEN - RETURN; -END IF; -out_no_signkey=FALSE; - -SELECT order_serial - INTO my_order_serial - FROM merchant_contract_terms - WHERE merchant_serial=my_merchant_id - AND h_contract_terms=in_h_contract_terms; -IF NOT FOUND -THEN - RETURN; -END IF; -out_no_order=FALSE; - -SELECT deposit_confirmation_serial - ,deposit_timestamp - ,exchange_url - ,total_without_fee - ,wire_fee - ,wire_transfer_deadline - ,account_serial - INTO my_record - FROM merchant_deposit_confirmations - WHERE order_serial=my_order_serial - AND exchange_url=in_exchange_url; -IF NOT FOUND -THEN - INSERT INTO merchant_deposit_confirmations - (order_serial - ,deposit_timestamp - ,exchange_url - ,total_without_fee - ,wire_fee - ,exchange_sig - ,wire_transfer_deadline - ,signkey_serial - ,account_serial - ) VALUES ( - my_order_serial - ,in_deposit_timestamp - ,in_exchange_url - ,in_total_without_fee - ,in_wire_fee - ,in_exchange_sig - ,in_wire_transfer_deadline - ,my_signkey_serial - ,my_account_serial - ) RETURNING deposit_confirmation_serial - INTO out_deposit_confirmation_serial; -ELSE - out_deposit_confirmation_serial = my_record.deposit_confirmation_serial; - IF (in_deposit_timestamp, - in_wire_transfer_deadline, - in_wire_fee, - my_account_serial) - IS DISTINCT FROM - (my_record.deposit_timestamp, - my_record.wire_transfer_deadline, - my_record.wire_fee, - my_record.account_serial) - THEN - out_conflict = TRUE; - RETURN; - END IF; - IF ( ((in_total_without_fee).val < (my_record.total_without_fee).val) OR - ( ((in_total_without_fee).val = (my_record.total_without_fee).val) AND - ((in_total_without_fee).frac <= (my_record.total_without_fee).frac) ) ) - THEN - -- new amount smaller or did not change, do NOT update. - RETURN; - END IF; - - -- Same deposit, but total amount increased, store this! - UPDATE merchant_deposit_confirmations - SET total_without_fee = in_total_without_fee - ,exchange_sig = in_exchange_sig - ,signkey_serial = my_signkey_serial - WHERE deposit_confirmation_serial = my_record.deposit_confirmation_serial; - -END IF; - --- Do notify on TALER_DBEVENT_MERCHANT_NEW_WIRE_DEADLINE -PERFORM pg_notify ('XBZ19D98AK2REYNX93F736A56MT14SCY2EEX7XNXQMNCQ01B121R0', - in_notify_arg_str); - -END $$; diff --git a/src/backenddb/pg_insert_deposit_to_transfer.sql b/src/backenddb/pg_insert_deposit_to_transfer.sql @@ -1,136 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024, 2025 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/> --- - - -DROP FUNCTION IF EXISTS merchant_insert_deposit_to_transfer; -CREATE FUNCTION merchant_insert_deposit_to_transfer ( - IN in_deposit_serial INT8, - IN in_coin_contribution taler_amount_currency, - IN in_execution_time INT8, - IN in_exchange_url TEXT, - IN in_h_wire BYTEA, - IN in_exchange_sig BYTEA, - IN in_exchange_pub BYTEA, - IN in_wtid BYTEA, - OUT out_dummy BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_signkey_serial INT8; - my_account_serial INT8; - my_decose INT8; - my_expected_credit_serial INT8; - my_wire_pending_cleared BOOL; -BEGIN - -- Just to return something (for now). - out_dummy=FALSE; - --- Find exchange sign key -SELECT signkey_serial - INTO my_signkey_serial - FROM merchant_exchange_signing_keys - WHERE exchange_pub=in_exchange_pub - ORDER BY start_date DESC - LIMIT 1; - -IF NOT FOUND -THEN - -- Maybe 'keys' is outdated, try again in 8 hours. - UPDATE merchant_deposits - SET settlement_last_ec=2029 -- MERCHANT_EXCHANGE_SIGN_PUB_UNKNOWN - ,settlement_last_http_status=200 - ,settlement_last_detail=ENCODE(in_exchange_pub, 'hex') - ,settlement_wtid=in_wtid - ,settlement_retry_needed=TRUE - ,settlement_retry_time=(EXTRACT(epoch FROM (CURRENT_TIME + interval '8 hours')) * 1000000)::INT8 - WHERE deposit_serial=in_deposit_serial; - RETURN; -END IF; - --- Find deposit confirmation -SELECT deposit_confirmation_serial - INTO my_decose - FROM merchant_deposits - WHERE deposit_serial=in_deposit_serial; - --- Find merchant account -SELECT account_serial - INTO my_account_serial - FROM merchant_deposit_confirmations mdc - JOIN merchant_accounts ma - USING (account_serial) - WHERE mdc.deposit_confirmation_serial=my_decose - AND ma.h_wire=in_h_wire; - -IF NOT FOUND -THEN - -- Merchant account referenced in exchange response is unknown to us. - -- Remember fatal error and do not try again. - UPDATE merchant_deposits - SET settlement_last_ec=2558 -- MERCHANT_EXCHANGE_TRANSFERS_TARGET_ACCOUNT_UNKNOWN - ,settlement_last_http_status=200 - ,settlement_last_detail=ENCODE(in_h_wire, 'hex') - ,settlement_wtid=in_wtid - ,settlement_retry_needed=FALSE - ,settlement_coin_contribution=in_coin_contribution - ,signkey_serial=my_signkey_serial - ,settlement_exchange_sig=in_exchange_sig - WHERE deposit_serial=in_deposit_serial; - RETURN; -END IF; - - --- Make sure wire transfer is expected. -SELECT expected_credit_serial - INTO my_expected_credit_serial - FROM merchant_expected_transfers - WHERE wtid=in_wtid - AND exchange_url=in_exchange_url - AND account_serial=my_account_serial; - -IF NOT FOUND -THEN - INSERT INTO merchant_expected_transfers - (exchange_url - ,wtid - ,account_serial - ,expected_time) - VALUES - (in_exchange_url - ,in_wtid - ,my_account_serial - ,in_execution_time) - RETURNING expected_credit_serial - INTO my_expected_credit_serial; -END IF; - --- Finally, update merchant_deposits so we do not try again. -UPDATE merchant_deposits - SET settlement_last_ec=0 - ,settlement_last_http_status=200 - ,settlement_last_detail=NULL - ,settlement_wtid=in_wtid - ,settlement_retry_needed=FALSE - ,settlement_coin_contribution=in_coin_contribution - ,settlement_expected_credit_serial=my_expected_credit_serial - ,signkey_serial=my_signkey_serial - ,settlement_exchange_sig=in_exchange_sig - WHERE deposit_serial=in_deposit_serial; - --- MERCHANT_WIRE_TRANSFER_EXPECTED -NOTIFY XR6849FMRD2AJFY1E2YY0GWA8GN0YT407Z66WHJB0SAKJWF8G2Q60; - -END $$; diff --git a/src/backenddb/pg_insert_product.sql b/src/backenddb/pg_insert_product.sql @@ -1,246 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024, 2025 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/> --- - -DROP FUNCTION IF EXISTS merchant_do_insert_product; -CREATE FUNCTION merchant_do_insert_product ( - IN in_instance_id TEXT, - IN in_product_id TEXT, - IN in_description TEXT, - IN in_description_i18n JSONB, -- $4 - IN in_unit TEXT, - IN in_image TEXT, - IN in_taxes JSONB, -- $7 - IN ina_price_list taler_amount_currency[], - IN in_total_stock INT8, -- $9 - IN in_total_stock_frac INT4, --$10 - IN in_allow_fractional_quantity BOOL, - IN in_fractional_precision_level INT4, - IN in_address JSONB, -- $13 - IN in_next_restock INT8, - IN in_minimum_age INT4, - IN ina_categories INT8[], -- $16 - IN in_product_name TEXT, - IN in_product_group_id INT8, -- NULL for default - IN in_money_pot_id INT8, -- NULL for none - IN in_price_is_net BOOL, -- $20 - OUT out_no_instance BOOL, - OUT out_conflict BOOL, - OUT out_no_cat INT8, - OUT out_no_group BOOL, - OUT out_no_pot BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_product_serial INT8; - i INT8; - ini_cat INT8; -BEGIN - -out_no_group = FALSE; -out_no_pot = FALSE; - --- Which instance are we using? -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - -IF NOT FOUND -THEN - out_no_instance=TRUE; - out_conflict=FALSE; - out_no_cat=NULL; - RETURN; -END IF; -out_no_instance=FALSE; - -IF in_product_group_id IS NOT NULL -THEN - PERFORM FROM merchant_product_groups - WHERE product_group_serial=in_product_group_id - AND merchant_serial=my_merchant_id; - IF NOT FOUND - THEN - out_no_group=TRUE; - out_conflict=FALSE; - out_no_cat=NULL; - RETURN; - END IF; -END IF; - -IF in_money_pot_id IS NOT NULL -THEN - PERFORM FROM merchant_money_pots - WHERE money_pot_serial=in_money_pot_id - AND merchant_serial=my_merchant_id; - IF NOT FOUND - THEN - out_no_pot=TRUE; - out_conflict=FALSE; - out_no_cat=NULL; - RETURN; - END IF; -END IF; - -INSERT INTO merchant_inventory - (merchant_serial - ,product_id - ,product_name - ,description - ,description_i18n - ,unit - ,image - ,image_hash - ,taxes - ,price_array - ,total_stock - ,total_stock_frac - ,allow_fractional_quantity - ,fractional_precision_level - ,address - ,next_restock - ,minimum_age - ,product_group_serial - ,money_pot_serial - ,price_is_net -) VALUES ( - my_merchant_id - ,in_product_id - ,in_product_name - ,in_description - ,in_description_i18n - ,in_unit - ,in_image - ,CASE - WHEN (in_image IS NULL) OR (in_image = '') - THEN NULL - ELSE encode(public.digest(convert_to(in_image, 'UTF8'), - 'sha256'), - 'hex') - END - ,in_taxes - ,ina_price_list - ,in_total_stock - ,in_total_stock_frac - ,in_allow_fractional_quantity - ,in_fractional_precision_level - ,in_address - ,in_next_restock - ,in_minimum_age - ,in_product_group_id - ,in_money_pot_id - ,in_price_is_net - ) -ON CONFLICT (merchant_serial, product_id) DO NOTHING - RETURNING product_serial - INTO my_product_serial; - - -IF NOT FOUND -THEN - -- Check for idempotency - SELECT product_serial - INTO my_product_serial - FROM merchant_inventory - WHERE merchant_serial=my_merchant_id - AND product_id=in_product_id - AND product_name=in_product_name - AND description=in_description - AND description_i18n=in_description_i18n - AND unit=in_unit - AND image=in_image - AND taxes=in_taxes - AND to_jsonb(COALESCE(price_array, ARRAY[]::taler_amount_currency[])) - = to_jsonb(COALESCE(ina_price_list, ARRAY[]::taler_amount_currency[])) -- FIXME: wild. Why so complicated? - AND total_stock=in_total_stock - AND total_stock_frac=in_total_stock_frac - AND allow_fractional_quantity=in_allow_fractional_quantity - AND fractional_precision_level=in_fractional_precision_level - AND address=in_address - AND next_restock=in_next_restock - AND minimum_age=in_minimum_age - AND product_group_serial IS NOT DISTINCT FROM in_product_group_id - AND money_pot_serial IS NOT DISTINCT FROM in_money_pot_id - AND price_is_net=in_price_is_net; - IF NOT FOUND - THEN - out_conflict=TRUE; - out_no_cat=NULL; - RETURN; - END IF; - - -- Check categories match as well - FOR i IN 1..COALESCE(array_length(ina_categories,1),0) - LOOP - ini_cat=ina_categories[i]; - - PERFORM - FROM merchant_product_categories - WHERE product_serial=my_product_serial - AND category_serial=ini_cat; - IF NOT FOUND - THEN - out_conflict=TRUE; - out_no_cat=NULL; - RETURN; - END IF; - END LOOP; - - -- Also check there are no additional categories - -- in either set. - SELECT COUNT(*) - INTO i - FROM merchant_product_categories - WHERE product_serial=my_product_serial; - IF i != COALESCE(array_length(ina_categories,1),0) - THEN - out_conflict=TRUE; - out_no_cat=NULL; - RETURN; - END IF; - - -- Is idempotent! - out_conflict=FALSE; - out_no_cat=NULL; - RETURN; -END IF; -out_conflict=FALSE; - - --- Add categories -FOR i IN 1..COALESCE(array_length(ina_categories,1),0) -LOOP - ini_cat=ina_categories[i]; - - INSERT INTO merchant_product_categories - (product_serial - ,category_serial) - VALUES - (my_product_serial - ,ini_cat) - ON CONFLICT DO NOTHING; - - IF NOT FOUND - THEN - out_no_cat=i; - RETURN; - END IF; -END LOOP; - --- Success! -out_no_cat=NULL; -END $$; diff --git a/src/backenddb/pg_insert_transfer.sql b/src/backenddb/pg_insert_transfer.sql @@ -1,122 +0,0 @@ --- --- 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/> --- - -DROP FUNCTION IF EXISTS merchant_do_insert_transfer; -CREATE FUNCTION merchant_do_insert_transfer ( - IN in_instance_id TEXT, - IN in_exchange_url TEXT, - IN in_wtid BYTEA, - IN in_credit_amount taler_amount_currency, - IN in_credited_account_payto TEXT, - IN in_bank_serial_id INT8, -- can be NULL if unknown - IN in_execution_time INT8, - OUT out_no_instance BOOL, - OUT out_no_account BOOL, - OUT out_conflict BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_account_serial INT8; - my_record RECORD; - my_bank_serial_id INT8; - my_credit_amount taler_amount_currency; -BEGIN - -out_conflict=FALSE; - --- Which instance are we using? -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; -IF NOT FOUND -THEN - out_no_instance=TRUE; - out_no_account=TRUE; -- also true... - RETURN; -END IF; -out_no_instance=FALSE; - -SELECT account_serial - INTO my_account_serial - FROM merchant_accounts - WHERE REGEXP_REPLACE(payto_uri, - '\\?.*','') - =REGEXP_REPLACE(in_credited_account_payto, - '\\?.*','') - AND merchant_serial=my_merchant_id; -IF NOT FOUND -THEN - out_no_account=TRUE; - RETURN; -END IF; -out_no_account=FALSE; - -SELECT bank_serial_id - ,credit_amount - INTO my_record - FROM merchant_transfers - WHERE wtid=in_wtid - AND account_serial=my_account_serial - AND exchange_url=in_exchange_url; -IF NOT FOUND -THEN - INSERT INTO merchant_transfers - (exchange_url - ,wtid - ,credit_amount - ,account_serial - ,bank_serial_id - ,execution_time - ) VALUES - (in_exchange_url - ,in_wtid - ,in_credit_amount - ,my_account_serial - ,in_bank_serial_id - ,in_execution_time); - -- Do notify on TALER_DBEVENT_MERCHANT_WIRE_TRANSFER_CONFIRMED - NOTIFY XJ5N652FA4TBS2WXGY3S1FMPMQYTD8KAZA9B7HW9JWJ4PZ2DB852G; - RETURN; -END IF; - -my_bank_serial_id = my_record.bank_serial_id; -my_credit_amount = my_record.credit_amount; - -IF ( (in_credit_amount.val != my_credit_amount.val) OR - (in_credit_amount.frac != my_credit_amount.frac) OR - (in_credit_amount.curr != my_credit_amount.curr) ) -THEN - out_conflict = TRUE; -- amounts differ, not OK! - RETURN; -END IF; - -IF ( (my_bank_serial_id IS NULL) AND - (in_bank_serial_id IS NOT NULL) ) -THEN - -- We learned the bank_bank_serial_id, update that - UPDATE merchant_transfers - SET bank_serial_id=in_bank_serial_id - WHERE wtid=in_wtid - AND account_serial=my_account_serial - AND exchange_url=in_exchange_url; - RETURN; -END IF; - --- idempotent request, success. - -END $$; diff --git a/src/backenddb/pg_insert_transfer_details.sql b/src/backenddb/pg_insert_transfer_details.sql @@ -1,269 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024, 2025 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_insert_transfer_details; -CREATE FUNCTION merchant_do_insert_transfer_details ( - IN in_instance_id TEXT, - IN in_exchange_url TEXT, - IN in_payto_uri TEXT, - IN in_wtid BYTEA, - IN in_execution_time INT8, - IN in_exchange_pub BYTEA, - IN in_exchange_sig BYTEA, - IN in_total_amount taler_amount_currency, - IN in_wire_fee taler_amount_currency, - IN ina_coin_values taler_amount_currency[], - IN ina_deposit_fees taler_amount_currency[], - IN ina_coin_pubs BYTEA[], - IN ina_contract_terms BYTEA[], - OUT out_no_instance BOOL, - OUT out_no_account BOOL, - OUT out_no_exchange BOOL, - OUT out_duplicate BOOL, - OUT out_conflict BOOL, - OUT out_order_id TEXT, - OUT out_merchant_pub BYTEA) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_signkey_serial INT8; - my_expected_credit_serial INT8; - my_affected_orders RECORD; - my_merchant_serial INT8; - my_decose INT8; - my_order_id TEXT; - i INT8; - curs CURSOR (arg_coin_pub BYTEA) FOR - SELECT mcon.deposit_confirmation_serial, - mcon.order_serial - FROM merchant_deposits dep - JOIN merchant_deposit_confirmations mcon - USING (deposit_confirmation_serial) - WHERE dep.coin_pub=arg_coin_pub; - ini_coin_pub BYTEA; - ini_contract_term BYTEA; - ini_coin_value taler_amount_currency; - ini_deposit_fee taler_amount_currency; -BEGIN - --- Which instance are we using? -SELECT merchant_serial, merchant_pub - INTO my_merchant_id, out_merchant_pub - FROM merchant_instances - WHERE merchant_id=in_instance_id; -IF NOT FOUND -THEN - out_no_instance=TRUE; - out_no_account=FALSE; - out_no_exchange=FALSE; - out_duplicate=FALSE; - out_conflict=FALSE; - RETURN; -END IF; -out_no_instance=FALSE; - --- Determine account that was credited. -SELECT expected_credit_serial - INTO my_expected_credit_serial - FROM merchant_expected_transfers - WHERE exchange_url=in_exchange_url - AND wtid=in_wtid - AND account_serial= - (SELECT account_serial - FROM merchant_accounts - WHERE payto_uri=in_payto_uri - AND merchant_serial=my_merchant_id); - -IF NOT FOUND -THEN - out_no_account=TRUE; - out_no_exchange=FALSE; - out_duplicate=FALSE; - out_conflict=FALSE; - RETURN; -END IF; -out_no_account=FALSE; - --- Find exchange sign key -SELECT signkey_serial - INTO my_signkey_serial - FROM merchant_exchange_signing_keys - WHERE exchange_pub=in_exchange_pub - ORDER BY start_date DESC - LIMIT 1; - -IF NOT FOUND -THEN - out_no_exchange=TRUE; - out_conflict=FALSE; - out_duplicate=FALSE; - RETURN; -END IF; -out_no_exchange=FALSE; - --- Add signature first, check for idempotent request -INSERT INTO merchant_transfer_signatures - (expected_credit_serial - ,signkey_serial - ,credit_amount - ,wire_fee - ,execution_time - ,exchange_sig) - VALUES - (my_expected_credit_serial - ,my_signkey_serial - ,in_total_amount - ,in_wire_fee - ,in_execution_time - ,in_exchange_sig) - ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - PERFORM 1 - FROM merchant_transfer_signatures - WHERE expected_credit_serial=my_expected_credit_serial - AND signkey_serial=my_signkey_serial - AND credit_amount=in_total_amount - AND wire_fee=in_wire_fee - AND execution_time=in_execution_time - AND exchange_sig=in_exchange_sig; - IF FOUND - THEN - -- duplicate case - out_duplicate=TRUE; - out_conflict=FALSE; - RETURN; - END IF; - -- conflict case - out_duplicate=FALSE; - out_conflict=TRUE; - RETURN; -END IF; - -out_duplicate=FALSE; -out_conflict=FALSE; - - -FOR i IN 1..array_length(ina_coin_pubs,1) -LOOP - ini_coin_value=ina_coin_values[i]; - ini_deposit_fee=ina_deposit_fees[i]; - ini_coin_pub=ina_coin_pubs[i]; - ini_contract_term=ina_contract_terms[i]; - - INSERT INTO merchant_expected_transfer_to_coin - (deposit_serial - ,expected_credit_serial - ,offset_in_exchange_list - ,exchange_deposit_value - ,exchange_deposit_fee) - SELECT - dep.deposit_serial - ,my_expected_credit_serial - ,i - ,ini_coin_value - ,ini_deposit_fee - FROM merchant_deposits dep - JOIN merchant_deposit_confirmations dcon - USING (deposit_confirmation_serial) - JOIN merchant_contract_terms cterm - USING (order_serial) - WHERE dep.coin_pub=ini_coin_pub - AND cterm.h_contract_terms=ini_contract_term - AND cterm.merchant_serial=my_merchant_id; - - RAISE NOTICE 'iterating over affected orders'; - OPEN curs (arg_coin_pub:=ini_coin_pub); - LOOP - FETCH NEXT FROM curs INTO my_affected_orders; - EXIT WHEN NOT FOUND; - - RAISE NOTICE 'checking affected order for completion'; - - my_decose=my_affected_orders.deposit_confirmation_serial; - - PERFORM FROM merchant_deposits md - WHERE md.deposit_confirmation_serial=my_decose - AND (settlement_retry_needed - OR (settlement_wtid IS NULL) ); - IF NOT FOUND - THEN - -- must be all done, clear flag - UPDATE merchant_deposit_confirmations - SET wire_pending=FALSE - WHERE (deposit_confirmation_serial=my_decose); - - IF FOUND - THEN - -- Also update contract terms, if all (other) associated - -- deposit_confirmations are also done. - - RAISE NOTICE 'checking affected contract for completion'; - PERFORM FROM merchant_deposit_confirmations mdc - WHERE mdc.wire_pending - AND mdc.order_serial=my_affected_orders.order_serial; - IF NOT FOUND - THEN - - UPDATE merchant_contract_terms - SET wired=TRUE - WHERE (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; - out_order_id = my_order_id; - -- Insert pending webhook if it exists - INSERT INTO merchant_pending_webhooks - (merchant_serial - ,webhook_serial - ,url - ,http_method - ,header - ,body) - SELECT mw.merchant_serial - ,mw.webhook_serial - ,mw.url - ,mw.http_method - ,replace_placeholder( - replace_placeholder(mw.header_template, 'order_id', my_order_id), - 'wtid', encode(in_wtid, 'hex') - )::TEXT - ,replace_placeholder( - replace_placeholder(mw.body_template, 'order_id', my_order_id), - 'wtid', encode(in_wtid, 'hex') - )::TEXT - FROM merchant_webhook mw - WHERE mw.event_type = 'order_settled' - AND mw.merchant_serial = my_merchant_serial; - IF FOUND - THEN - NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG; - END IF; -- found pending order_settled webhooks to deliver - END IF; -- no more merchant_deposits waiting for wire_pending - END IF; -- did clear wire_pending flag for deposit confirmation - END IF; -- no more merchant_deposits wait for settlement - - END LOOP; -- END curs LOOP - CLOSE curs; -END LOOP; -- END FOR loop - -END $$; diff --git a/src/backenddb/pg_insert_unclaim_signature.sql b/src/backenddb/pg_insert_unclaim_signature.sql @@ -1,85 +0,0 @@ --- --- 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_insert_unclaim_signature; -CREATE FUNCTION merchant_do_insert_unclaim_signature ( - IN in_instance_id TEXT, - IN in_order_id TEXT, - IN in_nonce_str TEXT, - IN in_notify_str TEXT, - IN in_h_contract_terms BYTEA, - IN in_nonce_sig BYTEA, - OUT out_found BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_serial INT8; - my_expiration_time INT8; -BEGIN - - SELECT merchant_serial - INTO my_merchant_serial - FROM merchant_instances - WHERE merchant_id=in_instance_id; - - IF NOT FOUND - THEN - out_found = FALSE; - RETURN; - END IF; - - SELECT pay_deadline - INTO my_expiration_time - FROM merchant_contract_terms - WHERE order_id=in_order_id - AND merchant_serial = my_merchant_serial - AND contract_terms->>'nonce' = in_nonce_str; - - IF NOT FOUND - THEN - out_found = FALSE; - RETURN; - END IF; - - INSERT INTO merchant_unclaim_signatures - (h_contract_terms - ,unclaim_sig - ,expiration_time) - VALUES - (in_h_contract_terms - ,in_nonce_sig - ,my_expiration_time) - ON CONFLICT DO NOTHING; - - IF FOUND - THEN - out_found = TRUE; - - -- order status change notification - EXECUTE FORMAT ( - 'NOTIFY %s' - ,in_notify_str); - - RETURN; - END IF; - - PERFORM FROM merchant_unclaim_signatures - WHERE h_contract_terms = in_h_contract_terms - AND unclaim_sig = in_nonce_sig; - out_found = FOUND; - -END $$; diff --git a/src/backenddb/pg_interval_to_start.sql b/src/backenddb/pg_interval_to_start.sql @@ -0,0 +1,29 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- +DROP FUNCTION IF EXISTS merchant.interval_to_start; +CREATE OR REPLACE FUNCTION merchant.interval_to_start ( + IN in_timestamp TIMESTAMP, + IN in_range merchant.statistic_range, + OUT out_bucket_start INT8 +) +LANGUAGE plpgsql +AS $$ +BEGIN + out_bucket_start = EXTRACT(EPOCH FROM DATE_TRUNC(in_range::text, in_timestamp)); +END $$; + +COMMENT ON FUNCTION merchant.interval_to_start + IS 'computes the start time of the bucket for an event at the current time given the desired bucket range'; diff --git a/src/backenddb/pg_merchant_send_kyc_notification.sql b/src/backenddb/pg_merchant_send_kyc_notification.sql @@ -48,7 +48,7 @@ BEGIN ,notification_language INTO my_email ,my_notification_language - FROM merchant_instances + FROM merchant.merchant_instances WHERE merchant_serial=my_instance_serial; IF NOT FOUND THEN diff --git a/src/backenddb/pg_replace_placeholder.sql b/src/backenddb/pg_replace_placeholder.sql @@ -0,0 +1,41 @@ +-- +-- 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/> +-- + + +DROP FUNCTION IF EXISTS replace_placeholder; +CREATE FUNCTION replace_placeholder( + template TEXT, + key TEXT, + value TEXT +) + RETURNS TEXT + LANGUAGE plpgsql + IMMUTABLE + STRICT + PARALLEL SAFE +AS $$ +BEGIN + RETURN regexp_replace( + template, + '{{\s*' || key || '\s*}}', -- Match the key with optional spaces + value, + 'g' -- Global replacement + ); +END; +$$; + +COMMENT ON FUNCTION replace_placeholder(TEXT,TEXT,TEXT) + IS 'Function to replace placeholders in a string with a given value'; diff --git a/src/backenddb/pg_select_accounts.sql b/src/backenddb/pg_select_accounts.sql @@ -0,0 +1,73 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.select_accounts; +CREATE FUNCTION merchant.select_accounts() +RETURNS TABLE( + out_merchant_id TEXT, + out_merchant_priv BYTEA, + out_h_wire BYTEA, + out_salt BYTEA, + out_payto_uri TEXT, + out_credit_facade_url TEXT, + out_credit_facade_credentials JSONB, + out_extra_wire_subject_metadata TEXT, + out_active BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + rec RECORD; +BEGIN + FOR rec IN + SELECT mi.merchant_serial + ,mi.merchant_id + ,mi.merchant_priv + FROM merchant.merchant_instances mi + LOOP + BEGIN + FOR out_h_wire, + out_salt, + out_payto_uri, + out_credit_facade_url, + out_credit_facade_credentials, + out_extra_wire_subject_metadata, + out_active + IN EXECUTE format('SELECT' + ' h_wire' + ',salt' + ',payto_uri' + ',credit_facade_url' + ',credit_facade_credentials' + ',extra_wire_subject_metadata' + ',active' + ' FROM %I.merchant_accounts', + 'merchant_instance_' || rec.merchant_serial::TEXT) + LOOP + out_merchant_id := rec.merchant_id; + out_merchant_priv := rec.merchant_priv; + RETURN NEXT; + END LOOP; + EXCEPTION + WHEN undefined_table THEN + NULL; + END; + END LOOP; +END $$; + +COMMENT ON FUNCTION merchant.select_accounts() + IS 'Returns one row per merchant_account across all (or one) instance schemas;' + ' merchant_priv is read from merchant.merchant_instances (NULL if absent).' + ' If in_merchant_id is non-NULL, only that instance is scanned.'; diff --git a/src/backenddb/pg_solve_mfa_challenge.sql b/src/backenddb/pg_solve_mfa_challenge.sql @@ -1,84 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2025 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/> --- - -DROP FUNCTION IF EXISTS merchant_do_solve_mfa_challenge; -CREATE FUNCTION merchant_do_solve_mfa_challenge ( - IN in_challenge_id INT8, - IN in_h_body BYTEA, - IN in_solution TEXT, - IN in_now INT8, - OUT out_solved BOOLEAN, - OUT out_retry_counter INT4 -) -LANGUAGE plpgsql -AS $$ -DECLARE - my_confirmation_date INT8; -DECLARE - my_rec RECORD; -BEGIN - - -- Check if challenge exists and matches - SELECT - tc.confirmation_date - ,tc.retry_counter - ,(tc.code = in_solution) AS solved - INTO - my_rec - FROM tan_challenges tc - WHERE tc.challenge_id = in_challenge_id - AND tc.h_body = in_h_body - AND tc.expiration_date > in_now; - - IF NOT FOUND - THEN - out_solved = FALSE; - RETURN; - END IF; - - my_confirmation_date = my_rec.confirmation_date; - out_retry_counter = my_rec.retry_counter; - out_solved = my_rec.solved; - - -- Check if already solved before - IF my_confirmation_date IS NOT NULL - THEN - out_solved = TRUE; - RETURN; - END IF; - - IF (0 = out_retry_counter) - THEN - out_solved = FALSE; - RETURN; - END IF; - - IF out_solved - THEN - -- Newly solved, update DB! - my_confirmation_date = in_now; - UPDATE tan_challenges - SET confirmation_date = my_confirmation_date - WHERE challenge_id = in_challenge_id; - ELSE - -- Failed to solve, decrement retry counter - out_retry_counter = out_retry_counter - 1; - UPDATE tan_challenges - SET retry_counter = out_retry_counter - WHERE challenge_id = in_challenge_id; - END IF; -END; -$$; diff --git a/src/backenddb/pg_statistics_examples.sql b/src/backenddb/pg_statistics_examples.sql @@ -21,14 +21,6 @@ -- Everything in one big transaction BEGIN; --- Check patch versioning is in place. --- SELECT _v.register_patch('example-statistics-0001', NULL, NULL); - -CREATE SCHEMA IF NOT EXISTS example_statistics; - -SET search_path TO example_statistics,merchant; - - -- Setup statistic: what do we want to track for 'deposits'? -- (Note: this is basically the one "manual" step we might not keep hard-coded) INSERT INTO merchant_statistic_bucket_meta @@ -41,7 +33,7 @@ VALUES ('deposits' ,'sales (before refunds)' ,'amount' - ,ARRAY['second'::statistic_range, 'minute', 'day', 'month', 'quarter', 'year'] + ,ARRAY['second'::merchant.statistic_range, 'minute', 'day', 'month', 'quarter', 'year'] ,ARRAY[120, 120, 95, 36, 40, 100] -- track last 120 s, 120 minutes, 95 days, 36 months, 40 quarters & 100 years ) ON CONFLICT DO NOTHING; @@ -72,19 +64,10 @@ CREATE FUNCTION merchant_deposits_statistics_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ -DECLARE - my_instance INT8; BEGIN -- SET search_path TO merchant; - SELECT mct.merchant_serial - INTO my_instance - FROM merchant_contract_terms mct - JOIN merchant_deposit_confirmations mdc - USING (order_serial) - WHERE mdc.deposit_confirmation_serial = NEW.deposit_confirmation_serial; CALL merchant_do_bump_amount_stat ('deposits' - ,my_instance ,CURRENT_TIMESTAMP(0) ,NEW.amount_with_fee); RETURN NEW; @@ -98,7 +81,6 @@ DECLARE BEGIN FOR rec IN SELECT 'deposits' AS in_slug - ,mct.merchant_serial AS in_merchant_serial ,TO_TIMESTAMP (mdc.deposit_timestamp / 1000.0 / 1000.0)::TIMESTAMP AS in_timestamp ,mdc.total_without_fee AS in_delta FROM merchant_deposit_confirmations mdc @@ -106,7 +88,7 @@ FOR rec IN USING (order_serial) WHERE mdc.deposit_timestamp > (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)) - 365*24*60*60) * 1000000 LOOP - CALL merchant_do_bump_amount_stat (rec.in_slug, rec.in_merchant_serial, rec.in_timestamp, rec.in_delta); + CALL merchant_do_bump_amount_stat (rec.in_slug, rec.in_timestamp, rec.in_delta); END LOOP; END $$; @@ -114,7 +96,8 @@ END $$; CREATE TRIGGER merchant_deposits_on_insert AFTER INSERT ON merchant_deposits - FOR EACH ROW EXECUTE FUNCTION merchant_deposits_statistics_trigger(); + FOR EACH ROW + EXECUTE FUNCTION merchant_deposits_statistics_trigger(); @@ -131,7 +114,7 @@ VALUES ('products-sold' ,'products sold (only those tracked in inventory)' ,'number' - ,ARRAY['second'::statistic_range, 'minute', 'day', 'week', 'month', 'quarter', 'year'] + ,ARRAY['second'::merchant.statistic_range, 'minute', 'day', 'week', 'month', 'quarter', 'year'] ,ARRAY[120, 120, 60, 12, 24, 8, 10] -- track last 120s, 120 minutes, 60 days, 12 weeks, 24 months, 8 quarters and 10 years ) ON CONFLICT DO NOTHING; @@ -150,7 +133,6 @@ BEGIN THEN CALL merchant_do_bump_number_stat ('products-sold' - ,NEW.merchant_serial ,CURRENT_TIMESTAMP(0) ,my_sold); END IF; @@ -163,12 +145,12 @@ CREATE TRIGGER merchant_products_on_sold ON merchant_inventory FOR EACH ROW EXECUTE FUNCTION merchant_products_sold_statistics_trigger(); -delete from merchant.merchant_statistic_bucket_counter ; -delete from merchant.merchant_statistic_bucket_amount ; -delete from merchant.merchant_statistic_interval_counter; -delete from merchant.merchant_statistic_interval_amount; -delete from merchant.merchant_statistic_amount_event; -delete from merchant.merchant_statistic_counter_event; +DELETE FROM merchant.merchant_statistic_bucket_counter ; +DELETE FROM merchant.merchant_statistic_bucket_amount ; +DELETE FROM merchant.merchant_statistic_interval_counter; +DELETE FROM merchant.merchant_statistic_interval_amount; +DELETE FROM merchant.merchant_statistic_amount_event; +DELETE FROM merchant.merchant_statistic_counter_event; call merchant_do_bump_number_stat ('products-sold'::text, 6, CURRENT_TIMESTAMP(0)::TIMESTAMP-INTERVAL '2 minutes', 1); @@ -180,21 +162,21 @@ call merchant_do_bump_amount_stat ('deposits'::text, 6, CURRENT_TIMESTAMP(0)::TI call merchant_do_bump_number_stat ('products-sold'::text, 6, CURRENT_TIMESTAMP(0)::TIMESTAMP, 4); call merchant_do_bump_amount_stat ('deposits'::text, 6, CURRENT_TIMESTAMP(0)::TIMESTAMP, (16,16,'EUR')::taler_amount_currency); -select * from merchant_statistic_interval_number_get ('products-sold', 'default'); +SELECT * FROM merchant_statistic_interval_number_get ('products-sold', 'default'); -select * from merchant_statistic_interval_amount_get ('deposits', 'default'); +SELECT * FROM merchant_statistic_interval_amount_get ('deposits', 'default'); -select * from merchant.merchant_statistic_amount_event; +SELECT * FROM merchant.merchant_statistic_amount_event; -select * from merchant.merchant_statistic_counter_event; +SELECT * FROM merchant.merchant_statistic_counter_event; -select * from merchant.merchant_statistic_interval_counter; +SELECT * FROM merchant.merchant_statistic_interval_counter; -select * from merchant.merchant_statistic_interval_amount; +SELECT * FROM merchant.merchant_statistic_interval_amount; -select * from merchant.merchant_statistic_bucket_counter ; +SELECT * FROM merchant.merchant_statistic_bucket_counter ; -select * from merchant.merchant_statistic_bucket_amount ; +SELECT * FROM merchant.merchant_statistic_bucket_amount ; -- ROLLBACK; COMMIT; diff --git a/src/backenddb/pg_statistics_helpers.sql b/src/backenddb/pg_statistics_helpers.sql @@ -14,26 +14,10 @@ -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -- -SET search_path TO merchant; -DROP FUNCTION IF EXISTS interval_to_start; -CREATE OR REPLACE FUNCTION interval_to_start ( - IN in_timestamp TIMESTAMP, - IN in_range statistic_range, - OUT out_bucket_start INT8 -) -LANGUAGE plpgsql -AS $$ -BEGIN - out_bucket_start = EXTRACT(EPOCH FROM DATE_TRUNC(in_range::text, in_timestamp)); -END $$; -COMMENT ON FUNCTION interval_to_start - IS 'computes the start time of the bucket for an event at the current time given the desired bucket range'; - DROP PROCEDURE IF EXISTS merchant_do_bump_number_bucket_stat; CREATE OR REPLACE PROCEDURE merchant_do_bump_number_bucket_stat( in_slug TEXT, - in_merchant_serial BIGINT, in_timestamp TIMESTAMP, in_delta INT8 ) @@ -41,7 +25,7 @@ LANGUAGE plpgsql AS $$ DECLARE my_meta INT8; - my_range statistic_range; + my_range merchant.statistic_range; my_bucket_start INT8; my_curs CURSOR (arg_slug TEXT) FOR SELECT UNNEST(ranges) @@ -65,25 +49,22 @@ BEGIN EXIT WHEN NOT FOUND; SELECT * INTO my_bucket_start - FROM interval_to_start (in_timestamp, my_range); + FROM merchant.interval_to_start (in_timestamp, my_range); UPDATE merchant_statistic_bucket_counter SET cumulative_number = cumulative_number + in_delta WHERE bmeta_serial_id=my_meta - AND merchant_serial=in_merchant_serial AND bucket_start=my_bucket_start AND bucket_range=my_range; IF NOT FOUND THEN INSERT INTO merchant_statistic_bucket_counter (bmeta_serial_id - ,merchant_serial ,bucket_start ,bucket_range ,cumulative_number ) VALUES ( my_meta - ,in_merchant_serial ,my_bucket_start ,my_range ,in_delta); @@ -96,15 +77,14 @@ END $$; DROP PROCEDURE IF EXISTS merchant_do_bump_amount_bucket_stat; CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_bucket_stat( in_slug TEXT, - in_merchant_serial BIGINT, in_timestamp TIMESTAMP, - in_delta taler_amount_currency + in_delta merchant.taler_amount_currency ) LANGUAGE plpgsql AS $$ DECLARE my_meta INT8; - my_range statistic_range; + my_range merchant.statistic_range; my_bucket_start INT8; my_curs CURSOR (arg_slug TEXT) FOR SELECT UNNEST(ranges) @@ -128,7 +108,7 @@ BEGIN EXIT WHEN NOT FOUND; SELECT * INTO my_bucket_start - FROM interval_to_start (in_timestamp, my_range); + FROM merchant.interval_to_start (in_timestamp, my_range); UPDATE merchant_statistic_bucket_amount SET @@ -145,7 +125,6 @@ BEGIN ELSE 0 END WHERE bmeta_serial_id=my_meta - AND merchant_serial=in_merchant_serial AND curr=(in_delta).curr AND bucket_start=my_bucket_start AND bucket_range=my_range; @@ -153,7 +132,6 @@ BEGIN THEN INSERT INTO merchant_statistic_bucket_amount (bmeta_serial_id - ,merchant_serial ,bucket_start ,bucket_range ,curr @@ -161,7 +139,6 @@ BEGIN ,cumulative_frac ) VALUES ( my_meta - ,in_merchant_serial ,my_bucket_start ,my_range ,(in_delta).curr @@ -179,7 +156,6 @@ COMMENT ON PROCEDURE merchant_do_bump_amount_bucket_stat DROP PROCEDURE IF EXISTS merchant_do_bump_number_interval_stat; CREATE OR REPLACE PROCEDURE merchant_do_bump_number_interval_stat( in_slug TEXT, - in_merchant_serial BIGINT, in_timestamp TIMESTAMP, in_delta INT8 ) @@ -233,12 +209,10 @@ BEGIN INSERT INTO merchant_statistic_counter_event AS msce (imeta_serial_id - ,merchant_serial ,slot ,delta) VALUES (my_meta - ,in_merchant_serial ,my_start ,in_delta) ON CONFLICT (imeta_serial_id, merchant_serial, slot) @@ -250,19 +224,16 @@ BEGIN UPDATE merchant_statistic_interval_counter SET cumulative_number = cumulative_number + in_delta WHERE imeta_serial_id = my_meta - AND merchant_serial = in_merchant_serial AND range=my_rangex; IF NOT FOUND THEN INSERT INTO merchant_statistic_interval_counter (imeta_serial_id - ,merchant_serial ,range ,event_delimiter ,cumulative_number ) VALUES ( my_meta - ,in_merchant_serial ,my_rangex ,my_event ,in_delta); @@ -276,9 +247,8 @@ COMMENT ON PROCEDURE merchant_do_bump_number_interval_stat DROP PROCEDURE IF EXISTS merchant_do_bump_amount_interval_stat; CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_interval_stat( in_slug TEXT, - in_merchant_serial BIGINT, in_timestamp TIMESTAMP, - in_delta taler_amount_currency -- new amount in table that we should add to the tracker + in_delta merchant.taler_amount_currency -- new amount in table that we should add to the tracker ) LANGUAGE plpgsql AS $$ @@ -330,14 +300,12 @@ BEGIN INSERT INTO merchant_statistic_amount_event AS msae (imeta_serial_id - ,merchant_serial ,slot ,delta_curr ,delta_value ,delta_frac ) VALUES ( my_meta - ,in_merchant_serial ,my_start ,(in_delta).curr ,(in_delta).val @@ -375,14 +343,12 @@ BEGIN ELSE 0 END WHERE imeta_serial_id=my_meta - AND merchant_serial=in_merchant_serial AND range=my_rangex AND curr=(in_delta).curr; IF NOT FOUND THEN INSERT INTO merchant_statistic_interval_amount (imeta_serial_id - ,merchant_serial ,range ,event_delimiter ,curr @@ -390,7 +356,6 @@ BEGIN ,cumulative_frac ) VALUES ( my_meta - ,in_merchant_serial ,my_rangex ,my_event ,(in_delta).curr @@ -405,15 +370,14 @@ COMMENT ON PROCEDURE merchant_do_bump_amount_interval_stat DROP PROCEDURE IF EXISTS merchant_do_bump_number_stat; CREATE OR REPLACE PROCEDURE merchant_do_bump_number_stat( in_slug TEXT, - in_merchant_serial BIGINT, in_timestamp TIMESTAMP, in_delta INT8 ) LANGUAGE plpgsql AS $$ BEGIN - CALL merchant_do_bump_number_bucket_stat (in_slug, in_merchant_serial, in_timestamp, in_delta); - CALL merchant_do_bump_number_interval_stat (in_slug, in_merchant_serial, in_timestamp, in_delta); + CALL merchant_do_bump_number_bucket_stat (in_slug, in_timestamp, in_delta); + CALL merchant_do_bump_number_interval_stat (in_slug, in_timestamp, in_delta); END $$; COMMENT ON PROCEDURE merchant_do_bump_number_stat IS 'Updates a numeric statistic (bucket or interval)'; @@ -422,373 +386,19 @@ COMMENT ON PROCEDURE merchant_do_bump_number_stat DROP PROCEDURE IF EXISTS merchant_do_bump_amount_stat; CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_stat( in_slug TEXT, - in_merchant_serial BIGINT, in_timestamp TIMESTAMP, - in_delta taler_amount_currency + in_delta merchant.taler_amount_currency ) LANGUAGE plpgsql AS $$ BEGIN - CALL merchant_do_bump_amount_bucket_stat (in_slug, in_merchant_serial, in_timestamp, in_delta); - CALL merchant_do_bump_amount_interval_stat (in_slug, in_merchant_serial, in_timestamp, in_delta); + CALL merchant_do_bump_amount_bucket_stat (in_slug, in_timestamp, in_delta); + CALL merchant_do_bump_amount_interval_stat (in_slug, in_timestamp, in_delta); END $$; COMMENT ON PROCEDURE merchant_do_bump_amount_stat IS 'Updates an amount statistic (bucket or interval)'; -DROP FUNCTION IF EXISTS merchant_statistic_interval_number_get; -CREATE FUNCTION merchant_statistic_interval_number_get ( - IN in_slug TEXT, - IN in_instance_id TEXT -) -RETURNS SETOF merchant_statistic_interval_number_get_return_value -LANGUAGE plpgsql -AS $$ -DECLARE - my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; - my_ranges INT8[]; - my_range INT8; - my_delta INT8; - my_meta INT8; - my_next_max_serial INT8; - my_instance_id INT8; - my_rec RECORD; - my_irec RECORD; - my_i INT; - my_min_serial INT8 DEFAULT NULL; - my_rval merchant_statistic_interval_number_get_return_value; -BEGIN - SELECT merchant_serial - INTO my_instance_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - IF NOT FOUND - THEN - RETURN; - END IF; - - SELECT imeta_serial_id - ,ranges - ,precisions - INTO my_rec - FROM merchant_statistic_interval_meta - WHERE slug=in_slug; - IF NOT FOUND - THEN - RETURN; - END IF; - my_rval.rvalue = 0; - my_ranges = my_rec.ranges; - my_meta = my_rec.imeta_serial_id; - - FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) - LOOP - my_range = my_ranges[my_i]; - SELECT event_delimiter - ,cumulative_number - INTO my_irec - FROM merchant_statistic_interval_counter - WHERE imeta_serial_id = my_meta - AND range = my_range - AND merchant_serial = my_instance_id; - IF FOUND - THEN - my_min_serial = my_irec.event_delimiter; - my_rval.rvalue = my_rval.rvalue + my_irec.cumulative_number; - - -- Check if we have events that left the applicable range - SELECT SUM(delta) AS delta_sum - INTO my_irec - FROM merchant_statistic_counter_event - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND slot < my_time - my_range - AND nevent_serial_id >= my_min_serial; - - IF FOUND AND my_irec.delta_sum IS NOT NULL - THEN - my_delta = my_irec.delta_sum; - my_rval.rvalue = my_rval.rvalue - my_delta; - - -- First find out the next event delimiter value - SELECT nevent_serial_id - INTO my_next_max_serial - FROM merchant_statistic_counter_event - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND slot >= my_time - my_range - AND nevent_serial_id >= my_min_serial - ORDER BY slot ASC - LIMIT 1; - - IF FOUND - THEN - -- remove expired events from the sum of the current slot - - UPDATE merchant_statistic_interval_counter - SET cumulative_number = cumulative_number - my_delta, - event_delimiter = my_next_max_serial - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND range = my_range; - ELSE - -- actually, slot is now empty, remove it entirely - DELETE FROM merchant_statistic_interval_counter - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND range = my_range; - END IF; - IF (my_i < COALESCE(array_length(my_ranges,1),0)) - THEN - -- carry over all events into the next slot - UPDATE merchant_statistic_interval_counter AS usic SET - cumulative_number = cumulative_number + my_delta, - event_delimiter = LEAST(usic.event_delimiter,my_min_serial) - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND range=my_ranges[my_i+1]; - IF NOT FOUND - THEN - INSERT INTO merchant_statistic_interval_counter - (imeta_serial_id - ,merchant_serial - ,range - ,event_delimiter - ,cumulative_number - ) VALUES ( - my_meta - ,my_instance_id - ,my_ranges[my_i+1] - ,my_min_serial - ,my_delta); - END IF; - ELSE - -- events are obsolete, delete them - DELETE FROM merchant_statistic_counter_event - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND slot < my_time - my_range; - END IF; - END IF; - - my_rval.range = my_range; - RETURN NEXT my_rval; - END IF; - END LOOP; -END $$; - -COMMENT ON FUNCTION merchant_statistic_interval_number_get - IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value for each range'; - - -DROP FUNCTION IF EXISTS merchant_statistic_interval_amount_get; -CREATE FUNCTION merchant_statistic_interval_amount_get ( - IN in_slug TEXT, - IN in_instance_id TEXT -) -RETURNS SETOF merchant_statistic_interval_amount_get_return_value -LANGUAGE plpgsql -AS $$ -DECLARE - my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; - my_ranges INT8[]; - my_range INT8; - my_delta_value INT8; - my_delta_frac INT8; - my_meta INT8; - my_instance_id INT8; - my_next_max_serial INT8; - my_currency TEXT; - my_rec RECORD; - my_irec RECORD; - my_jrec RECORD; - my_i INT; - my_min_serial INT8 DEFAULT NULL; - my_rval merchant_statistic_interval_amount_get_return_value; -BEGIN - SELECT merchant_serial - INTO my_instance_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - IF NOT FOUND - THEN - RETURN; - END IF; - - SELECT imeta_serial_id - ,ranges - ,precisions - INTO my_rec - FROM merchant_statistic_interval_meta - WHERE slug=in_slug; - IF NOT FOUND - THEN - RETURN; - END IF; - - my_meta = my_rec.imeta_serial_id; - my_ranges = my_rec.ranges; - - FOR my_currency IN - SELECT DISTINCT delta_curr - FROM merchant_statistic_amount_event - WHERE imeta_serial_id = my_meta - LOOP - - my_rval.rvalue.val = 0; - my_rval.rvalue.frac = 0; - my_rval.rvalue.curr = my_currency; - - FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) - LOOP - my_range = my_ranges[my_i]; - SELECT event_delimiter - ,cumulative_value - ,cumulative_frac - INTO my_irec - FROM merchant_statistic_interval_amount - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND curr = my_currency - AND range = my_range; - - IF FOUND - THEN - my_min_serial = my_irec.event_delimiter; - my_rval.rvalue.val = (my_rval.rvalue).val + my_irec.cumulative_value + my_irec.cumulative_frac / 100000000; - my_rval.rvalue.frac = (my_rval.rvalue).frac + my_irec.cumulative_frac % 100000000; - IF (my_rval.rvalue).frac > 100000000 - THEN - my_rval.rvalue.frac = (my_rval.rvalue).frac - 100000000; - my_rval.rvalue.val = (my_rval.rvalue).val + 1; - END IF; - - -- Check if we have events that left the applicable range - SELECT SUM(delta_value) AS value_sum - ,SUM(delta_frac) AS frac_sum - INTO my_jrec - FROM merchant_statistic_amount_event - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND delta_curr = my_currency - AND slot < my_time - my_range - AND aevent_serial_id >= my_min_serial; - - IF FOUND AND my_jrec.value_sum IS NOT NULL - THEN - -- Normalize sum - my_delta_value = my_jrec.value_sum + my_jrec.frac_sum / 100000000; - my_delta_frac = my_jrec.frac_sum % 100000000; - my_rval.rvalue.val = (my_rval.rvalue).val - my_delta_value; - IF ((my_rval.rvalue).frac >= my_delta_frac) - THEN - my_rval.rvalue.frac = (my_rval.rvalue).frac - my_delta_frac; - ELSE - my_rval.rvalue.frac = 100000000 + (my_rval.rvalue).frac - my_delta_frac; - my_rval.rvalue.val = (my_rval.rvalue).val - 1; - END IF; - - -- First find out the next event delimiter value - SELECT aevent_serial_id - INTO my_next_max_serial - FROM merchant_statistic_amount_event - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND delta_curr = my_currency - AND slot >= my_time - my_range - AND aevent_serial_id >= my_min_serial - ORDER BY slot ASC - LIMIT 1; - IF FOUND - THEN - -- remove expired events from the sum of the current slot - UPDATE merchant_statistic_interval_amount SET - cumulative_value = cumulative_value - my_delta_value - - CASE - WHEN cumulative_frac < my_delta_frac - THEN 1 - ELSE 0 - END, - cumulative_frac = cumulative_frac - my_delta_frac - + CASE - WHEN cumulative_frac < my_delta_frac - THEN 100000000 - ELSE 0 - END, - event_delimiter = my_next_max_serial - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND curr = my_currency - AND range = my_range; - ELSE - -- actually, slot is now empty, remove it entirely - DELETE FROM merchant_statistic_interval_amount - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND curr = my_currency - AND range = my_range; - END IF; - IF (my_i < COALESCE(array_length(my_ranges,1),0)) - THEN - -- carry over all events into the next (larger) slot - UPDATE merchant_statistic_interval_amount AS msia SET - cumulative_value = cumulative_value + my_delta_value - + CASE - WHEN cumulative_frac + my_delta_frac > 100000000 - THEN 1 - ELSE 0 - END, - cumulative_frac = cumulative_frac + my_delta_frac - - CASE - WHEN cumulative_frac + my_delta_frac > 100000000 - THEN 100000000 - ELSE 0 - END, - event_delimiter = LEAST (msia.event_delimiter,my_min_serial) - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND range=my_ranges[my_i+1]; - IF NOT FOUND - THEN - INSERT INTO merchant_statistic_interval_amount - (imeta_serial_id - ,merchant_serial - ,event_delimiter - ,range - ,curr - ,cumulative_value - ,cumulative_frac - ) VALUES ( - my_meta - ,my_instance_id - ,my_min_serial - ,my_ranges[my_i+1] - ,my_currency - ,my_delta_value - ,my_delta_frac); - END IF; - ELSE - -- events are obsolete, delete them - DELETE FROM merchant_statistic_amount_event - WHERE imeta_serial_id = my_meta - AND merchant_serial = my_instance_id - AND slot < my_time - my_range; - END IF; - END IF; - - my_rval.range = my_range; - RETURN NEXT my_rval; - END IF; - END LOOP; -- over my_ranges - END LOOP; -- over my_currency -END $$; - -COMMENT ON FUNCTION merchant_statistic_interval_amount_get - IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value; multiple values are returned, one per currency and range'; - - - - DROP PROCEDURE IF EXISTS merchant_statistic_counter_gc; CREATE OR REPLACE PROCEDURE merchant_statistic_counter_gc () @@ -826,7 +436,7 @@ BEGIN -- First, we query the current interval statistic to update its counters SELECT merchant_id INTO my_instance_name - FROM merchant_instances + FROM merchant.merchant_instances WHERE merchant_serial = my_instance; PERFORM FROM merchant_statistic_interval_number_get (my_rec.slug, my_instance_name); @@ -944,7 +554,7 @@ BEGIN -- First, we query the current interval statistic to update its counters SELECT merchant_id INTO my_instance_name - FROM merchant_instances + FROM merchant.merchant_instances WHERE merchant_serial = my_instance; PERFORM FROM merchant_statistic_interval_amount_get (my_rec.slug, my_instance_name); @@ -1075,7 +685,7 @@ COMMENT ON PROCEDURE merchant_statistic_bucket_gc DROP FUNCTION IF EXISTS merchant_statistics_bucket_end; CREATE FUNCTION merchant_statistics_bucket_end ( IN in_bucket_start INT8, - IN in_range statistic_range, + IN in_range merchant.statistic_range, OUT out_bucket_end INT8 ) LANGUAGE plpgsql diff --git a/src/backenddb/pg_update_money_pot.sql b/src/backenddb/pg_update_money_pot.sql @@ -1,82 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2025 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_update_money_pot; -CREATE FUNCTION merchant_do_update_money_pot ( - IN in_instance_id TEXT, - IN in_money_pot_serial INT8, - IN in_name TEXT, - IN in_description TEXT, - IN in_old_totals taler_amount_currency[], -- can be NULL! - IN in_new_totals taler_amount_currency[], -- can be NULL! - OUT out_conflict_total BOOL, - OUT out_conflict_name BOOL, - OUT out_not_found BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; -BEGIN - -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - -IF NOT FOUND -THEN - -- If instance does not exist, pot cannot exist - out_conflict_total = FALSE; - out_conflict_name = FALSE; - out_not_found = TRUE; - RETURN; -END IF; - -BEGIN - UPDATE merchant_money_pots SET - money_pot_name=in_name - ,money_pot_description=in_description - ,pot_totals=COALESCE(in_new_totals, pot_totals) - WHERE merchant_serial=my_merchant_id - AND money_pot_serial=in_money_pot_serial - AND ( (in_old_totals IS NULL) OR (pot_totals=in_old_totals) ); - IF NOT FOUND - THEN - -- Check if pot_total was the problem - PERFORM FROM merchant_money_pots - WHERE merchant_serial=my_merchant_id - AND money_pot_serial=in_money_pot_serial; - out_conflict_total = FOUND; - out_not_found = NOT FOUND; - out_conflict_name = FALSE; - ELSE - out_conflict_total = FALSE; - out_not_found = FALSE; - out_conflict_name = FALSE; - END IF; - RETURN; -EXCEPTION - -- money_pot_name already used - WHEN unique_violation - THEN - out_conflict_name = TRUE; - out_conflict_total = FALSE; - out_not_found = FALSE; - RETURN; -END; - -END $$; diff --git a/src/backenddb/pg_update_product.sql b/src/backenddb/pg_update_product.sql @@ -1,199 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2024, 2025 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/> --- - - -DROP FUNCTION IF EXISTS merchant_do_update_product; -CREATE FUNCTION merchant_do_update_product ( - IN in_instance_id TEXT, - IN in_product_id TEXT, - IN in_description TEXT, - IN in_description_i18n JSONB, -- $4 - IN in_unit TEXT, - IN in_image TEXT, - IN in_taxes JSONB, -- $7 - IN ina_price_list taler_amount_currency[], - IN in_total_stock INT8, - IN in_total_stock_frac INT4, - IN in_allow_fractional_quantity BOOL, - IN in_fractional_precision_level INT4, - IN in_total_lost INT8, -- NOTE: not in insert_product - IN in_address JSONB, -- $14 - IN in_next_restock INT8, - IN in_minimum_age INT4, - IN ina_categories INT8[], -- $17 - IN in_product_name TEXT, - IN in_product_group_id INT8, -- NULL for default - IN in_money_pot_id INT8, -- NULL for none - IN in_price_is_net BOOL, -- $21 - OUT out_no_instance BOOL, - OUT out_no_product BOOL, - OUT out_lost_reduced BOOL, - OUT out_sold_reduced BOOL, - OUT out_stocked_reduced BOOL, - OUT out_no_cat INT8, - OUT out_no_group BOOL, - OUT out_no_pot BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_product_serial INT8; - i INT8; - ini_cat INT8; - rec RECORD; -BEGIN - -out_no_group = FALSE; -out_no_pot = FALSE; -out_no_instance=FALSE; -out_no_product=FALSE; -out_lost_reduced=FALSE; -out_sold_reduced=FALSE; -- We currently don't allow updating 'sold', hence always FALSE -out_stocked_reduced=FALSE; -out_no_cat=NULL; - --- Which instance are we using? -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - -IF NOT FOUND -THEN - out_no_instance=TRUE; - RETURN; -END IF; - -IF in_product_group_id IS NOT NULL -THEN - PERFORM FROM merchant_product_groups - WHERE product_group_serial=in_product_group_id - AND merchant_serial=my_merchant_id; - IF NOT FOUND - THEN - out_no_group=TRUE; - RETURN; - END IF; -END IF; - -IF in_money_pot_id IS NOT NULL -THEN - PERFORM FROM merchant_money_pots - WHERE money_pot_serial=in_money_pot_id - AND merchant_serial=my_merchant_id; - IF NOT FOUND - THEN - out_no_pot=TRUE; - RETURN; - END IF; -END IF; - --- Check existing entry satisfies constraints -SELECT total_stock - ,total_stock_frac - ,total_lost - ,allow_fractional_quantity - ,product_serial - INTO rec - FROM merchant_inventory - WHERE merchant_serial=my_merchant_id - AND product_id=in_product_id; - -IF NOT FOUND -THEN - out_no_product=TRUE; - RETURN; -END IF; - -my_product_serial = rec.product_serial; - -IF rec.total_stock > in_total_stock -THEN - out_stocked_reduced=TRUE; - RETURN; -END IF; - -IF rec.total_lost > in_total_lost -THEN - out_lost_reduced=TRUE; - RETURN; -END IF; -IF rec.allow_fractional_quantity - AND (NOT in_allow_fractional_quantity) -THEN - DELETE - FROM merchant_inventory_locks - WHERE product_serial = my_product_serial - AND total_locked_frac <> 0; -END IF; - --- Remove old categories -DELETE FROM merchant_product_categories - WHERE product_serial=my_product_serial; - --- Add new categories -FOR i IN 1..COALESCE(array_length(ina_categories,1),0) -LOOP - ini_cat=ina_categories[i]; - - INSERT INTO merchant_product_categories - (product_serial - ,category_serial) - VALUES - (my_product_serial - ,ini_cat) - ON CONFLICT DO NOTHING; - - IF NOT FOUND - THEN - out_no_cat=i; - RETURN; - END IF; -END LOOP; - -UPDATE merchant_inventory SET - description=in_description - ,description_i18n=in_description_i18n - ,product_name=in_product_name - ,unit=in_unit - ,image=in_image - ,image_hash=CASE - WHEN (in_image IS NULL) OR (in_image = '') - THEN NULL - ELSE encode(public.digest(convert_to(in_image, 'UTF8'), - 'sha256'), - 'hex') - END - ,taxes=in_taxes - ,price_array=ina_price_list - ,total_stock=in_total_stock - ,total_stock_frac=in_total_stock_frac - ,allow_fractional_quantity=in_allow_fractional_quantity - ,fractional_precision_level=in_fractional_precision_level - ,total_lost=in_total_lost - ,address=in_address - ,next_restock=in_next_restock - ,minimum_age=in_minimum_age - ,product_group_serial=in_product_group_id - ,money_pot_serial=in_money_pot_id - ,price_is_net=in_price_is_net - WHERE merchant_serial=my_merchant_id - AND product_serial=my_product_serial; -- could also match on product_id - -ASSERT FOUND,'SELECTED it earlier, should UPDATE it now'; - --- Success! -END $$; diff --git a/src/backenddb/pg_update_product_group.sql b/src/backenddb/pg_update_product_group.sql @@ -1,55 +0,0 @@ --- --- This file is part of TALER --- Copyright (C) 2025 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/> --- - -SET search_path TO merchant; - -DROP FUNCTION IF EXISTS merchant_do_update_product_group; -CREATE FUNCTION merchant_do_update_product_group ( - IN in_instance_id TEXT, - IN in_product_group_serial INT8, - IN in_name TEXT, - IN in_description TEXT, - OUT out_conflict BOOL, - OUT out_not_found BOOL) -LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; -BEGIN - -SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id=in_instance_id; - -BEGIN - UPDATE merchant_product_groups SET - product_group_name=in_name - ,product_group_description=in_description - WHERE merchant_serial=my_merchant_id - AND product_group_serial=in_product_group_serial; - out_not_found = NOT FOUND; - out_conflict = FALSE; - RETURN; -EXCEPTION - WHEN unique_violation - THEN - out_conflict = TRUE; - RETURN; -END; - - -END $$; diff --git a/src/backenddb/pg_uri_escape.sql b/src/backenddb/pg_uri_escape.sql @@ -13,12 +13,14 @@ -- 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/> -- -DROP FUNCTION IF EXISTS uri_escape; -CREATE FUNCTION uri_escape(input TEXT) -RETURNS TEXT -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE +DROP FUNCTION IF EXISTS merchant.uri_escape; +CREATE FUNCTION merchant.uri_escape( + input TEXT +) + RETURNS TEXT + LANGUAGE sql + IMMUTABLE + PARALLEL SAFE AS $$ SELECT COALESCE(string_agg( CASE @@ -33,5 +35,5 @@ FROM ( ) s, generate_series(0, length(b) - 1) AS i; $$; -COMMENT ON FUNCTION uri_escape(TEXT) +COMMENT ON FUNCTION merchant.uri_escape(TEXT) IS 'Percent-encodes all characters except RFC 3986 unreserved chars: A-Z a-z 0-9 - . _ ~'; diff --git a/src/backenddb/purge_instance.c b/src/backenddb/purge_instance.c @@ -19,12 +19,11 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/purge_instance.h" #include "helper.h" + /** * Purge an instance and all associated information from our database. * Highly likely to cause undesired data loss. Use with caution. @@ -34,8 +33,9 @@ * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_purge_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id) +TALER_MERCHANTDB_purge_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (merchant_id), @@ -45,8 +45,8 @@ TALER_MERCHANTDB_purge_instance (struct TALER_MERCHANTDB_PostgresContext *pg, check_connection (pg); PREPARE (pg, "purge_instance", - "DELETE FROM merchant_instances" - " WHERE merchant_instances.merchant_id = $1"); + "DELETE FROM merchant.merchant_instances" + " WHERE merchant_id = $1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "purge_instance", params); diff --git a/src/backenddb/refund_coin.c b/src/backenddb/refund_coin.c @@ -19,58 +19,56 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/refund_coin.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_refund_coin (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - struct GNUNET_TIME_Timestamp refund_timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *reason) +TALER_MERCHANTDB_refund_coin ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + struct GNUNET_TIME_Timestamp refund_timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *reason) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_timestamp (&refund_timestamp), GNUNET_PQ_query_param_auto_from_type (coin_pub), GNUNET_PQ_query_param_string (reason), GNUNET_PQ_query_param_end }; - PREPARE (pg, - "refund_coin", - "INSERT INTO merchant_refunds" - "(order_serial" - ",rtransaction_id" - ",refund_timestamp" - ",coin_pub" - ",reason" - ",refund_amount" - ") " - "SELECT " - " dcon.order_serial" - ",0" /* rtransaction_id always 0 for /abort */ - ",$3" - ",dep.coin_pub" - ",$5" - ",dep.amount_with_fee" - " FROM merchant_deposits dep" - " JOIN merchant_deposit_confirmations dcon" - " USING (deposit_confirmation_serial)" - " WHERE dep.coin_pub=$4" - " AND dcon.order_serial=" - " (SELECT order_serial" - " FROM merchant_contract_terms" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1))"); + + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + TMH_PQ_prepare_anon (pg, + "INSERT INTO merchant_refunds" + "(order_serial" + ",rtransaction_id" + ",refund_timestamp" + ",coin_pub" + ",reason" + ",refund_amount" + ") " + "SELECT " + " dcon.order_serial" + ",0" /* rtransaction_id always 0 for /abort */ + ",$2" + ",dep.coin_pub" + ",$4" + ",dep.amount_with_fee" + " FROM merchant_deposits dep" + " JOIN merchant_deposit_confirmations dcon" + " USING (deposit_confirmation_serial)" + " WHERE dep.coin_pub=$3" + " AND dcon.order_serial=" + " (SELECT order_serial" + " FROM merchant_contract_terms" + " WHERE h_contract_terms=$1)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "refund_coin", + "", params); } diff --git a/src/backenddb/select_account.c b/src/backenddb/select_account.c @@ -19,21 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_account.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - const struct TALER_MerchantWireHashP *h_wire, - struct TALER_MERCHANTDB_AccountDetails *ad) +TALER_MERCHANTDB_select_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + const struct TALER_MerchantWireHashP *h_wire, + struct TALER_MERCHANTDB_AccountDetails *ad) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_auto_from_type (h_wire), GNUNET_PQ_query_param_end }; @@ -59,26 +57,24 @@ TALER_MERCHANTDB_select_account (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); ad->h_wire = *h_wire; ad->instance_id = id; check_connection (pg); - PREPARE (pg, - "select_account", - "SELECT" - " salt" - ",payto_uri" - ",credit_facade_url" - ",credit_facade_credentials::TEXT" - ",active" - ",extra_wire_subject_metadata" - " FROM merchant_accounts" - " WHERE merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1) " - " AND (h_wire=$2);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " salt" + ",payto_uri" + ",credit_facade_url" + ",credit_facade_credentials::TEXT" + ",active" + ",extra_wire_subject_metadata" + " FROM merchant_accounts" + " WHERE h_wire=$1;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_account", + "", params, rs); } diff --git a/src/backenddb/select_account_by_uri.c b/src/backenddb/select_account_by_uri.c @@ -19,21 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_account_by_uri.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_account_by_uri (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - struct TALER_FullPayto payto_uri, - struct TALER_MERCHANTDB_AccountDetails *ad) +TALER_MERCHANTDB_select_account_by_uri ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + struct TALER_FullPayto payto_uri, + struct TALER_MERCHANTDB_AccountDetails *ad) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_string (payto_uri.full_payto), GNUNET_PQ_query_param_end }; @@ -55,28 +53,26 @@ TALER_MERCHANTDB_select_account_by_uri (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); ad->credit_facade_url = NULL; ad->credit_facade_credentials = NULL; ad->payto_uri.full_payto = GNUNET_strdup (payto_uri.full_payto); ad->instance_id = id; check_connection (pg); - PREPARE (pg, - "select_account_by_uri", - "SELECT" - " salt" - ",h_wire" - ",credit_facade_url" - ",credit_facade_credentials::TEXT" - ",active" - " FROM merchant_accounts" - " WHERE merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND payto_uri = $2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " salt" + ",h_wire" + ",credit_facade_url" + ",credit_facade_credentials::TEXT" + ",active" + " FROM merchant_accounts" + " WHERE payto_uri = $1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_account_by_uri", + "", params, rs); } diff --git a/src/backenddb/select_accounts.c b/src/backenddb/select_accounts.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_accounts.h" #include "helper.h" @@ -80,30 +78,30 @@ select_account_cb (void *cls, struct TALER_MerchantPrivateKeyP merchant_priv; bool no_priv; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("h_wire", + GNUNET_PQ_result_spec_auto_from_type ("out_h_wire", &acc.h_wire), - GNUNET_PQ_result_spec_auto_from_type ("salt", + GNUNET_PQ_result_spec_auto_from_type ("out_salt", &acc.salt), - GNUNET_PQ_result_spec_string ("payto_uri", + GNUNET_PQ_result_spec_string ("out_payto_uri", &payto.full_payto), - GNUNET_PQ_result_spec_string ("merchant_id", + GNUNET_PQ_result_spec_string ("out_merchant_id", &instance_id), GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_string ("credit_facade_url", + GNUNET_PQ_result_spec_string ("out_credit_facade_url", &facade_url), NULL), GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_string ("extra_wire_subject_metadata", + GNUNET_PQ_result_spec_string ("out_extra_wire_subject_metadata", &extra_wire_subject_metadata), NULL), GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_json ("credit_facade_credentials", + TALER_PQ_result_spec_json ("out_credit_facade_credentials", &credential), NULL), - GNUNET_PQ_result_spec_bool ("active", + GNUNET_PQ_result_spec_bool ("out_active", &acc.active), GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_auto_from_type ("merchant_priv", + GNUNET_PQ_result_spec_auto_from_type ("out_merchant_priv", &merchant_priv), &no_priv), GNUNET_PQ_result_spec_end @@ -135,7 +133,6 @@ select_account_cb (void *cls, enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_select_accounts ( struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, TALER_MERCHANTDB_AccountCallback cb, void *cb_cls) { @@ -145,9 +142,6 @@ TALER_MERCHANTDB_select_accounts ( .pg = pg }; struct GNUNET_PQ_QueryParam params[] = { - NULL == id - ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; @@ -156,26 +150,16 @@ TALER_MERCHANTDB_select_accounts ( PREPARE (pg, "select_accounts", "SELECT" - " ma.h_wire" - ",ma.salt" - ",ma.payto_uri" - ",ma.credit_facade_url" - ",ma.credit_facade_credentials::TEXT" - ",ma.extra_wire_subject_metadata" - ",ma.active" - ",mk.merchant_priv" - ",mi.merchant_id" - " FROM merchant_accounts ma" - " JOIN merchant_instances mi" - " ON (mi.merchant_serial=ma.merchant_serial)" - " LEFT JOIN merchant_keys mk" - " ON (mk.merchant_serial=ma.merchant_serial)" - " WHERE" - " ($1::TEXT IS NULL) OR" - " (ma.merchant_serial=" - " (SELECT merchant_serial " - " FROM merchant_instances" - " WHERE merchant_id=$1));"); + " out_merchant_id" + " ,out_merchant_priv" + " ,out_h_wire" + " ,out_salt" + " ,out_payto_uri" + " ,out_credit_facade_url" + " ,out_credit_facade_credentials::TEXT" + " ,out_extra_wire_subject_metadata" + " ,out_active" + " FROM merchant.select_accounts()"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "select_accounts", params, diff --git a/src/backenddb/select_accounts_by_exchange.c b/src/backenddb/select_accounts_by_exchange.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_accounts_by_exchange.h" #include "helper.h" diff --git a/src/backenddb/select_accounts_by_instance.c b/src/backenddb/select_accounts_by_instance.c @@ -0,0 +1,176 @@ +/* + This file is part of TALER + Copyright (C) 2022, 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 src/backenddb/select_accounts_by_instance.c + * @brief Implementation of the select_accounts_by_instance function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_pq_lib.h> +#include "merchant-database/select_accounts_by_instance.h" +#include "helper.h" + + +/** + * Context for select_accounts(). + */ +struct SelectAccountsContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_AccountCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Database context. + */ + struct TALER_MERCHANTDB_PostgresContext *pg; + + /** + * Merchant instance ID. + */ + const char *id; + + /** + * Set to the return value on errors. + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about accounts. + * + * @param cls of type `struct SelectAccountsContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +select_account_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct SelectAccountsContext *lic = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + struct TALER_FullPayto payto; + char *facade_url = NULL; + char *extra_wire_subject_metadata = NULL; + json_t *credential = NULL; + struct TALER_MERCHANTDB_AccountDetails acc; + struct TALER_MerchantPrivateKeyP merchant_priv; + bool no_priv; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("h_wire", + &acc.h_wire), + GNUNET_PQ_result_spec_auto_from_type ("salt", + &acc.salt), + GNUNET_PQ_result_spec_string ("payto_uri", + &payto.full_payto), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("credit_facade_url", + &facade_url), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("extra_wire_subject_metadata", + &extra_wire_subject_metadata), + NULL), + GNUNET_PQ_result_spec_allow_null ( + TALER_PQ_result_spec_json ("credit_facade_credentials", + &credential), + NULL), + GNUNET_PQ_result_spec_bool ("active", + &acc.active), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_auto_from_type ("merchant_priv", + &merchant_priv), + &no_priv), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + lic->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + acc.instance_id = lic->id; + acc.payto_uri = payto; + acc.credit_facade_url = facade_url; + acc.credit_facade_credentials = credential; + acc.extra_wire_subject_metadata = extra_wire_subject_metadata; + if (NULL != lic->cb) + lic->cb (lic->cb_cls, + no_priv ? NULL : &merchant_priv, + &acc); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_select_accounts_by_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + TALER_MERCHANTDB_AccountCallback cb, + void *cb_cls) +{ + struct SelectAccountsContext lic = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg, + .id = id + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (id), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + TMH_PQ_prepare_anon (pg, + "SELECT" + " ma.h_wire" + ",ma.salt" + ",ma.payto_uri" + ",ma.credit_facade_url" + ",ma.credit_facade_credentials::TEXT" + ",ma.extra_wire_subject_metadata" + ",ma.active" + ",mi.merchant_priv" + " FROM merchant_accounts ma" + " JOIN merchant.merchant_instances mi" + " ON (mi.merchant_id=$1)"); + qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, + "", + params, + &select_account_cb, + &lic); + if (0 > lic.qs) + return lic.qs; + return qs; +} diff --git a/src/backenddb/select_all_donau_instances.c b/src/backenddb/select_all_donau_instances.c @@ -21,8 +21,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_all_donau_instances.h" #include "helper.h" @@ -76,24 +74,24 @@ select_donau_instance_cb (void *cls, int64_t current_year; json_t *donau_keys_json = NULL; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_uint64 ("donau_instances_serial", + GNUNET_PQ_result_spec_uint64 ("out_donau_instances_serial", &donau_instance_serial), - GNUNET_PQ_result_spec_string ("donau_url", + GNUNET_PQ_result_spec_string ("out_donau_url", &donau_url), - GNUNET_PQ_result_spec_string ("charity_name", + GNUNET_PQ_result_spec_string ("out_charity_name", &charity_name), - GNUNET_PQ_result_spec_auto_from_type ("charity_pub_key", + GNUNET_PQ_result_spec_auto_from_type ("out_charity_pub_key", &charity_pub_key), - GNUNET_PQ_result_spec_uint64 ("charity_id", + GNUNET_PQ_result_spec_uint64 ("out_charity_id", &charity_id), - TALER_PQ_result_spec_amount_with_currency ("charity_max_per_year", + TALER_PQ_result_spec_amount_with_currency ("out_charity_max_per_year", &charity_max_per_year), - TALER_PQ_result_spec_amount_with_currency ("charity_receipts_to_date", + TALER_PQ_result_spec_amount_with_currency ("out_charity_receipts_to_date", &charity_receipts_to_date), - GNUNET_PQ_result_spec_int64 ("current_year", + GNUNET_PQ_result_spec_int64 ("out_current_year", &current_year), GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_json ("keys_json", + TALER_PQ_result_spec_json ("out_keys_json", &donau_keys_json), NULL), GNUNET_PQ_result_spec_end @@ -144,20 +142,16 @@ TALER_MERCHANTDB_select_all_donau_instances ( PREPARE (pg, "select_all_donau_instances", "SELECT" - " di.donau_instances_serial" - ",di.donau_url" - ",di.charity_name" - ",mi.merchant_pub AS charity_pub_key" - ",di.charity_id" - ",di.charity_max_per_year" - ",di.charity_receipts_to_date" - ",di.current_year" - ",dk.keys_json::TEXT" - " FROM merchant_donau_instances di" - " LEFT JOIN merchant_donau_keys dk" - " ON di.donau_url = dk.donau_url" - " JOIN merchant_instances mi" - " ON di.merchant_instance_serial = mi.merchant_serial"); + " out_donau_instances_serial" + " ,out_donau_url" + " ,out_charity_name" + " ,out_charity_pub_key" + " ,out_charity_id" + " ,out_charity_max_per_year" + " ,out_charity_receipts_to_date" + " ,out_current_year" + " ,out_keys_json::TEXT" + " FROM merchant.select_all_donau_instances()"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "select_all_donau_instances", params, diff --git a/src/backenddb/select_all_donau_instances.sql b/src/backenddb/select_all_donau_instances.sql @@ -0,0 +1,82 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.select_all_donau_instances(); +CREATE FUNCTION merchant.select_all_donau_instances() +RETURNS TABLE( + out_donau_instances_serial INT8, + out_donau_url TEXT, + out_charity_name TEXT, + out_charity_pub_key BYTEA, + out_charity_id INT8, + out_charity_max_per_year merchant.taler_amount_currency, + out_charity_receipts_to_date merchant.taler_amount_currency, + out_current_year INT8, + out_keys_json JSONB) +LANGUAGE plpgsql +AS $FN$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; +BEGIN + FOR rec + IN + SELECT merchant_serial + ,merchant_pub + FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + FOR inner_rec + IN + EXECUTE format( + 'SELECT' + ' di.donau_instances_serial AS dis' + ' ,di.donau_url AS du' + ' ,di.charity_name AS cn' + ' ,di.charity_id AS ci' + ' ,di.charity_max_per_year AS cmp' + ' ,di.charity_receipts_to_date AS crt' + ' ,di.current_year AS cy' + ' ,dk.keys_json AS kj' + ' FROM %I.merchant_donau_instances di' + ' LEFT JOIN merchant.merchant_donau_keys dk' + ' ON di.donau_url = dk.donau_url', s) + LOOP + out_donau_instances_serial := inner_rec.dis; + out_donau_url := inner_rec.du; + out_charity_name := inner_rec.cn; + out_charity_pub_key := rec.merchant_pub; + out_charity_id := inner_rec.ci; + out_charity_max_per_year := inner_rec.cmp; + out_charity_receipts_to_date := inner_rec.crt; + out_current_year := inner_rec.cy; + out_keys_json := inner_rec.kj; + RETURN NEXT; + END LOOP; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; +END +$FN$; +COMMENT ON FUNCTION merchant.select_all_donau_instances() + IS 'Returns all donau-instance configurations across every per-instance' + ' schema, joined with merchant.merchant_donau_keys for the keys json' + ' and with merchant.merchant_instances for the charity public key.'; diff --git a/src/backenddb/select_category.c b/src/backenddb/select_category.c @@ -19,80 +19,60 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_category.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_category (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t category_id, - struct TALER_MERCHANTDB_CategoryDetails *cd, - size_t *num_products, - char **products) +TALER_MERCHANTDB_select_category ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t category_id, + struct TALER_MERCHANTDB_CategoryDetails *cd, + size_t *num_products, + char **products) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&category_id), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("category_name", + &cd->category_name), + TALER_PQ_result_spec_json ("category_name_i18n", + &cd->category_name_i18n), + GNUNET_PQ_result_spec_array_string (pg->conn, + "products", + num_products, + products), + GNUNET_PQ_result_spec_end + }; - if (NULL == cd) - { - struct GNUNET_PQ_ResultSpec rs_null[] = { - GNUNET_PQ_result_spec_end - }; - - check_connection (pg); - return GNUNET_PQ_eval_prepared_singleton_select ( - pg->conn, - "select_category", - params, - rs_null); - } - else - { - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("category_name", - &cd->category_name), - TALER_PQ_result_spec_json ("category_name_i18n", - &cd->category_name_i18n), - GNUNET_PQ_result_spec_array_string (pg->conn, - "products", - num_products, - products), - GNUNET_PQ_result_spec_end - }; - - PREPARE (pg, - "select_category", - "SELECT" - " category_name" - ",category_name_i18n::TEXT" - ",t.product_array AS products" - " FROM merchant_categories mc" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - ",LATERAL (" - " SELECT ARRAY (" - " SELECT " - " mi.product_id AS product_id" - " FROM merchant_product_categories mpc" - " JOIN merchant_inventory mi" - " USING (product_serial)" - " WHERE mpc.category_serial = mc.category_serial" - " ) AS product_array" - " ) t" - " WHERE inst.merchant_id=$1" - " AND mc.category_serial=$2"); - check_connection (pg); - return GNUNET_PQ_eval_prepared_singleton_select ( - pg->conn, - "select_category", - params, - rs); - } + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + check_connection (pg); + TMH_PQ_prepare_anon (pg, + "SELECT" + " category_name" + ",category_name_i18n::TEXT" + ",t.product_array AS products" + " FROM merchant_categories mc" + ",LATERAL (" + " SELECT ARRAY (" + " SELECT " + " mi.product_id AS product_id" + " FROM merchant_product_categories mpc" + " JOIN merchant_inventory mi" + " USING (product_serial)" + " WHERE mpc.category_serial = mc.category_serial" + " ) AS product_array" + " ) t" + " WHERE mc.category_serial=$1"); + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "", + params, + rs); } diff --git a/src/backenddb/select_category_by_name.c b/src/backenddb/select_category_by_name.c @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_category_by_name.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_category_by_name (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *category_name, - json_t **name_i18n, - uint64_t *category_id) +TALER_MERCHANTDB_select_category_by_name ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *category_name, + json_t **name_i18n, + uint64_t *category_id) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (category_name), GNUNET_PQ_query_param_end }; @@ -46,20 +44,19 @@ TALER_MERCHANTDB_select_category_by_name (struct TALER_MERCHANTDB_PostgresContex GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "select_category_by_name", - "SELECT" - " category_serial" - ",category_name_i18n::TEXT" - " FROM merchant_categories mc" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE mi.merchant_id=$1" - " AND mc.category_name=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " category_serial" + ",category_name_i18n::TEXT" + " FROM merchant_categories" + " WHERE category_name=$1"); return GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "select_category_by_name", + "", params, rs); } diff --git a/src/backenddb/select_donau_instance_by_serial.c b/src/backenddb/select_donau_instance_by_serial.c @@ -20,18 +20,17 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_donau_instance_by_serial.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_donau_instance_by_serial (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t serial, - char **donau_url, - uint64_t *charity_id) +TALER_MERCHANTDB_select_donau_instance_by_serial ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t serial, + char **donau_url, + uint64_t *charity_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&serial), @@ -45,17 +44,17 @@ TALER_MERCHANTDB_select_donau_instance_by_serial (struct TALER_MERCHANTDB_Postgr GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "select_donau_instance_by_serial", - "SELECT donau_url" - " ,charity_id" - " FROM merchant_donau_instances" - " WHERE donau_instances_serial = $1"); + TMH_PQ_prepare_anon (pg, + "SELECT donau_url" + " ,charity_id" + " FROM merchant_donau_instances" + " WHERE donau_instances_serial = $1"); return GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "select_donau_instance_by_serial", + "", params, rs); } diff --git a/src/backenddb/select_donau_instances.c b/src/backenddb/select_donau_instances.c @@ -20,8 +20,6 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include <donau/donau_util.h> #include "merchant-database/select_donau_instances.h" @@ -33,6 +31,11 @@ struct SelectDonauInstanceContext { /** + * Database connection. + */ + struct TALER_MERCHANTDB_PostgresContext *pg; + + /** * Function to call with the results. */ TALER_MERCHANTDB_DonauInstanceCallback cb; @@ -63,13 +66,19 @@ select_donau_instance_cb (void *cls, unsigned int num_results) { struct SelectDonauInstanceContext *sdc = cls; + struct TALER_MERCHANTDB_PostgresContext *pg = sdc->pg; + struct DONAU_CharityPublicKeyP charity_pub_key; + GNUNET_static_assert (sizeof (charity_pub_key) == + sizeof (pg->current_merchant_pub)); + memcpy (&charity_pub_key, + &pg->current_merchant_pub, + sizeof (charity_pub_key)); for (unsigned int i = 0; i < num_results; i++) { uint64_t donau_instance_serial; char *donau_url; char *charity_name; - struct DONAU_CharityPublicKeyP charity_pub_key; uint64_t charity_id; struct TALER_Amount charity_max_per_year; struct TALER_Amount charity_receipts_to_date; @@ -82,8 +91,6 @@ select_donau_instance_cb (void *cls, &donau_url), GNUNET_PQ_result_spec_string ("charity_name", &charity_name), - GNUNET_PQ_result_spec_auto_from_type ("charity_pub_key", - &charity_pub_key), GNUNET_PQ_result_spec_uint64 ("charity_id", &charity_id), TALER_PQ_result_spec_amount_with_currency ("charity_max_per_year", @@ -131,43 +138,41 @@ TALER_MERCHANTDB_select_donau_instances ( void *cb_cls) { struct SelectDonauInstanceContext sdc = { + .pg = pg, .cb = cb, .cb_cls = cb_cls, /* Can be overwritten by the select_donau_instance_cb */ .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "select_donau_instances", - "SELECT" - " di.donau_instances_serial" - ",di.donau_url" - ",di.charity_name" - ",mi.merchant_pub AS charity_pub_key" - ",di.charity_id" - ",di.charity_max_per_year" - ",di.charity_receipts_to_date" - ",di.current_year" - ",dk.keys_json::TEXT" - " FROM merchant_donau_instances di" - " LEFT JOIN merchant_donau_keys dk" - " ON di.donau_url = dk.donau_url" - " JOIN merchant_instances mi" - " ON di.merchant_instance_serial = mi.merchant_serial" - " WHERE mi.merchant_id = $1"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " di.donau_instances_serial" + ",di.donau_url" + ",di.charity_name" + ",di.charity_id" + ",di.charity_max_per_year" + ",di.charity_receipts_to_date" + ",di.current_year" + ",dk.keys_json::TEXT" + " FROM merchant_donau_instances di" + " LEFT JOIN merchant_donau_keys dk" + " ON di.donau_url = dk.donau_url"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "select_donau_instances", + "", params, &select_donau_instance_cb, &sdc); -/* If there was an error inside select_donau_instance_cb, return a hard error. */ + /* If there was an error inside select_donau_instance_cb, return a hard error. */ if (sdc.extract_failed) return GNUNET_DB_STATUS_HARD_ERROR; diff --git a/src/backenddb/select_donau_instances_filtered.c b/src/backenddb/select_donau_instances_filtered.c @@ -20,12 +20,11 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_donau_instances_filtered.h" #include "helper.h" + /** * Context for select_donau_instances_filtered(). */ @@ -101,7 +100,6 @@ TALER_MERCHANTDB_select_donau_instances_filtered ( /* Can be overwritten by the select_donau_instance_cb */ .extract_failed = false, }; - struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (currency), GNUNET_PQ_query_param_end @@ -109,15 +107,12 @@ TALER_MERCHANTDB_select_donau_instances_filtered ( enum GNUNET_DB_QueryStatus qs; check_connection (pg); - PREPARE (pg, - "select_donau_instances_filtered", - "SELECT" - " donau_url" - " FROM merchant_donau_instances" - " WHERE (charity_max_per_year).curr = $1"); - + TMH_PQ_prepare_anon (pg, + "SELECT donau_url" + " FROM merchant_donau_instances" + " WHERE (charity_max_per_year).curr = $1"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "select_donau_instances_filtered", + "", params, &select_donau_instance_cb, &sdc); @@ -125,6 +120,5 @@ TALER_MERCHANTDB_select_donau_instances_filtered ( /* If there was an error inside select_donau_instance_cb, return a hard error. */ if (sdc.extract_failed) return GNUNET_DB_STATUS_HARD_ERROR; - return qs; } diff --git a/src/backenddb/select_exchange_keys.c b/src/backenddb/select_exchange_keys.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include <taler/taler_exchange_service.h> #include "merchant-database/select_exchange_keys.h" @@ -57,7 +55,7 @@ TALER_MERCHANTDB_select_exchange_keys ( "SELECT" " first_retry" ",keys_json::TEXT" - " FROM merchant_exchange_keys" + " FROM merchant.merchant_exchange_keys" " WHERE exchange_url=$1;"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "select_exchange_keys", diff --git a/src/backenddb/select_exchanges.c b/src/backenddb/select_exchanges.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_exchanges.h" #include "helper.h" @@ -136,7 +134,7 @@ TALER_MERCHANTDB_select_exchanges ( " ,keys_json::TEXT" " ,exchange_http_status" " ,exchange_ec_code" - " FROM merchant_exchange_keys"); + " FROM merchant.merchant_exchange_keys"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, "select_exchanges", diff --git a/src/backenddb/select_login_token.c b/src/backenddb/select_login_token.c @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_login_token.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_login_token (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - const struct TALER_MERCHANTDB_LoginTokenP *token, - struct GNUNET_TIME_Timestamp *expiration_time, - uint32_t *validity_scope) +TALER_MERCHANTDB_select_login_token ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + const struct TALER_MERCHANTDB_LoginTokenP *token, + struct GNUNET_TIME_Timestamp *expiration_time, + uint32_t *validity_scope) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_auto_from_type (token), GNUNET_PQ_query_param_end }; @@ -46,20 +44,18 @@ TALER_MERCHANTDB_select_login_token (struct TALER_MERCHANTDB_PostgresContext *pg GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "select_login_token", - "SELECT" - " expiration_time" - ",validity_scope" - " FROM merchant_login_tokens" - " WHERE token=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " expiration_time" + ",validity_scope" + " FROM merchant_login_tokens" + " WHERE token=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_login_token", + "", params, rs); } diff --git a/src/backenddb/select_money_pot.c b/src/backenddb/select_money_pot.c @@ -19,24 +19,22 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_money_pot.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t money_pot_id, - char **name, - char **description, - size_t *pot_total_len, - struct TALER_Amount **pot_totals) +TALER_MERCHANTDB_select_money_pot ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t money_pot_id, + char **name, + char **description, + size_t *pot_total_len, + struct TALER_Amount **pot_totals) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&money_pot_id), GNUNET_PQ_query_param_end }; @@ -53,21 +51,20 @@ TALER_MERCHANTDB_select_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "select_money_pot", - "SELECT" - " mp.money_pot_name" - " ,mp.money_pot_description" - " ,mp.pot_totals" - " FROM merchant_money_pots mp" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE merchant_id=$1" - " AND money_pot_serial=$2;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " money_pot_name" + " ,money_pot_description" + " ,pot_totals" + " FROM merchant_money_pots" + " WHERE money_pot_serial=$1;"); return GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "select_money_pot", + "", params, rs); } diff --git a/src/backenddb/select_money_pots.c b/src/backenddb/select_money_pots.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_money_pots.h" #include "helper.h" @@ -107,12 +105,13 @@ lookup_money_pots_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_money_pots (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - int64_t limit, - uint64_t offset, - TALER_MERCHANTDB_MoneyPotsCallback cb, - void *cb_cls) +TALER_MERCHANTDB_select_money_pots ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + int64_t limit, + uint64_t offset, + TALER_MERCHANTDB_MoneyPotsCallback cb, + void *cb_cls) { uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); struct LookupMoneyPotsContext plc = { @@ -123,45 +122,43 @@ TALER_MERCHANTDB_select_money_pots (struct TALER_MERCHANTDB_PostgresContext *pg, .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&offset), GNUNET_PQ_query_param_uint64 (&plimit), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_money_pots_asc", - "SELECT" - " money_pot_serial" - " ,money_pot_name" - " ,pot_totals" - " FROM merchant_money_pots" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND money_pot_serial > $2" - " ORDER BY money_pot_serial ASC" - " LIMIT $3"); - PREPARE (pg, - "lookup_money_pots_desc", - "SELECT" - " money_pot_serial" - " ,money_pot_name" - " ,pot_totals" - " FROM merchant_money_pots" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND money_pot_serial < $2" - " ORDER BY money_pot_serial DESC" - " LIMIT $3"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " money_pot_serial" + " ,money_pot_name" + " ,pot_totals" + " FROM merchant_money_pots" + " WHERE money_pot_serial > $1" + " ORDER BY money_pot_serial ASC" + " LIMIT $2"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " money_pot_serial" + " ,money_pot_name" + " ,pot_totals" + " FROM merchant_money_pots" + " WHERE money_pot_serial < $1" + " ORDER BY money_pot_serial DESC" + " LIMIT $2"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "lookup_money_pots_asc" - : "lookup_money_pots_desc", + "", params, &lookup_money_pots_cb, &plc); diff --git a/src/backenddb/select_open_transfers.c b/src/backenddb/select_open_transfers.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_open_transfers.h" #include "helper.h" @@ -77,17 +75,17 @@ open_transfers_cb (void *cls, struct TALER_WireTransferIdentifierRawP wtid; struct GNUNET_TIME_Absolute retry_time; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_uint64 ("expected_credit_serial", + GNUNET_PQ_result_spec_uint64 ("out_expected_credit_serial", &rowid), - GNUNET_PQ_result_spec_string ("instance_id", + GNUNET_PQ_result_spec_string ("out_instance_id", &instance_id), - GNUNET_PQ_result_spec_string ("exchange_url", + GNUNET_PQ_result_spec_string ("out_exchange_url", &exchange_url), - GNUNET_PQ_result_spec_string ("payto_uri", + GNUNET_PQ_result_spec_string ("out_payto_uri", &payto_uri.full_payto), - GNUNET_PQ_result_spec_auto_from_type ("wtid", + GNUNET_PQ_result_spec_auto_from_type ("out_wtid", &wtid), - GNUNET_PQ_result_spec_absolute_time ("retry_time", + GNUNET_PQ_result_spec_absolute_time ("out_retry_time", &retry_time), GNUNET_PQ_result_spec_end }; @@ -134,20 +132,13 @@ TALER_MERCHANTDB_select_open_transfers ( PREPARE (pg, "select_open_transfers", "SELECT" - " met.expected_credit_serial" - ",mi.merchant_id AS instance_id" - ",met.exchange_url" - ",ma.payto_uri" - ",met.wtid" - ",met.retry_time" - " FROM merchant_expected_transfers met" - " JOIN merchant_accounts ma" - " USING (account_serial)" - " JOIN merchant_instances mi" - " ON (ma.merchant_serial=mi.merchant_serial)" - " WHERE retry_needed" - " ORDER BY retry_time ASC" - " LIMIT $1;"); + " out_expected_credit_serial" + " ,out_instance_id" + " ,out_exchange_url" + " ,out_payto_uri" + " ,out_wtid" + " ,out_retry_time" + " FROM merchant.select_open_transfers($1)"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, "select_open_transfers", diff --git a/src/backenddb/select_open_transfers.sql b/src/backenddb/select_open_transfers.sql @@ -0,0 +1,77 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.select_open_transfers; +CREATE FUNCTION merchant.select_open_transfers(IN p_limit INT8) +RETURNS TABLE( + out_expected_credit_serial INT8, + out_instance_id TEXT, + out_exchange_url TEXT, + out_payto_uri TEXT, + out_wtid BYTEA, + out_retry_time INT8) +LANGUAGE plpgsql +AS $FN$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; + remaining INT8 := p_limit; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + FROM merchant.merchant_instances + LOOP + EXIT WHEN remaining <= 0; + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + FOR inner_rec IN + EXECUTE format( + 'SELECT' + ' met.expected_credit_serial AS ecs' + ' ,met.exchange_url AS exu' + ' ,ma.payto_uri AS pu' + ' ,met.wtid AS wt' + ' ,met.retry_time AS rt' + ' FROM %I.merchant_expected_transfers met' + ' JOIN %I.merchant_accounts ma USING (account_serial)' + ' WHERE retry_needed' + ' ORDER BY retry_time ASC' + ' LIMIT $1', s, s) + USING remaining + LOOP + out_expected_credit_serial := inner_rec.ecs; + out_instance_id := rec.merchant_id; + out_exchange_url := inner_rec.exu; + out_payto_uri := inner_rec.pu; + out_wtid := inner_rec.wt; + out_retry_time := inner_rec.rt; + RETURN NEXT; + remaining := remaining - 1; + EXIT WHEN remaining <= 0; + END LOOP; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; +END +$FN$; +COMMENT ON FUNCTION merchant.select_open_transfers(INT8) + IS 'Returns up to p_limit retry-needed expected_transfer rows across all' + ' instance schemas, joined with the per-instance merchant_accounts.'; diff --git a/src/backenddb/select_order_blinded_sigs.c b/src/backenddb/select_order_blinded_sigs.c @@ -19,8 +19,6 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_order_blinded_sigs.h" #include "helper.h" @@ -90,35 +88,35 @@ restore_sig_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_order_blinded_sigs (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *order_id, - TALER_MERCHANTDB_BlindedSigCallback cb, - void *cb_cls) +TALER_MERCHANTDB_select_order_blinded_sigs ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *order_id, + TALER_MERCHANTDB_BlindedSigCallback cb, + void *cb_cls) { struct SelectBlindedSigsContext ctx = { .cb = cb, .cb_cls = cb_cls, .qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS }; - struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (order_id), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "select_blinded_sigs", - "SELECT" - " motbs.token_blinded_signature" - " ,motbs.token_hash" - " FROM merchant_order_token_blinded_sigs AS motbs" - " JOIN merchant_contract_terms AS mct USING (order_serial)" - " WHERE mct.order_id = $1" - " ORDER BY motbs.token_index ASC"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " motbs.token_blinded_signature" + " ,motbs.token_hash" + " FROM merchant_order_token_blinded_sigs AS motbs" + " JOIN merchant_contract_terms AS mct USING (order_serial)" + " WHERE mct.order_id = $1" + " ORDER BY motbs.token_index ASC"); return GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - "select_blinded_sigs", + "", params, &restore_sig_cb, &ctx); diff --git a/src/backenddb/select_otp.c b/src/backenddb/select_otp.c @@ -19,71 +19,52 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_otp.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_otp (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *otp_id, - struct TALER_MERCHANTDB_OtpDeviceDetails *td) +TALER_MERCHANTDB_select_otp ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *otp_id, + struct TALER_MERCHANTDB_OtpDeviceDetails *td) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (otp_id), GNUNET_PQ_query_param_end }; + uint32_t pos32; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("otp_description", + &td->otp_description), + GNUNET_PQ_result_spec_uint64 ("otp_ctr", + &td->otp_ctr), + GNUNET_PQ_result_spec_string ("otp_key", + &td->otp_key), + GNUNET_PQ_result_spec_uint32 ("otp_algorithm", + &pos32), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; - PREPARE (pg, - "select_otp", - "SELECT" - " otp_description" - ",otp_ctr" - ",otp_key" - ",otp_algorithm" - " FROM merchant_otp_devices" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_otp_devices.otp_id=$2"); - if (NULL == td) - { - struct GNUNET_PQ_ResultSpec rs_null[] = { - GNUNET_PQ_result_spec_end - }; - - check_connection (pg); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_otp", - params, - rs_null); - } - else - { - uint32_t pos32; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("otp_description", - &td->otp_description), - GNUNET_PQ_result_spec_uint64 ("otp_ctr", - &td->otp_ctr), - GNUNET_PQ_result_spec_string ("otp_key", - &td->otp_key), - GNUNET_PQ_result_spec_uint32 ("otp_algorithm", - &pos32), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - check_connection (pg); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_otp", - params, - rs); - td->otp_algorithm = (enum TALER_MerchantConfirmationAlgorithm) pos32; - return qs; - } + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + check_connection (pg); + TMH_PQ_prepare_anon (pg, + "SELECT" + " otp_description" + ",otp_ctr" + ",otp_key" + ",otp_algorithm" + " FROM merchant_otp_devices" + " WHERE otp_id=$1"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); + td->otp_algorithm = (enum TALER_MerchantConfirmationAlgorithm) pos32; + return qs; } diff --git a/src/backenddb/select_otp_serial.c b/src/backenddb/select_otp_serial.c @@ -19,21 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_otp_serial.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_otp_serial (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *otp_id, - uint64_t *serial) +TALER_MERCHANTDB_select_otp_serial ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *otp_id, + uint64_t *serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (otp_id), GNUNET_PQ_query_param_end }; @@ -43,18 +41,17 @@ TALER_MERCHANTDB_select_otp_serial (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_result_spec_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "select_otp_serial", - "SELECT" - " otp_serial" - " FROM merchant_otp_devices" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND merchant_otp_devices.otp_id=$2"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " otp_serial" + " FROM merchant_otp_devices" + " WHERE otp_id=$1"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_otp_serial", + "", params, rs); } diff --git a/src/backenddb/select_product_groups.c b/src/backenddb/select_product_groups.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_product_groups.h" #include "helper.h" @@ -97,12 +95,13 @@ lookup_product_groups_cb (void *cls, enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_product_groups (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - int64_t limit, - uint64_t offset, - TALER_MERCHANTDB_ProductGroupsCallback cb, - void *cb_cls) +TALER_MERCHANTDB_select_product_groups ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + int64_t limit, + uint64_t offset, + TALER_MERCHANTDB_ProductGroupsCallback cb, + void *cb_cls) { uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); struct LookupProductGroupsContext plc = { @@ -112,45 +111,43 @@ TALER_MERCHANTDB_select_product_groups (struct TALER_MERCHANTDB_PostgresContext .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&offset), GNUNET_PQ_query_param_uint64 (&plimit), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "lookup_product_groups_asc", - "SELECT" - " product_group_serial" - " ,product_group_name" - " ,product_group_description" - " FROM merchant_product_groups" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND product_group_serial > $2" - " ORDER BY product_group_serial ASC" - " LIMIT $3"); - PREPARE (pg, - "lookup_product_groups_desc", - "SELECT" - " product_group_serial" - " ,product_group_name" - " ,product_group_description" - " FROM merchant_product_groups" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND product_group_serial < $2" - " ORDER BY product_group_serial DESC" - " LIMIT $3"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " product_group_serial" + " ,product_group_name" + " ,product_group_description" + " FROM merchant_product_groups" + " WHERE product_group_serial > $1" + " ORDER BY product_group_serial ASC" + " LIMIT $2"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " product_group_serial" + " ,product_group_name" + " ,product_group_description" + " FROM merchant_product_groups" + " WHERE product_group_serial < $1" + " ORDER BY product_group_serial DESC" + " LIMIT $2"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "lookup_product_groups_asc" - : "lookup_product_groups_desc", + "", params, &lookup_product_groups_cb, &plc); diff --git a/src/backenddb/select_report.c b/src/backenddb/select_report.c @@ -19,29 +19,28 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_report.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_report (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t report_id, - char **report_program_section, - char **report_description, - char **mime_type, - char **data_source, - char **target_address, - struct GNUNET_TIME_Relative *frequency, - struct GNUNET_TIME_Relative *frequency_shift, - struct GNUNET_TIME_Absolute *next_transmission, - enum TALER_ErrorCode *last_error_code, - char **last_error_detail) +TALER_MERCHANTDB_select_report ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t report_id, + char **report_program_section, + char **report_description, + char **mime_type, + char **data_source, + char **target_address, + struct GNUNET_TIME_Relative *frequency, + struct GNUNET_TIME_Relative *frequency_shift, + struct GNUNET_TIME_Absolute *next_transmission, + enum TALER_ErrorCode *last_error_code, + char **last_error_detail) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&report_id), GNUNET_PQ_query_param_end }; @@ -75,30 +74,29 @@ TALER_MERCHANTDB_select_report (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); *last_error_detail = NULL; code = TALER_EC_NONE; check_connection (pg); - PREPARE (pg, - "select_report", - "SELECT" - " mr.report_program_section" - " ,mr.report_description" - " ,mr.mime_type" - " ,mr.data_source" - " ,mr.target_address" - " ,mr.frequency" - " ,mr.frequency_shift" - " ,mr.next_transmission" - " ,mr.last_error_code" - " ,mr.last_error_detail" - " FROM merchant_reports mr" - " JOIN merchant_instances mi" - " USING (merchant_serial)" - " WHERE merchant_id=$1" - " AND report_serial=$2;"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " report_program_section" + " ,report_description" + " ,mime_type" + " ,data_source" + " ,target_address" + " ,frequency" + " ,frequency_shift" + " ,next_transmission" + " ,last_error_code" + " ,last_error_detail" + " FROM merchant_reports" + " WHERE report_serial=$1;"); qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, - "select_report", + "", params, rs); *last_error_code = (enum TALER_ErrorCode) code; diff --git a/src/backenddb/select_reports.c b/src/backenddb/select_reports.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_reports.h" #include "helper.h" @@ -113,47 +111,45 @@ TALER_MERCHANTDB_select_reports ( .extract_failed = false, }; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&offset), GNUNET_PQ_query_param_uint64 (&plimit), GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "select_reports_asc", - "SELECT" - " report_serial" - " ,report_description" - " ,frequency" - " FROM merchant_reports" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND report_serial > $2" - " AND NOT one_shot_hidden" - " ORDER BY report_serial ASC" - " LIMIT $3"); - PREPARE (pg, - "select_reports_desc", - "SELECT" - " report_serial" - " ,report_description" - " ,frequency" - " FROM merchant_reports" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE merchant_instances.merchant_id=$1" - " AND report_serial < $2" - " AND NOT one_shot_hidden" - " ORDER BY report_serial DESC" - " LIMIT $3"); + if (limit > 0) + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " report_serial" + " ,report_description" + " ,frequency" + " FROM merchant_reports" + " WHERE report_serial > $1" + " AND NOT one_shot_hidden" + " ORDER BY report_serial ASC" + " LIMIT $2"); + } + else + { + TMH_PQ_prepare_anon (pg, + "SELECT" + " report_serial" + " ,report_description" + " ,frequency" + " FROM merchant_reports" + " WHERE report_serial < $1" + " AND NOT one_shot_hidden" + " ORDER BY report_serial DESC" + " LIMIT $2"); + } qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, - (limit > 0) - ? "select_reports_asc" - : "select_reports_desc", + "", params, &select_reports_cb, &plc); diff --git a/src/backenddb/select_unit.c b/src/backenddb/select_unit.c @@ -19,116 +19,90 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_unit.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_unit (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *unit_id, - struct TALER_MERCHANTDB_UnitDetails *ud) +TALER_MERCHANTDB_select_unit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *unit_id, + struct TALER_MERCHANTDB_UnitDetails *ud) { enum GNUNET_DB_QueryStatus qs; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (unit_id), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("unit_serial", + &ud->unit_serial), + GNUNET_PQ_result_spec_string ("unit", + &ud->unit), + GNUNET_PQ_result_spec_string ("unit_name_long", + &ud->unit_name_long), + GNUNET_PQ_result_spec_string ("unit_name_short", + &ud->unit_name_short), + TALER_PQ_result_spec_json ("unit_name_long_i18n", + &ud->unit_name_long_i18n), + TALER_PQ_result_spec_json ("unit_name_short_i18n", + &ud->unit_name_short_i18n), + GNUNET_PQ_result_spec_bool ("unit_allow_fraction", + &ud->unit_allow_fraction), + GNUNET_PQ_result_spec_uint32 ("unit_precision_level", + &ud->unit_precision_level), + GNUNET_PQ_result_spec_bool ("unit_active", + &ud->unit_active), + GNUNET_PQ_result_spec_bool ("unit_builtin", + &ud->unit_builtin), + GNUNET_PQ_result_spec_end + }; - if (NULL == ud) - { - struct GNUNET_PQ_ResultSpec rs_null[] = { - GNUNET_PQ_result_spec_end - }; - - check_connection (pg); - return GNUNET_PQ_eval_prepared_singleton_select ( - pg->conn, - "select_unit", - params, - rs_null); - } - else - { - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_uint64 ("unit_serial", - &ud->unit_serial), - GNUNET_PQ_result_spec_string ("unit", - &ud->unit), - GNUNET_PQ_result_spec_string ("unit_name_long", - &ud->unit_name_long), - GNUNET_PQ_result_spec_string ("unit_name_short", - &ud->unit_name_short), - TALER_PQ_result_spec_json ("unit_name_long_i18n", - &ud->unit_name_long_i18n), - TALER_PQ_result_spec_json ("unit_name_short_i18n", - &ud->unit_name_short_i18n), - GNUNET_PQ_result_spec_bool ("unit_allow_fraction", - &ud->unit_allow_fraction), - GNUNET_PQ_result_spec_uint32 ("unit_precision_level", - &ud->unit_precision_level), - GNUNET_PQ_result_spec_bool ("unit_active", - &ud->unit_active), - GNUNET_PQ_result_spec_bool ("unit_builtin", - &ud->unit_builtin), - GNUNET_PQ_result_spec_end - }; - - check_connection (pg); - - PREPARE (pg, - "select_unit_custom", - "SELECT" - " cu.unit_serial" - " ,cu.unit" - " ,cu.unit_name_long" - " ,cu.unit_name_short" - " ,cu.unit_name_long_i18n" - " ,cu.unit_name_short_i18n" - " ,cu.unit_allow_fraction" - " ,cu.unit_precision_level" - " ,cu.unit_active" - " ,FALSE AS unit_builtin" - " FROM merchant_custom_units cu" - " JOIN merchant_instances inst" - " USING (merchant_serial)" - " WHERE inst.merchant_id=$1" - " AND cu.unit=$2"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_unit_custom", + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + check_connection (pg); + // FIXME: combine both SELECT statements into a single statement! + TMH_PQ_prepare_anon (pg, + "SELECT" + " cu.unit_serial" + " ,cu.unit" + " ,cu.unit_name_long" + " ,cu.unit_name_short" + " ,cu.unit_name_long_i18n" + " ,cu.unit_name_short_i18n" + " ,cu.unit_allow_fraction" + " ,cu.unit_precision_level" + " ,cu.unit_active" + " ,FALSE AS unit_builtin" + " FROM merchant_custom_units cu" + " WHERE cu.unit=$1"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", + params, + rs); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) + return qs; + TMH_PQ_prepare_anon (pg, + "SELECT" + " bu.unit_serial" + " ,bu.unit" + " ,bu.unit_name_long" + " ,bu.unit_name_short" + " ,bu.unit_name_long_i18n" + " ,bu.unit_name_short_i18n" + " ,COALESCE(bo.override_allow_fraction, bu.unit_allow_fraction)" + " ,COALESCE(bo.override_precision_level, bu.unit_precision_level)" + " ,COALESCE(bo.override_active, bu.unit_active)" + " ,TRUE AS unit_builtin" + " FROM merchant.merchant_builtin_units bu" + " LEFT JOIN merchant_builtin_unit_overrides bo" + " ON bo.builtin_unit_serial = bu.unit_serial" + " WHERE bu.unit=$1"); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "", params, rs); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs) - return qs; - - PREPARE (pg, - "select_unit_builtin", - "SELECT" - " bu.unit_serial" - " ,bu.unit" - " ,bu.unit_name_long" - " ,bu.unit_name_short" - " ,bu.unit_name_long_i18n" - " ,bu.unit_name_short_i18n" - " ,COALESCE(bo.override_allow_fraction, bu.unit_allow_fraction)" - " ,COALESCE(bo.override_precision_level, bu.unit_precision_level)" - " ,COALESCE(bo.override_active, bu.unit_active)" - " ,TRUE AS unit_builtin" - " FROM merchant_builtin_units bu" - " JOIN merchant_instances inst" - " ON TRUE" - " LEFT JOIN merchant_builtin_unit_overrides bo" - " ON bo.builtin_unit_serial = bu.unit_serial" - " AND bo.merchant_serial = inst.merchant_serial" - " WHERE inst.merchant_id=$1" - " AND bu.unit=$2"); - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "select_unit_builtin", - params, - rs); - } } diff --git a/src/backenddb/select_wirewatch_accounts.c b/src/backenddb/select_wirewatch_accounts.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/select_wirewatch_accounts.h" #include "helper.h" @@ -71,17 +69,17 @@ handle_results (void *cls, json_t *credential; uint64_t last_serial; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_string ("merchant_id", + GNUNET_PQ_result_spec_string ("out_merchant_id", &instance), - GNUNET_PQ_result_spec_string ("payto_uri", + GNUNET_PQ_result_spec_string ("out_payto_uri", &payto_uri.full_payto), - GNUNET_PQ_result_spec_string ("credit_facade_url", + GNUNET_PQ_result_spec_string ("out_credit_facade_url", &facade_url), GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_json ("credit_facade_credentials", + TALER_PQ_result_spec_json ("out_credit_facade_credentials", &credential), NULL), - GNUNET_PQ_result_spec_uint64 ("last_bank_serial", + GNUNET_PQ_result_spec_uint64 ("out_last_bank_serial", &last_serial), GNUNET_PQ_result_spec_end }; @@ -124,16 +122,12 @@ TALER_MERCHANTDB_select_wirewatch_accounts ( PREPARE (pg, "select_wirewatch_progress", "SELECT" - " last_bank_serial" - ",merchant_id" - ",payto_uri" - ",credit_facade_url" - ",credit_facade_credentials::TEXT" - " FROM merchant_accounts" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE active" - " AND credit_facade_url IS NOT NULL"); + " out_last_bank_serial" + " ,out_merchant_id" + " ,out_payto_uri" + " ,out_credit_facade_url" + " ,out_credit_facade_credentials::TEXT" + " FROM merchant.select_wirewatch_accounts()"); check_connection (pg); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "select_wirewatch_progress", diff --git a/src/backenddb/select_wirewatch_accounts.sql b/src/backenddb/select_wirewatch_accounts.sql @@ -0,0 +1,67 @@ +-- +-- 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/> +-- + +DROP FUNCTION IF EXISTS merchant.select_wirewatch_accounts(); +CREATE FUNCTION merchant.select_wirewatch_accounts() +RETURNS TABLE( + out_merchant_id TEXT, + out_payto_uri TEXT, + out_credit_facade_url TEXT, + out_credit_facade_credentials JSONB, + out_last_bank_serial INT8) +LANGUAGE plpgsql +AS $FN$ +DECLARE + rec RECORD; + s TEXT; + inner_rec RECORD; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; + BEGIN + FOR inner_rec IN + EXECUTE format( + 'SELECT' + ' payto_uri AS pu' + ' ,credit_facade_url AS cfu' + ' ,credit_facade_credentials AS cfc' + ' ,last_bank_serial AS lbs' + ' FROM %I.merchant_accounts' + ' WHERE active' + ' AND credit_facade_url IS NOT NULL', s) + LOOP + out_merchant_id := rec.merchant_id; + out_payto_uri := inner_rec.pu; + out_credit_facade_url := inner_rec.cfu; + out_credit_facade_credentials := inner_rec.cfc; + out_last_bank_serial := inner_rec.lbs; + RETURN NEXT; + END LOOP; + EXCEPTION + WHEN undefined_table + THEN + NULL; + END; + END LOOP; +END +$FN$; +COMMENT ON FUNCTION merchant.select_wirewatch_accounts() + IS 'Returns one row per active credit-facade-enabled merchant_account' + ' across all instance schemas.'; diff --git a/src/backenddb/set_instance.c b/src/backenddb/set_instance.c @@ -0,0 +1,99 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser 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 src/backenddb/set_instance.c + * @brief Implementation of TALER_MERCHANTDB_set_instance + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_pq_lib.h> +#include <taler/taler_pq_lib.h> +#include "merchant-database/set_instance.h" +#include "helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_set_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id) +{ + uint64_t serial; + struct TALER_MerchantPublicKeyP merchant_pub; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("merchant_serial", + &serial), + GNUNET_PQ_result_spec_auto_from_type ("merchant_pub", + &merchant_pub), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + char sp_sql[128]; + struct GNUNET_PQ_ExecuteStatement es[] = { + GNUNET_PQ_make_execute (sp_sql), + GNUNET_PQ_EXECUTE_STATEMENT_END + }; + + if (NULL == instance_id) + { + pg->current_merchant_id = 0; + pg->current_merchant_serial = 0; + memset (&pg->current_merchant_pub, + 0, + sizeof (pg->current_merchant_pub)); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + if ( (NULL != pg->current_merchant_id) && + (0 == strcmp (pg->current_merchant_id, + instance_id)) ) + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + check_connection (pg); + PREPARE (pg, + "set_instance_lookup_serial", + "SELECT merchant_serial" + " ,merchant_pub" + " FROM merchant.merchant_instances" + " WHERE merchant_id=$1"); + { + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_end + }; + + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "set_instance_lookup_serial", + params, + rs); + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + return qs; + GNUNET_snprintf (sp_sql, + sizeof (sp_sql), + "SET search_path TO merchant_instance_%llu", + (unsigned long long) serial); + if (GNUNET_OK != + GNUNET_PQ_exec_statements (pg->conn, + es)) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + GNUNET_free (pg->current_merchant_id); + pg->current_merchant_id = GNUNET_strdup (instance_id); + pg->current_merchant_serial = serial; + pg->current_merchant_pub = merchant_pub; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} diff --git a/src/backenddb/solve_mfa_challenge.c b/src/backenddb/solve_mfa_challenge.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "taler/taler_merchant_util.h" #include "merchant-database/solve_mfa_challenge.h" @@ -60,15 +58,14 @@ TALER_MERCHANTDB_solve_mfa_challenge ( safe values, even though these should not be used with qs <= 0 */ *solved = false; *retry_counter = 0; - PREPARE (pg, - "solve_mfa_challenge", - "SELECT" - " out_solved" - " ,out_retry_counter" - " FROM merchant_do_solve_mfa_challenge" - " ($1, $2, $3, $4);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_solved" + " ,out_retry_counter" + " FROM merchant.merchant_do_solve_mfa_challenge" + " ($1, $2, $3, $4);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "solve_mfa_challenge", + "", params, rs); if (qs <= 0) diff --git a/src/backenddb/solve_mfa_challenge.sql b/src/backenddb/solve_mfa_challenge.sql @@ -0,0 +1,84 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_solve_mfa_challenge; +CREATE FUNCTION merchant_do_solve_mfa_challenge ( + IN in_challenge_id INT8, + IN in_h_body BYTEA, + IN in_solution TEXT, + IN in_now INT8, + OUT out_solved BOOLEAN, + OUT out_retry_counter INT4 +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_confirmation_date INT8; +DECLARE + my_rec RECORD; +BEGIN + + -- Check if challenge exists and matches + SELECT + tc.confirmation_date + ,tc.retry_counter + ,(tc.code = in_solution) AS solved + INTO + my_rec + FROM merchant.tan_challenges tc + WHERE tc.challenge_id = in_challenge_id + AND tc.h_body = in_h_body + AND tc.expiration_date > in_now; + + IF NOT FOUND + THEN + out_solved = FALSE; + RETURN; + END IF; + + my_confirmation_date = my_rec.confirmation_date; + out_retry_counter = my_rec.retry_counter; + out_solved = my_rec.solved; + + -- Check if already solved before + IF my_confirmation_date IS NOT NULL + THEN + out_solved = TRUE; + RETURN; + END IF; + + IF (0 = out_retry_counter) + THEN + out_solved = FALSE; + RETURN; + END IF; + + IF out_solved + THEN + -- Newly solved, update DB! + my_confirmation_date = in_now; + UPDATE merchant.tan_challenges + SET confirmation_date = my_confirmation_date + WHERE challenge_id = in_challenge_id; + ELSE + -- Failed to solve, decrement retry counter + out_retry_counter = out_retry_counter - 1; + UPDATE merchant.tan_challenges + SET retry_counter = out_retry_counter + WHERE challenge_id = in_challenge_id; + END IF; +END; +$$; diff --git a/src/backenddb/sql-schema/amalgamate-sql.sh b/src/backenddb/sql-schema/amalgamate-sql.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# 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/> + +set -eu + +if [ $# -lt 1 ]; then + echo "Usage: $0 SQLFILES..." >&2 + exit 1 +fi + +cat <<'EOF' +-- +-- This file is part of TALER +-- Copyright (C) 2014--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/> +-- +EOF + +for x in $@; do + echo -- generated from $x + # Remove SQL comments and empty lines + cat $x | sed -e "s/--.*//" | awk 'NF' +done diff --git a/src/backenddb/sql-schema/drop.sql b/src/backenddb/sql-schema/drop.sql @@ -28,6 +28,21 @@ WITH xpatches AS ( FROM xpatches; +-- Drop all per-instance schemas created by the per-instance trigger. +DO $$ +DECLARE + r RECORD; +BEGIN + FOR r IN + SELECT nspname + FROM pg_namespace + WHERE nspname LIKE 'merchant_instance_%' + LOOP + EXECUTE format('DROP SCHEMA %I CASCADE', r.nspname); + END LOOP; +END +$$; + DROP SCHEMA merchant CASCADE; -- And we're out of here... diff --git a/src/backenddb/sql-schema/gen-procedures.sh b/src/backenddb/sql-schema/gen-procedures.sh @@ -16,8 +16,8 @@ set -eu -if [ $# -lt 1 ]; then - echo "Usage: $0 SQLFILES..." >&2 +if [ $# -lt 2 ]; then + echo "Usage: $0 SCHEMANAME SQLFILES..." >&2 exit 1 fi @@ -41,10 +41,12 @@ cat <<'EOF' -- BEGIN; +EOF -SET search_path TO merchant; +echo "CREATE SCHEMA IF NOT EXISTS $1;" +echo "SET search_path TO $1;" +shift 1 -EOF # Output procedures, stripping comments @@ -54,29 +56,4 @@ for x in $@; do cat $x | sed -e "s/--.*//" | awk 'NF' done -# Output epilogue - -cat <<'EOF' --- Epilogue -DROP PROCEDURE IF EXISTS merchant_do_gc; -CREATE PROCEDURE merchant_do_gc(in_now INT8) -LANGUAGE plpgsql -AS $$ -BEGIN - DELETE FROM merchant_instances - WHERE validation_needed - AND validation_expiration < in_now; - CALL merchant_statistic_amount_gc (); - CALL merchant_statistic_bucket_gc (); - CALL merchant_statistic_counter_gc (); - - DELETE FROM tan_challenges - WHERE expiration_date < in_now; - DELETE FROM merchant_unclaim_signatures - WHERE expiration_time < in_now; -END $$; -COMMENT ON PROCEDURE merchant_do_gc - IS 'calls all other garbage collection subroutines'; - -COMMIT; -EOF +echo "COMMIT;" diff --git a/src/backenddb/sql-schema/merchant-0013.sql b/src/backenddb/sql-schema/merchant-0013.sql @@ -68,22 +68,6 @@ COMMENT ON COLUMN merchant_token_family_keys.private_key_deleted_at COMMENT ON COLUMN merchant_token_family_keys.private_key_created_at IS 'Specifies when the private key was created. Not terribly useful, mostly for debugging.'; --- Function to replace placeholders in a string with a given value -CREATE 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; - CREATE FUNCTION handle_inventory_changes() RETURNS TRIGGER AS $$ BEGIN diff --git a/src/backenddb/sql-schema/merchant-0027.sql b/src/backenddb/sql-schema/merchant-0027.sql @@ -148,308 +148,4 @@ VALUES ('WeightUnitPound', 'pound', 'lb', TRUE, 3, TRUE), ('WeightUnitTon', 'metric tonne', 't', TRUE, 3, TRUE); -DROP FUNCTION IF EXISTS merchant_do_insert_unit; -CREATE FUNCTION merchant_do_insert_unit ( - IN in_instance_id TEXT, - IN in_unit TEXT, - IN in_unit_name_long TEXT, - IN in_unit_name_short TEXT, - IN in_unit_name_long_i18n BYTEA, - IN in_unit_name_short_i18n BYTEA, - IN in_unit_allow_fraction BOOL, - IN in_unit_precision_level INT4, - IN in_unit_active BOOL, - OUT out_no_instance BOOL, - OUT out_conflict BOOL, - OUT out_unit_serial INT8) - LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; -BEGIN - SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id = in_instance_id; - - IF NOT FOUND THEN - out_no_instance := TRUE; - out_conflict := FALSE; - out_unit_serial := NULL; - RETURN; - END IF; - - out_no_instance := FALSE; - - -- Reject attempts to shadow builtin identifiers. - IF EXISTS ( - SELECT 1 FROM merchant_builtin_units bu WHERE bu.unit = in_unit - ) THEN - out_conflict := TRUE; - out_unit_serial := NULL; - RETURN; - END IF; - - INSERT INTO merchant_custom_units ( - merchant_serial, - unit, - unit_name_long, - unit_name_short, - unit_name_long_i18n, - unit_name_short_i18n, - unit_allow_fraction, - unit_precision_level, - unit_active) - VALUES ( - my_merchant_id, - in_unit, - in_unit_name_long, - in_unit_name_short, - in_unit_name_long_i18n, - in_unit_name_short_i18n, - in_unit_allow_fraction, - in_unit_precision_level, - in_unit_active) - ON CONFLICT (merchant_serial, unit) DO NOTHING - RETURNING unit_serial - INTO out_unit_serial; - - IF FOUND THEN - out_conflict := FALSE; - RETURN; - END IF; - - -- Conflict: custom unit already exists. - SELECT unit_serial - INTO out_unit_serial - FROM merchant_custom_units - WHERE merchant_serial = my_merchant_id - AND unit = in_unit; - - out_conflict := TRUE; -END $$; - -DROP FUNCTION IF EXISTS merchant_do_update_unit; -CREATE FUNCTION merchant_do_update_unit ( - IN in_instance_id TEXT, - IN in_unit_id TEXT, - IN in_unit_name_long TEXT, - IN in_unit_name_long_i18n BYTEA, - IN in_unit_name_short TEXT, - IN in_unit_name_short_i18n BYTEA, - IN in_unit_allow_fraction BOOL, - IN in_unit_precision_level INT4, - IN in_unit_active BOOL, - OUT out_no_instance BOOL, - OUT out_no_unit BOOL, - OUT out_builtin_conflict BOOL) - LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_custom merchant_custom_units%ROWTYPE; - my_builtin merchant_builtin_units%ROWTYPE; - my_override merchant_builtin_unit_overrides%ROWTYPE; - new_unit_name_long TEXT; - new_unit_name_short TEXT; - new_unit_name_long_i18n BYTEA; - new_unit_name_short_i18n BYTEA; - new_unit_allow_fraction BOOL; - new_unit_precision_level INT4; - new_unit_active BOOL; - old_unit_allow_fraction BOOL; - old_unit_precision_level INT4; - old_unit_active BOOL; -BEGIN - out_no_instance := FALSE; - out_no_unit := FALSE; - out_builtin_conflict := FALSE; - - SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id = in_instance_id; - - IF NOT FOUND THEN - out_no_instance := TRUE; - RETURN; - END IF; - - SELECT * - INTO my_custom - FROM merchant_custom_units - WHERE merchant_serial = my_merchant_id - AND unit = in_unit_id - FOR UPDATE; - - IF FOUND THEN - old_unit_allow_fraction := my_custom.unit_allow_fraction; - old_unit_precision_level := my_custom.unit_precision_level; - old_unit_active := my_custom.unit_active; - - new_unit_name_long := COALESCE (in_unit_name_long, my_custom.unit_name_long); - new_unit_name_short := COALESCE (in_unit_name_short, my_custom.unit_name_short); - new_unit_name_long_i18n := COALESCE (in_unit_name_long_i18n, - my_custom.unit_name_long_i18n); - new_unit_name_short_i18n := COALESCE (in_unit_name_short_i18n, - my_custom.unit_name_short_i18n); - new_unit_allow_fraction := COALESCE (in_unit_allow_fraction, - my_custom.unit_allow_fraction); - new_unit_precision_level := COALESCE (in_unit_precision_level, - my_custom.unit_precision_level); - IF NOT new_unit_allow_fraction THEN - new_unit_precision_level := 0; - END IF; - - new_unit_active := COALESCE (in_unit_active, my_custom.unit_active); - - UPDATE merchant_custom_units SET - unit_name_long = new_unit_name_long - ,unit_name_long_i18n = new_unit_name_long_i18n - ,unit_name_short = new_unit_name_short - ,unit_name_short_i18n = new_unit_name_short_i18n - ,unit_allow_fraction = new_unit_allow_fraction - ,unit_precision_level = new_unit_precision_level - ,unit_active = new_unit_active - WHERE unit_serial = my_custom.unit_serial; - - ASSERT FOUND,'SELECTED it earlier, should UPDATE it now'; - - IF old_unit_allow_fraction IS DISTINCT FROM new_unit_allow_fraction - OR old_unit_precision_level IS DISTINCT FROM new_unit_precision_level - THEN - UPDATE merchant_inventory SET - allow_fractional_quantity = new_unit_allow_fraction - , fractional_precision_level = new_unit_precision_level - WHERE merchant_serial = my_merchant_id - AND unit = in_unit_id - AND allow_fractional_quantity = old_unit_allow_fraction - AND fractional_precision_level = old_unit_precision_level; - END IF; - RETURN; - END IF; - - -- Try builtin with overrides. - SELECT * - INTO my_builtin - FROM merchant_builtin_units - WHERE unit = in_unit_id; - - IF NOT FOUND THEN - out_no_unit := TRUE; - RETURN; - END IF; - - SELECT * - INTO my_override - FROM merchant_builtin_unit_overrides - WHERE merchant_serial = my_merchant_id - AND builtin_unit_serial = my_builtin.unit_serial - FOR UPDATE; - - old_unit_allow_fraction := COALESCE (my_override.override_allow_fraction, - my_builtin.unit_allow_fraction); - old_unit_precision_level := COALESCE (my_override.override_precision_level, - my_builtin.unit_precision_level); - old_unit_active := COALESCE (my_override.override_active, - my_builtin.unit_active); - - -- Only policy flags can change for builtin units. - IF in_unit_name_long IS NOT NULL - OR in_unit_name_short IS NOT NULL - OR in_unit_name_long_i18n IS NOT NULL - OR in_unit_name_short_i18n IS NOT NULL THEN - out_builtin_conflict := TRUE; - RETURN; - END IF; - - new_unit_allow_fraction := COALESCE (in_unit_allow_fraction, - old_unit_allow_fraction); - new_unit_precision_level := COALESCE (in_unit_precision_level, - old_unit_precision_level); - IF NOT new_unit_allow_fraction THEN - new_unit_precision_level := 0; - END IF; - new_unit_active := COALESCE (in_unit_active, old_unit_active); - - INSERT INTO merchant_builtin_unit_overrides ( - merchant_serial, - builtin_unit_serial, - override_allow_fraction, - override_precision_level, - override_active) - VALUES (my_merchant_id, - my_builtin.unit_serial, - new_unit_allow_fraction, - new_unit_precision_level, - new_unit_active) - ON CONFLICT (merchant_serial, builtin_unit_serial) - DO UPDATE SET override_allow_fraction = EXCLUDED.override_allow_fraction - , override_precision_level = EXCLUDED.override_precision_level - , override_active = EXCLUDED.override_active; - - IF old_unit_allow_fraction IS DISTINCT FROM new_unit_allow_fraction - OR old_unit_precision_level IS DISTINCT FROM new_unit_precision_level - THEN - UPDATE merchant_inventory SET - allow_fractional_quantity = new_unit_allow_fraction - , fractional_precision_level = new_unit_precision_level - WHERE merchant_serial = my_merchant_id - AND unit = in_unit_id - AND allow_fractional_quantity = old_unit_allow_fraction - AND fractional_precision_level = old_unit_precision_level; - END IF; - - RETURN; -END $$; - -DROP FUNCTION IF EXISTS merchant_do_delete_unit; -CREATE FUNCTION merchant_do_delete_unit ( - IN in_instance_id TEXT, - IN in_unit_id TEXT, - OUT out_no_instance BOOL, - OUT out_no_unit BOOL, - OUT out_builtin_conflict BOOL) - LANGUAGE plpgsql -AS $$ -DECLARE - my_merchant_id INT8; - my_unit merchant_custom_units%ROWTYPE; -BEGIN - out_no_instance := FALSE; - out_no_unit := FALSE; - out_builtin_conflict := FALSE; - - SELECT merchant_serial - INTO my_merchant_id - FROM merchant_instances - WHERE merchant_id = in_instance_id; - - IF NOT FOUND THEN - out_no_instance := TRUE; - RETURN; - END IF; - - SELECT * - INTO my_unit - FROM merchant_custom_units - WHERE merchant_serial = my_merchant_id - AND unit = in_unit_id - FOR UPDATE; - - IF NOT FOUND THEN - IF EXISTS (SELECT 1 FROM merchant_builtin_units bu WHERE bu.unit = in_unit_id) THEN - out_builtin_conflict := TRUE; - ELSE - out_no_unit := TRUE; - END IF; - RETURN; - END IF; - - DELETE FROM merchant_custom_units - WHERE unit_serial = my_unit.unit_serial; - - RETURN; -END $$; - COMMIT; diff --git a/src/backenddb/sql-schema/merchant-0036-copy.sql.fragment b/src/backenddb/sql-schema/merchant-0036-copy.sql.fragment @@ -0,0 +1,477 @@ + -- ================================================================= + -- Copy per-instance row data from merchant.* into the new schema. + -- Tables are listed in FK-dependency order so plain INSERT works + -- (no DEFERRABLE games, no WITH ORDINALITY). $1 is bound to + -- rec.merchant_serial via USING. The merchant_serial column is + -- dropped from every column list — the schema name is the + -- discriminator. All target tables use GENERATED BY DEFAULT AS + -- IDENTITY (so we can keep the source serial values), except + -- merchant_login_tokens whose `serial` is GENERATED ALWAYS — that + -- one needs OVERRIDING SYSTEM VALUE. + -- + -- Two stat-meta tables (merchant_statistic_bucket_meta, + -- merchant_statistic_interval_meta) have no merchant_serial in the + -- source schema — they are global slug catalogs and are copied + -- verbatim into every per-instance schema. + -- ================================================================= + + -- ---------------- direct merchant_serial (no JOIN) -------------- + + EXECUTE format('INSERT INTO %I.merchant_accounts' + || ' (account_serial, h_wire, salt, credit_facade_url,' + || ' credit_facade_credentials, last_bank_serial, payto_uri,' + || ' active, extra_wire_subject_metadata)' + || ' SELECT account_serial, h_wire, salt, credit_facade_url,' + || ' credit_facade_credentials, last_bank_serial, payto_uri,' + || ' active, extra_wire_subject_metadata' + || ' FROM merchant.merchant_accounts' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_categories' + || ' (category_serial, category_name, category_name_i18n)' + || ' SELECT category_serial, category_name, category_name_i18n' + || ' FROM merchant.merchant_categories' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_contract_terms' + || ' (order_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, choice_index)' + || ' SELECT order_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, choice_index' + || ' FROM merchant.merchant_contract_terms' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_custom_units' + || ' (unit_serial, unit, unit_name_long, unit_name_short,' + || ' unit_name_long_i18n, unit_name_short_i18n,' + || ' unit_allow_fraction, unit_precision_level, unit_active)' + || ' SELECT unit_serial, unit, unit_name_long, unit_name_short,' + || ' unit_name_long_i18n, unit_name_short_i18n,' + || ' unit_allow_fraction, unit_precision_level, unit_active' + || ' FROM merchant.merchant_custom_units' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + -- merchant_donau_instances: source filter column is named + -- merchant_instance_serial (not merchant_serial). + EXECUTE format('INSERT INTO %I.merchant_donau_instances' + || ' (donau_instances_serial, donau_url, charity_name, charity_id,' + || ' charity_max_per_year, charity_receipts_to_date, current_year)' + || ' SELECT donau_instances_serial, donau_url, charity_name, charity_id,' + || ' charity_max_per_year, charity_receipts_to_date, current_year' + || ' FROM merchant.merchant_donau_instances' + || ' WHERE merchant_instance_serial = $1', s) + USING rec.merchant_serial; + + -- merchant_login_tokens: target `serial` column is GENERATED ALWAYS + -- → must use OVERRIDING SYSTEM VALUE to preserve serial values. + EXECUTE format('INSERT INTO %I.merchant_login_tokens' + || ' (token, creation_time, expiration_time, validity_scope,' + || ' description, serial)' + || ' OVERRIDING SYSTEM VALUE' + || ' SELECT token, creation_time, expiration_time, validity_scope,' + || ' description, serial' + || ' FROM merchant.merchant_login_tokens' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_money_pots' + || ' (money_pot_serial, money_pot_name, money_pot_description, pot_totals)' + || ' SELECT money_pot_serial, money_pot_name, money_pot_description, pot_totals' + || ' FROM merchant.merchant_money_pots' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_otp_devices' + || ' (otp_serial, otp_id, otp_description, otp_key, otp_algorithm, otp_ctr)' + || ' SELECT otp_serial, otp_id, otp_description, otp_key, otp_algorithm, otp_ctr' + || ' FROM merchant.merchant_otp_devices' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_orders' + || ' (order_serial, order_id, claim_token, h_post_data, pay_deadline,' + || ' creation_time, contract_terms, pos_key, pos_algorithm,' + || ' fulfillment_url, session_id)' + || ' SELECT order_serial, order_id, claim_token, h_post_data, pay_deadline,' + || ' creation_time, contract_terms, pos_key, pos_algorithm,' + || ' fulfillment_url, session_id' + || ' FROM merchant.merchant_orders' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_product_groups' + || ' (product_group_serial, product_group_name, product_group_description)' + || ' SELECT product_group_serial, product_group_name, product_group_description' + || ' FROM merchant.merchant_product_groups' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + -- merchant_inventory references merchant_product_groups and merchant_money_pots + -- (already copied above). + EXECUTE format('INSERT INTO %I.merchant_inventory' + || ' (product_serial, product_id, description, description_i18n,' + || ' unit, image, taxes, total_stock, total_sold, total_lost,' + || ' address, next_restock, minimum_age, product_name, image_hash,' + || ' price_array, total_stock_frac, total_sold_frac, total_lost_frac,' + || ' allow_fractional_quantity, fractional_precision_level,' + || ' product_group_serial, money_pot_serial, price_is_net)' + || ' SELECT product_serial, product_id, description, description_i18n,' + || ' unit, image, taxes, total_stock, total_sold, total_lost,' + || ' address, next_restock, minimum_age, product_name, image_hash,' + || ' price_array, total_stock_frac, total_sold_frac, total_lost_frac,' + || ' allow_fractional_quantity, fractional_precision_level,' + || ' product_group_serial, money_pot_serial, price_is_net' + || ' FROM merchant.merchant_inventory' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_reports' + || ' (report_serial, report_program_section, report_description,' + || ' mime_type, report_token, data_source, target_address,' + || ' frequency, frequency_shift, next_transmission,' + || ' last_error_code, last_error_detail, one_shot_hidden)' + || ' SELECT report_serial, report_program_section, report_description,' + || ' mime_type, report_token, data_source, target_address,' + || ' frequency, frequency_shift, next_transmission,' + || ' last_error_code, last_error_detail, one_shot_hidden' + || ' FROM merchant.merchant_reports' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + -- merchant_template references merchant_otp_devices (already copied). + EXECUTE format('INSERT INTO %I.merchant_template' + || ' (template_serial, template_id, template_description,' + || ' otp_device_id, template_contract, editable_defaults)' + || ' SELECT template_serial, template_id, template_description,' + || ' otp_device_id, template_contract, editable_defaults' + || ' FROM merchant.merchant_template' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_token_families' + || ' (token_family_serial, slug, name, description, description_i18n,' + || ' valid_after, valid_before, duration, kind, issued, used,' + || ' validity_granularity, start_offset, cipher_choice, extra_data)' + || ' SELECT token_family_serial, slug, name, description, description_i18n,' + || ' valid_after, valid_before, duration, kind, issued, used,' + || ' validity_granularity, start_offset, cipher_choice, extra_data' + || ' FROM merchant.merchant_token_families' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_webhook' + || ' (webhook_serial, webhook_id, event_type, url, http_method,' + || ' header_template, body_template)' + || ' SELECT webhook_serial, webhook_id, event_type, url, http_method,' + || ' header_template, body_template' + || ' FROM merchant.merchant_webhook' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + + EXECUTE format('INSERT INTO %I.merchant_builtin_unit_overrides' + || ' (builtin_unit_serial, override_allow_fraction,' + || ' override_precision_level, override_active)' + || ' SELECT builtin_unit_serial, override_allow_fraction,' + || ' override_precision_level, override_active' + || ' FROM merchant.merchant_builtin_unit_overrides' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + -- ---------------- statistics: meta tables are GLOBAL ------------ + -- Copied unfiltered (every instance gets a full slug catalog). + + EXECUTE format('INSERT INTO %I.merchant_statistic_bucket_meta' + || ' (bmeta_serial_id, slug, description, stype, ranges, ages)' + || ' SELECT bmeta_serial_id, slug, description, stype, ranges, ages' + || ' FROM merchant.merchant_statistic_bucket_meta', s); + + EXECUTE format('INSERT INTO %I.merchant_statistic_interval_meta' + || ' (imeta_serial_id, slug, description, stype, ranges, precisions)' + || ' SELECT imeta_serial_id, slug, description, stype, ranges, precisions' + || ' FROM merchant.merchant_statistic_interval_meta', s); + + -- ---------------- statistics: per-instance event/bucket tables -- + + EXECUTE format('INSERT INTO %I.merchant_statistic_amount_event' + || ' (aevent_serial_id, imeta_serial_id, slot,' + || ' delta_curr, delta_value, delta_frac)' + || ' SELECT aevent_serial_id, imeta_serial_id, slot,' + || ' delta_curr, delta_value, delta_frac' + || ' FROM merchant.merchant_statistic_amount_event' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_statistic_counter_event' + || ' (nevent_serial_id, imeta_serial_id, slot, delta)' + || ' SELECT nevent_serial_id, imeta_serial_id, slot, delta' + || ' FROM merchant.merchant_statistic_counter_event' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_statistic_bucket_amount' + || ' (bmeta_serial_id, bucket_start, bucket_range, curr,' + || ' cumulative_value, cumulative_frac)' + || ' SELECT bmeta_serial_id, bucket_start, bucket_range, curr,' + || ' cumulative_value, cumulative_frac' + || ' FROM merchant.merchant_statistic_bucket_amount' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_statistic_bucket_counter' + || ' (bmeta_serial_id, bucket_start, bucket_range, cumulative_number)' + || ' SELECT bmeta_serial_id, bucket_start, bucket_range, cumulative_number' + || ' FROM merchant.merchant_statistic_bucket_counter' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_statistic_interval_amount' + || ' (imeta_serial_id, event_delimiter, range, curr,' + || ' cumulative_value, cumulative_frac)' + || ' SELECT imeta_serial_id, event_delimiter, range, curr,' + || ' cumulative_value, cumulative_frac' + || ' FROM merchant.merchant_statistic_interval_amount' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_statistic_interval_counter' + || ' (imeta_serial_id, range, event_delimiter, cumulative_number)' + || ' SELECT imeta_serial_id, range, event_delimiter, cumulative_number' + || ' FROM merchant.merchant_statistic_interval_counter' + || ' WHERE merchant_serial = $1', s) + USING rec.merchant_serial; + + -- ---------------- account-linked (JOIN via account_serial) ------ + + EXECUTE format('INSERT INTO %I.merchant_kyc' + || ' (kyc_serial_id, kyc_timestamp, kyc_ok, account_serial,' + || ' exchange_url, access_token, exchange_http_status,' + || ' exchange_ec_code, aml_review, jaccount_limits,' + || ' last_rule_gen, next_kyc_poll, kyc_backoff)' + || ' SELECT k.kyc_serial_id, k.kyc_timestamp, k.kyc_ok, k.account_serial,' + || ' k.exchange_url, k.access_token, k.exchange_http_status,' + || ' k.exchange_ec_code, k.aml_review, k.jaccount_limits,' + || ' k.last_rule_gen, k.next_kyc_poll, k.kyc_backoff' + || ' FROM merchant.merchant_kyc k' + || ' JOIN merchant.merchant_accounts a' + || ' ON k.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_deposit_confirmations' + || ' (deposit_confirmation_serial, order_serial, deposit_timestamp,' + || ' exchange_url, total_without_fee, wire_fee, signkey_serial,' + || ' exchange_sig, account_serial, wire_transfer_deadline,' + || ' wire_pending, exchange_failure, retry_backoff)' + || ' SELECT dc.deposit_confirmation_serial, dc.order_serial, dc.deposit_timestamp,' + || ' dc.exchange_url, dc.total_without_fee, dc.wire_fee, dc.signkey_serial,' + || ' dc.exchange_sig, dc.account_serial, dc.wire_transfer_deadline,' + || ' dc.wire_pending, dc.exchange_failure, dc.retry_backoff' + || ' FROM merchant.merchant_deposit_confirmations dc' + || ' JOIN merchant.merchant_accounts a' + || ' ON dc.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_expected_transfers' + || ' (expected_credit_serial, exchange_url, wtid, expected_credit_amount,' + || ' wire_fee, account_serial, expected_time, retry_time, last_http_status,' + || ' last_ec, last_detail, retry_needed, signkey_serial, exchange_sig,' + || ' h_details, confirmed)' + || ' SELECT et.expected_credit_serial, et.exchange_url, et.wtid, et.expected_credit_amount,' + || ' et.wire_fee, et.account_serial, et.expected_time, et.retry_time, et.last_http_status,' + || ' et.last_ec, et.last_detail, et.retry_needed, et.signkey_serial, et.exchange_sig,' + || ' et.h_details, et.confirmed' + || ' FROM merchant.merchant_expected_transfers et' + || ' JOIN merchant.merchant_accounts a' + || ' ON et.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_transfers' + || ' (credit_serial, exchange_url, wtid, credit_amount, account_serial,' + || ' bank_serial_id, expected, execution_time)' + || ' SELECT t.credit_serial, t.exchange_url, t.wtid, t.credit_amount, t.account_serial,' + || ' t.bank_serial_id, t.expected, t.execution_time' + || ' FROM merchant.merchant_transfers t' + || ' JOIN merchant.merchant_accounts a' + || ' ON t.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- ---------------- deposits: deeper join (dc → accounts) --------- + + EXECUTE format('INSERT INTO %I.merchant_deposits' + || ' (deposit_serial, coin_offset, deposit_confirmation_serial,' + || ' coin_pub, coin_sig, amount_with_fee, deposit_fee, refund_fee,' + || ' settlement_retry_needed, settlement_retry_time,' + || ' settlement_last_http_status, settlement_last_ec,' + || ' settlement_last_detail, settlement_wtid,' + || ' settlement_coin_contribution, settlement_expected_credit_serial,' + || ' signkey_serial, settlement_exchange_sig)' + || ' SELECT d.deposit_serial, d.coin_offset, d.deposit_confirmation_serial,' + || ' d.coin_pub, d.coin_sig, d.amount_with_fee, d.deposit_fee, d.refund_fee,' + || ' d.settlement_retry_needed, d.settlement_retry_time,' + || ' d.settlement_last_http_status, d.settlement_last_ec,' + || ' d.settlement_last_detail, d.settlement_wtid,' + || ' d.settlement_coin_contribution, d.settlement_expected_credit_serial,' + || ' d.signkey_serial, d.settlement_exchange_sig' + || ' FROM merchant.merchant_deposits d' + || ' JOIN merchant.merchant_deposit_confirmations dc' + || ' ON d.deposit_confirmation_serial = dc.deposit_confirmation_serial' + || ' JOIN merchant.merchant_accounts a' + || ' ON dc.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- expected_transfer_to_coin: deposit_serial → deposits → dc → accounts. + EXECUTE format('INSERT INTO %I.merchant_expected_transfer_to_coin' + || ' (deposit_serial, expected_credit_serial, offset_in_exchange_list,' + || ' exchange_deposit_value, exchange_deposit_fee)' + || ' SELECT ettc.deposit_serial, ettc.expected_credit_serial, ettc.offset_in_exchange_list,' + || ' ettc.exchange_deposit_value, ettc.exchange_deposit_fee' + || ' FROM merchant.merchant_expected_transfer_to_coin ettc' + || ' JOIN merchant.merchant_deposits d' + || ' ON ettc.deposit_serial = d.deposit_serial' + || ' JOIN merchant.merchant_deposit_confirmations dc' + || ' ON d.deposit_confirmation_serial = dc.deposit_confirmation_serial' + || ' JOIN merchant.merchant_accounts a' + || ' ON dc.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- transfer_signatures: expected_credit_serial → expected_transfers → accounts. + EXECUTE format('INSERT INTO %I.merchant_transfer_signatures' + || ' (expected_credit_serial, signkey_serial, wire_fee, credit_amount,' + || ' execution_time, exchange_sig)' + || ' SELECT ts.expected_credit_serial, ts.signkey_serial, ts.wire_fee, ts.credit_amount,' + || ' ts.execution_time, ts.exchange_sig' + || ' FROM merchant.merchant_transfer_signatures ts' + || ' JOIN merchant.merchant_expected_transfers et' + || ' ON ts.expected_credit_serial = et.expected_credit_serial' + || ' JOIN merchant.merchant_accounts a' + || ' ON et.account_serial = a.account_serial' + || ' WHERE a.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- ---------------- order/contract-linked (JOIN via order_serial) --- + + EXECUTE format('INSERT INTO %I.merchant_refunds' + || ' (refund_serial, order_serial, rtransaction_id, refund_timestamp,' + || ' coin_pub, reason, refund_amount)' + || ' SELECT r.refund_serial, r.order_serial, r.rtransaction_id, r.refund_timestamp,' + || ' r.coin_pub, r.reason, r.refund_amount' + || ' FROM merchant.merchant_refunds r' + || ' JOIN merchant.merchant_contract_terms ct' + || ' ON r.order_serial = ct.order_serial' + || ' WHERE ct.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_refund_proofs' + || ' (refund_serial, exchange_sig, signkey_serial)' + || ' SELECT rp.refund_serial, rp.exchange_sig, rp.signkey_serial' + || ' FROM merchant.merchant_refund_proofs rp' + || ' JOIN merchant.merchant_refunds r' + || ' ON rp.refund_serial = r.refund_serial' + || ' JOIN merchant.merchant_contract_terms ct' + || ' ON r.order_serial = ct.order_serial' + || ' WHERE ct.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_order_token_blinded_sigs' + || ' (order_token_bs_serial, order_serial, token_index,' + || ' token_blinded_signature, token_hash)' + || ' SELECT otbs.order_token_bs_serial, otbs.order_serial, otbs.token_index,' + || ' otbs.token_blinded_signature, otbs.token_hash' + || ' FROM merchant.merchant_order_token_blinded_sigs otbs' + || ' JOIN merchant.merchant_contract_terms ct' + || ' ON otbs.order_serial = ct.order_serial' + || ' WHERE ct.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- unclaim_signatures: linked via h_contract_terms. + EXECUTE format('INSERT INTO %I.merchant_unclaim_signatures' + || ' (unclaim_serial, h_contract_terms, unclaim_sig, expiration_time)' + || ' SELECT us.unclaim_serial, us.h_contract_terms, us.unclaim_sig, us.expiration_time' + || ' FROM merchant.merchant_unclaim_signatures us' + || ' JOIN merchant.merchant_contract_terms ct' + || ' ON us.h_contract_terms = ct.h_contract_terms' + || ' WHERE ct.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- order_locks: order_serial → contract_terms. + EXECUTE format('INSERT INTO %I.merchant_order_locks' + || ' (product_serial, total_locked, order_serial, total_locked_frac)' + || ' SELECT ol.product_serial, ol.total_locked, ol.order_serial, ol.total_locked_frac' + || ' FROM merchant.merchant_order_locks ol' + || ' JOIN merchant.merchant_contract_terms ct' + || ' ON ol.order_serial = ct.order_serial' + || ' WHERE ct.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- inventory_locks: product_serial → inventory. + EXECUTE format('INSERT INTO %I.merchant_inventory_locks' + || ' (product_serial, lock_uuid, total_locked, expiration, total_locked_frac)' + || ' SELECT il.product_serial, il.lock_uuid, il.total_locked, il.expiration, il.total_locked_frac' + || ' FROM merchant.merchant_inventory_locks il' + || ' JOIN merchant.merchant_inventory i' + || ' ON il.product_serial = i.product_serial' + || ' WHERE i.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- product_categories: junction; pivot on product_serial → inventory. + EXECUTE format('INSERT INTO %I.merchant_product_categories' + || ' (category_serial, product_serial)' + || ' SELECT pc.category_serial, pc.product_serial' + || ' FROM merchant.merchant_product_categories pc' + || ' JOIN merchant.merchant_inventory i' + || ' ON pc.product_serial = i.product_serial' + || ' WHERE i.merchant_serial = $1', s) + USING rec.merchant_serial; + + -- ---------------- token chain (token_families → keys → tokens) -- + + EXECUTE format('INSERT INTO %I.merchant_token_family_keys' + || ' (token_family_key_serial, token_family_serial, pub, h_pub,' + || ' priv, cipher, signature_validity_start, signature_validity_end,' + || ' private_key_deleted_at, private_key_created_at)' + || ' SELECT tfk.token_family_key_serial, tfk.token_family_serial, tfk.pub, tfk.h_pub,' + || ' tfk.priv, tfk.cipher, tfk.signature_validity_start, tfk.signature_validity_end,' + || ' tfk.private_key_deleted_at, tfk.private_key_created_at' + || ' FROM merchant.merchant_token_family_keys tfk' + || ' JOIN merchant.merchant_token_families tf' + || ' ON tfk.token_family_serial = tf.token_family_serial' + || ' WHERE tf.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_issued_tokens' + || ' (issued_token_serial, h_contract_terms, token_family_key_serial, blind_sig)' + || ' SELECT it.issued_token_serial, it.h_contract_terms, it.token_family_key_serial, it.blind_sig' + || ' FROM merchant.merchant_issued_tokens it' + || ' JOIN merchant.merchant_token_family_keys tfk' + || ' ON it.token_family_key_serial = tfk.token_family_key_serial' + || ' JOIN merchant.merchant_token_families tf' + || ' ON tfk.token_family_serial = tf.token_family_serial' + || ' WHERE tf.merchant_serial = $1', s) + USING rec.merchant_serial; + + EXECUTE format('INSERT INTO %I.merchant_used_tokens' + || ' (spent_token_serial, h_contract_terms, token_family_key_serial,' + || ' token_pub, token_sig, blind_sig)' + || ' SELECT ut.spent_token_serial, ut.h_contract_terms, ut.token_family_key_serial,' + || ' ut.token_pub, ut.token_sig, ut.blind_sig' + || ' FROM merchant.merchant_used_tokens ut' + || ' JOIN merchant.merchant_token_family_keys tfk' + || ' ON ut.token_family_key_serial = tfk.token_family_key_serial' + || ' JOIN merchant.merchant_token_families tf' + || ' ON tfk.token_family_serial = tf.token_family_serial' + || ' WHERE tf.merchant_serial = $1', s) + USING rec.merchant_serial; diff --git a/src/backenddb/sql-schema/merchant-0036-drop.sql.fragment b/src/backenddb/sql-schema/merchant-0036-drop.sql.fragment @@ -0,0 +1,63 @@ +-- +-- 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/> + +-- Drop the now-empty per-instance tables from the merchant schema. +-- CASCADE removes the (now obsolete) trigger attachments along with them. +-- The per-instance trigger functions in merchant.* (handle_category_changes, +-- merchant_*_statistics_trigger, ...) are kept around — they will be replaced +-- by the procedures.sql reload after the patch finishes. + +DROP TABLE merchant.merchant_unclaim_signatures CASCADE; +DROP TABLE merchant.merchant_used_tokens CASCADE; +DROP TABLE merchant.merchant_issued_tokens CASCADE; +DROP TABLE merchant.merchant_token_family_keys CASCADE; +DROP TABLE merchant.merchant_token_families CASCADE; +DROP TABLE merchant.merchant_webhook CASCADE; +DROP TABLE merchant.merchant_transfer_signatures CASCADE; +DROP TABLE merchant.merchant_expected_transfer_to_coin CASCADE; +DROP TABLE merchant.merchant_transfers CASCADE; +DROP TABLE merchant.merchant_expected_transfers CASCADE; +DROP TABLE merchant.merchant_kyc CASCADE; +DROP TABLE merchant.merchant_deposits CASCADE; +DROP TABLE merchant.merchant_deposit_confirmations CASCADE; +DROP TABLE merchant.merchant_refund_proofs CASCADE; +DROP TABLE merchant.merchant_refunds CASCADE; +DROP TABLE merchant.merchant_order_token_blinded_sigs CASCADE; +DROP TABLE merchant.merchant_order_locks CASCADE; +DROP TABLE merchant.merchant_inventory_locks CASCADE; +DROP TABLE merchant.merchant_product_categories CASCADE; +DROP TABLE merchant.merchant_template CASCADE; +DROP TABLE merchant.merchant_otp_devices CASCADE; +DROP TABLE merchant.merchant_inventory CASCADE; +DROP TABLE merchant.merchant_product_groups CASCADE; +DROP TABLE merchant.merchant_money_pots CASCADE; +DROP TABLE merchant.merchant_categories CASCADE; +DROP TABLE merchant.merchant_contract_terms CASCADE; +DROP TABLE merchant.merchant_orders CASCADE; +DROP TABLE merchant.merchant_custom_units CASCADE; +DROP TABLE merchant.merchant_builtin_unit_overrides CASCADE; +DROP TABLE merchant.merchant_login_tokens CASCADE; +DROP TABLE merchant.merchant_donau_instances CASCADE; +DROP TABLE merchant.merchant_reports CASCADE; +DROP TABLE merchant.merchant_keys CASCADE; +DROP TABLE merchant.merchant_accounts CASCADE; +DROP TABLE merchant.merchant_statistic_amount_event CASCADE; +DROP TABLE merchant.merchant_statistic_counter_event CASCADE; +DROP TABLE merchant.merchant_statistic_interval_amount CASCADE; +DROP TABLE merchant.merchant_statistic_interval_counter CASCADE; +DROP TABLE merchant.merchant_statistic_interval_meta CASCADE; +DROP TABLE merchant.merchant_statistic_bucket_amount CASCADE; +DROP TABLE merchant.merchant_statistic_bucket_counter CASCADE; +DROP TABLE merchant.merchant_statistic_bucket_meta CASCADE; diff --git a/src/backenddb/sql-schema/merchant-0036-init.sql.fragment b/src/backenddb/sql-schema/merchant-0036-init.sql.fragment @@ -0,0 +1,964 @@ +-- +-- 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-0036-init.sql.fragment +-- @brief Create initial set of per-instance tables and indices +-- @author Christian Grothoff + +CREATE PROCEDURE merchant.merchant_0036_init(s TEXT) + LANGUAGE plpgsql + AS $$ +BEGIN + -- ------------------------------------------------------------------- + -- 2. CREATE TABLE statements for all per-instance tables. + -- The merchant_serial column has been dropped everywhere; PK, + -- UNIQUE and CHECK constraints have been adjusted accordingly. + -- ------------------------------------------------------------------- + + -- merchant_accounts + EXECUTE format('CREATE TABLE %I.merchant_accounts (' + || ' account_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' h_wire BYTEA NOT NULL UNIQUE,' + || ' salt BYTEA NOT NULL,' + || ' credit_facade_url TEXT,' + || ' credit_facade_credentials JSONB,' + || ' last_bank_serial INT8 DEFAULT 0 NOT NULL,' + || ' payto_uri TEXT NOT NULL UNIQUE,' + || ' active BOOLEAN NOT NULL,' + || ' extra_wire_subject_metadata TEXT,' + || ' CONSTRAINT merchant_accounts_h_wire_check CHECK ((LENGTH(h_wire) = 64)),' + || ' CONSTRAINT merchant_accounts_salt_check CHECK ((LENGTH(salt) = 16))' + || ')', s); + + -- merchant_builtin_unit_overrides + EXECUTE format('CREATE TABLE %I.merchant_builtin_unit_overrides (' + || ' builtin_unit_serial INT8 NOT NULL PRIMARY KEY,' + || ' override_allow_fraction BOOLEAN,' + || ' override_precision_level INT4,' + || ' override_active BOOLEAN,' + || ' CONSTRAINT merchant_builtin_unit_overrides_override_precision_level_check' + || ' CHECK (((override_precision_level >= 0) AND (override_precision_level <= 6)))' + || ')', s); + + -- merchant_categories + EXECUTE format('CREATE TABLE %I.merchant_categories (' + || ' category_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' category_name TEXT NOT NULL UNIQUE,' + || ' category_name_i18n JSONB NOT NULL' + || ')', s); + + -- merchant_contract_terms + EXECUTE format('CREATE TABLE %I.merchant_contract_terms (' + || ' order_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' order_id TEXT NOT NULL UNIQUE,' + || ' contract_terms JSONB NOT NULL,' + || ' wallet_data TEXT,' + || ' h_contract_terms BYTEA NOT NULL UNIQUE,' + || ' creation_time INT8 NOT NULL,' + || ' pay_deadline INT8 NOT NULL,' + || ' refund_deadline INT8 NOT NULL,' + || ' paid BOOLEAN DEFAULT FALSE NOT NULL,' + || ' wired BOOLEAN DEFAULT FALSE NOT NULL,' + || ' fulfillment_url TEXT,' + || $DDL$ session_id TEXT DEFAULT ''::text NOT NULL,$DDL$ + || ' pos_key TEXT,' + || ' pos_algorithm INT4 DEFAULT 0 NOT NULL,' + || ' claim_token BYTEA NOT NULL,' + || ' choice_index INT2,' + || ' CONSTRAINT merchant_contract_terms_claim_token_check CHECK ((LENGTH(claim_token) = 16)),' + || ' CONSTRAINT merchant_contract_terms_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64))' + || ')', s); + + -- merchant_custom_units + EXECUTE format($DDL$CREATE TABLE %I.merchant_custom_units ( + unit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + unit TEXT NOT NULL UNIQUE, + unit_name_long TEXT NOT NULL, + unit_name_short TEXT NOT NULL, + unit_name_long_i18n BYTEA DEFAULT convert_to('{}'::text, 'UTF8'::name) NOT NULL, + unit_name_short_i18n BYTEA DEFAULT convert_to('{}'::text, 'UTF8'::name) NOT NULL, + unit_allow_fraction BOOLEAN DEFAULT FALSE NOT NULL, + unit_precision_level INT4 DEFAULT 0 NOT NULL, + unit_active BOOLEAN DEFAULT TRUE NOT NULL, + CONSTRAINT merchant_custom_units_unit_precision_level_check + CHECK (((unit_precision_level >= 0) AND (unit_precision_level <= 6))) + )$DDL$, s); + + -- merchant_deposit_confirmations + EXECUTE format('CREATE TABLE %I.merchant_deposit_confirmations (' + || ' deposit_confirmation_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' order_serial INT8,' + || ' deposit_timestamp INT8 NOT NULL,' + || ' exchange_url TEXT NOT NULL,' + || ' total_without_fee merchant.taler_amount_currency NOT NULL,' + || ' wire_fee merchant.taler_amount_currency NOT NULL,' + || ' signkey_serial INT8 NOT NULL,' + || ' exchange_sig BYTEA NOT NULL,' + || ' account_serial INT8 NOT NULL,' + || ' wire_transfer_deadline INT8 DEFAULT 0 NOT NULL,' + || ' wire_pending BOOLEAN DEFAULT TRUE NOT NULL,' + || ' exchange_failure TEXT,' + || ' retry_backoff INT8 DEFAULT 0 NOT NULL,' + || ' CONSTRAINT merchant_deposit_confirmations_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64)),' + || ' UNIQUE (order_serial, exchange_sig)' + || ')', s); + + -- merchant_deposits + EXECUTE format('CREATE TABLE %I.merchant_deposits (' + || ' deposit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' coin_offset INT4 NOT NULL,' + || ' deposit_confirmation_serial INT8 NOT NULL,' + || ' coin_pub BYTEA NOT NULL,' + || ' coin_sig BYTEA NOT NULL,' + || ' amount_with_fee merchant.taler_amount_currency NOT NULL,' + || ' deposit_fee merchant.taler_amount_currency NOT NULL,' + || ' refund_fee merchant.taler_amount_currency NOT NULL,' + || ' settlement_retry_needed BOOLEAN DEFAULT TRUE,' + || ' settlement_retry_time INT8 DEFAULT 0,' + || ' settlement_last_http_status INT4,' + || ' settlement_last_ec INT4,' + || ' settlement_last_detail TEXT,' + || ' settlement_wtid BYTEA,' + || ' settlement_coin_contribution merchant.taler_amount_currency,' + || ' settlement_expected_credit_serial INT8,' + || ' signkey_serial INT8,' + || ' settlement_exchange_sig BYTEA,' + || ' CONSTRAINT merchant_deposits_coin_pub_check CHECK ((LENGTH(coin_pub) = 32)),' + || ' CONSTRAINT merchant_deposits_coin_sig_check CHECK ((LENGTH(coin_sig) = 64)),' + || ' CONSTRAINT merchant_deposits_settlement_exchange_sig_check CHECK ((LENGTH(settlement_exchange_sig) = 64)),' + || ' CONSTRAINT merchant_deposits_settlement_wtid_check CHECK ((LENGTH(settlement_wtid) = 32)),' + || ' UNIQUE (deposit_confirmation_serial, coin_pub)' + || ')', s); + + -- merchant_donau_instances + EXECUTE format('CREATE TABLE %I.merchant_donau_instances (' + || ' donau_instances_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' donau_url TEXT NOT NULL,' + || ' charity_name TEXT NOT NULL,' + || ' charity_id INT8 NOT NULL,' + || ' charity_max_per_year merchant.taler_amount_currency NOT NULL,' + || ' charity_receipts_to_date merchant.taler_amount_currency NOT NULL,' + || ' current_year INT8 NOT NULL,' + || ' UNIQUE (donau_url, charity_id)' + || ')', s); + + -- merchant_expected_transfer_to_coin + EXECUTE format('CREATE TABLE %I.merchant_expected_transfer_to_coin (' + || ' deposit_serial INT8 NOT NULL UNIQUE,' + || ' expected_credit_serial INT8 NOT NULL,' + || ' offset_in_exchange_list INT8 NOT NULL,' + || ' exchange_deposit_value merchant.taler_amount_currency NOT NULL,' + || ' exchange_deposit_fee merchant.taler_amount_currency NOT NULL' + || ')', s); + + -- merchant_expected_transfers + EXECUTE format('CREATE TABLE %I.merchant_expected_transfers (' + || ' expected_credit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' exchange_url TEXT NOT NULL,' + || ' wtid BYTEA NOT NULL,' + || ' expected_credit_amount merchant.taler_amount_currency,' + || ' wire_fee merchant.taler_amount_currency,' + || ' account_serial INT8 NOT NULL,' + || ' expected_time INT8 NOT NULL,' + || ' retry_time INT8 DEFAULT 0 NOT NULL,' + || ' last_http_status INT4,' + || ' last_ec INT4,' + || ' last_detail TEXT,' + || ' retry_needed BOOLEAN DEFAULT TRUE NOT NULL,' + || ' signkey_serial INT8,' + || ' exchange_sig BYTEA,' + || ' h_details BYTEA,' + || ' confirmed BOOLEAN DEFAULT FALSE NOT NULL,' + || ' CONSTRAINT merchant_expected_transfers_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64)),' + || ' CONSTRAINT merchant_expected_transfers_h_details_check CHECK ((LENGTH(h_details) = 64)),' + || ' CONSTRAINT merchant_expected_transfers_wtid_check CHECK ((LENGTH(wtid) = 32)),' + || ' UNIQUE (wtid, exchange_url, account_serial)' + || ')', s); + + -- merchant_inventory + EXECUTE format($DDL$CREATE TABLE %I.merchant_inventory ( + product_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + product_id TEXT NOT NULL UNIQUE, + description TEXT NOT NULL, + description_i18n JSONB NOT NULL, + unit TEXT NOT NULL, + image TEXT NOT NULL, + taxes JSONB NOT NULL, + total_stock INT8 NOT NULL, + total_sold INT8 DEFAULT 0 NOT NULL, + total_lost INT8 DEFAULT 0 NOT NULL, + address JSONB NOT NULL, + next_restock INT8 NOT NULL, + minimum_age INT4 DEFAULT 0 NOT NULL, + product_name TEXT NOT NULL, + image_hash TEXT, + price_array merchant.taler_amount_currency[] + DEFAULT ARRAY[]::merchant.taler_amount_currency[] NOT NULL, + total_stock_frac INT4 DEFAULT 0 NOT NULL, + total_sold_frac INT4 DEFAULT 0 NOT NULL, + total_lost_frac INT4 DEFAULT 0 NOT NULL, + allow_fractional_quantity BOOLEAN DEFAULT FALSE NOT NULL, + fractional_precision_level INT4 DEFAULT 0 NOT NULL, + product_group_serial INT8, + money_pot_serial INT8, + price_is_net BOOLEAN DEFAULT FALSE + )$DDL$, s); + + -- merchant_inventory_locks + EXECUTE format('CREATE TABLE %I.merchant_inventory_locks (' + || ' product_serial INT8 NOT NULL,' + || ' lock_uuid BYTEA NOT NULL,' + || ' total_locked INT8 NOT NULL,' + || ' expiration INT8 NOT NULL,' + || ' total_locked_frac INT4 DEFAULT 0 NOT NULL,' + || ' CONSTRAINT merchant_inventory_locks_lock_uuid_check CHECK ((LENGTH(lock_uuid) = 16))' + || ')', s); + + -- merchant_issued_tokens + EXECUTE format('CREATE TABLE %I.merchant_issued_tokens (' + || ' issued_token_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' h_contract_terms BYTEA NOT NULL,' + || ' token_family_key_serial INT8,' + || ' blind_sig BYTEA NOT NULL,' + || ' CONSTRAINT merchant_issued_tokens_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64))' + || ')', s); + + -- merchant_kyc (PK survives unchanged: (account_serial, exchange_url)) + EXECUTE format('CREATE TABLE %I.merchant_kyc (' + || ' kyc_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE,' + || ' kyc_timestamp INT8 NOT NULL,' + || ' kyc_ok BOOLEAN DEFAULT FALSE NOT NULL,' + || ' account_serial INT8 NOT NULL,' + || ' exchange_url TEXT NOT NULL,' + || ' access_token BYTEA,' + || ' exchange_http_status INT4 DEFAULT 0,' + || ' exchange_ec_code INT4 DEFAULT 0,' + || ' aml_review BOOLEAN DEFAULT FALSE,' + || ' jaccount_limits JSONB,' + || ' last_rule_gen INT8 DEFAULT 0 NOT NULL,' + || ' next_kyc_poll INT8 DEFAULT 0 NOT NULL,' + || ' kyc_backoff INT8 DEFAULT 0 NOT NULL,' + || ' CONSTRAINT access_token_LENGTH_check CHECK ((LENGTH(access_token) = 32)),' + || ' PRIMARY KEY (account_serial, exchange_url)' + || ')', s); + + -- merchant_login_tokens (note: serial is GENERATED ALWAYS) + EXECUTE format('CREATE TABLE %I.merchant_login_tokens (' + || ' serial INT8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY,' + || ' token BYTEA NOT NULL UNIQUE,' + || ' creation_time INT8 NOT NULL,' + || ' expiration_time INT8 NOT NULL,' + || ' validity_scope INT4 NOT NULL,' + || ' description TEXT NOT NULL,' + || ' CONSTRAINT merchant_login_tokens_token_check CHECK ((LENGTH(token) = 32))' + || ')', s); + + -- merchant_money_pots + EXECUTE format('CREATE TABLE %I.merchant_money_pots (' + || ' money_pot_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' money_pot_name TEXT NOT NULL UNIQUE,' + || ' money_pot_description TEXT NOT NULL,' + || ' pot_totals merchant.taler_amount_currency[]' + || ' DEFAULT ARRAY[]::merchant.taler_amount_currency[] NOT NULL' + || ')', s); + + -- merchant_order_locks + EXECUTE format('CREATE TABLE %I.merchant_order_locks (' + || ' product_serial INT8 NOT NULL,' + || ' total_locked INT8 NOT NULL,' + || ' order_serial INT8 NOT NULL,' + || ' total_locked_frac INT4 DEFAULT 0 NOT NULL' + || ')', s); + + -- merchant_order_token_blinded_sigs + EXECUTE format('CREATE TABLE %I.merchant_order_token_blinded_sigs (' + || ' order_token_bs_serial INT8 GENERATED BY DEFAULT AS IDENTITY,' + || ' order_serial INT8 NOT NULL,' + || ' token_index INT4 NOT NULL,' + || ' token_blinded_signature BYTEA NOT NULL,' + || ' token_hash BYTEA NOT NULL,' + || ' CONSTRAINT merchant_order_token_blinded_sigs_token_hash_check CHECK ((LENGTH(token_hash) = 64)),' + || ' PRIMARY KEY (order_serial, token_index)' + || ')', s); + + -- merchant_orders + EXECUTE format($DDL$CREATE TABLE %I.merchant_orders ( + order_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + order_id TEXT NOT NULL UNIQUE, + claim_token BYTEA NOT NULL, + h_post_data BYTEA NOT NULL, + pay_deadline INT8 NOT NULL, + creation_time INT8 NOT NULL, + contract_terms JSONB NOT NULL, + pos_key TEXT, + pos_algorithm INT4 DEFAULT 0 NOT NULL, + fulfillment_url TEXT, + session_id TEXT DEFAULT ''::text NOT NULL, + CONSTRAINT merchant_orders_claim_token_check CHECK ((LENGTH(claim_token) = 16)), + CONSTRAINT merchant_orders_h_post_data_check CHECK ((LENGTH(h_post_data) = 64)) + )$DDL$, s); + + -- merchant_otp_devices + EXECUTE format('CREATE TABLE %I.merchant_otp_devices (' + || ' otp_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' otp_id TEXT NOT NULL UNIQUE,' + || ' otp_description TEXT NOT NULL,' + || ' otp_key TEXT,' + || ' otp_algorithm INT4 DEFAULT 0 NOT NULL,' + || ' otp_ctr INT8 DEFAULT 0 NOT NULL' + || ')', s); + + -- merchant_pending_webhooks + -- The original (merchant_serial, webhook_pending_serial) UNIQUE + -- collapses to just (webhook_pending_serial), which is already the + -- PK, so no extra UNIQUE constraint is needed. + EXECUTE format('CREATE TABLE %I.merchant_pending_webhooks (' + || ' webhook_pending_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' webhook_serial INT8 NOT NULL,' + || ' next_attempt INT8 DEFAULT 0 NOT NULL,' + || ' retries INT4 DEFAULT 0 NOT NULL,' + || ' url TEXT NOT NULL,' + || ' http_method TEXT NOT NULL,' + || ' header TEXT,' + || ' body TEXT' + || ')', s); + + -- merchant_product_categories + EXECUTE format('CREATE TABLE %I.merchant_product_categories (' + || ' category_serial INT8 NOT NULL,' + || ' product_serial INT8 NOT NULL' + || ')', s); + + -- merchant_product_groups + EXECUTE format('CREATE TABLE %I.merchant_product_groups (' + || ' product_group_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' product_group_name TEXT NOT NULL UNIQUE,' + || ' product_group_description TEXT NOT NULL' + || ')', s); + + -- merchant_refund_proofs + EXECUTE format('CREATE TABLE %I.merchant_refund_proofs (' + || ' refund_serial INT8 NOT NULL PRIMARY KEY,' + || ' exchange_sig BYTEA NOT NULL,' + || ' signkey_serial INT8 NOT NULL,' + || ' CONSTRAINT merchant_refund_proofs_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64))' + || ')', s); + + -- merchant_refunds + EXECUTE format('CREATE TABLE %I.merchant_refunds (' + || ' refund_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' order_serial INT8 NOT NULL,' + || ' rtransaction_id INT8 NOT NULL,' + || ' refund_timestamp INT8 NOT NULL,' + || ' coin_pub BYTEA NOT NULL,' + || ' reason TEXT NOT NULL,' + || ' refund_amount merchant.taler_amount_currency NOT NULL,' + || ' UNIQUE (order_serial, coin_pub, rtransaction_id)' + || ')', s); + + -- merchant_reports + EXECUTE format('CREATE TABLE %I.merchant_reports (' + || ' report_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' report_program_section TEXT NOT NULL,' + || ' report_description TEXT NOT NULL,' + || ' mime_type TEXT NOT NULL,' + || ' report_token BYTEA NOT NULL,' + || ' data_source TEXT NOT NULL,' + || ' target_address TEXT NOT NULL,' + || ' frequency INT8 NOT NULL,' + || ' frequency_shift INT8 NOT NULL,' + || ' next_transmission INT8 NOT NULL,' + || ' last_error_code INT4,' + || ' last_error_detail TEXT,' + || ' one_shot_hidden BOOLEAN DEFAULT FALSE,' + || ' CONSTRAINT merchant_reports_report_token_check CHECK ((LENGTH(report_token) = 32))' + || ')', s); + + -- merchant_used_tokens + EXECUTE format('CREATE TABLE %I.merchant_used_tokens (' + || ' spent_token_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' h_contract_terms BYTEA NOT NULL,' + || ' token_family_key_serial INT8,' + || ' token_pub BYTEA NOT NULL UNIQUE,' + || ' token_sig BYTEA NOT NULL,' + || ' blind_sig BYTEA NOT NULL,' + || ' CONSTRAINT merchant_spent_tokens_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64)),' + || ' CONSTRAINT merchant_spent_tokens_token_pub_check CHECK ((LENGTH(token_pub) = 32)),' + || ' CONSTRAINT merchant_spent_tokens_token_sig_check CHECK ((LENGTH(token_sig) = 64))' + || ')', s); + + -- merchant_statistic_amount_event + EXECUTE format('CREATE TABLE %I.merchant_statistic_amount_event (' + || ' aevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' imeta_serial_id INT8,' + || ' slot INT8 NOT NULL,' + || ' delta_curr character varying(12) NOT NULL,' + || ' delta_value INT8 NOT NULL,' + || ' delta_frac INT4 NOT NULL,' + || ' CONSTRAINT event_key UNIQUE (imeta_serial_id, delta_curr, slot)' + || ')', s); + + -- merchant_statistic_bucket_amount + EXECUTE format('CREATE TABLE %I.merchant_statistic_bucket_amount (' + || ' bmeta_serial_id INT8 NOT NULL,' + || ' bucket_start INT8 NOT NULL,' + || ' bucket_range merchant.statistic_range NOT NULL,' + || ' curr character varying(12) NOT NULL,' + || ' cumulative_value INT8 NOT NULL,' + || ' cumulative_frac INT4 NOT NULL,' + || ' PRIMARY KEY (bmeta_serial_id, curr, bucket_start, bucket_range)' + || ')', s); + + -- merchant_statistic_bucket_counter + EXECUTE format('CREATE TABLE %I.merchant_statistic_bucket_counter (' + || ' bmeta_serial_id INT8 NOT NULL,' + || ' bucket_start INT8 NOT NULL,' + || ' bucket_range merchant.statistic_range NOT NULL,' + || ' cumulative_number INT8 NOT NULL,' + || ' PRIMARY KEY (bmeta_serial_id, bucket_start, bucket_range)' + || ')', s); + + -- merchant_statistic_bucket_meta + EXECUTE format('CREATE TABLE %I.merchant_statistic_bucket_meta (' + || ' bmeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' slug TEXT NOT NULL,' + || ' description TEXT NOT NULL,' + || ' stype merchant.statistic_type NOT NULL,' + || ' ranges merchant.statistic_range[] NOT NULL,' + || ' ages INT4[] NOT NULL,' + || ' CONSTRAINT equal_array_LENGTH CHECK ((array_LENGTH(ranges, 1) = array_LENGTH(ages, 1))),' + || ' UNIQUE (slug, stype)' + || ')', s); + + -- merchant_statistic_counter_event + EXECUTE format('CREATE TABLE %I.merchant_statistic_counter_event (' + || ' nevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' imeta_serial_id INT8,' + || ' slot INT8 NOT NULL,' + || ' delta INT8 NOT NULL,' + || ' UNIQUE (imeta_serial_id, slot)' + || ')', s); + + -- merchant_statistic_interval_amount + EXECUTE format('CREATE TABLE %I.merchant_statistic_interval_amount (' + || ' imeta_serial_id INT8 NOT NULL,' + || ' event_delimiter INT8 NOT NULL,' + || ' range INT8 NOT NULL,' + || ' curr character varying(12) NOT NULL,' + || ' cumulative_value INT8 NOT NULL,' + || ' cumulative_frac INT4 NOT NULL,' + || ' PRIMARY KEY (imeta_serial_id, curr, range)' + || ')', s); + + -- merchant_statistic_interval_counter + EXECUTE format('CREATE TABLE %I.merchant_statistic_interval_counter (' + || ' imeta_serial_id INT8 NOT NULL,' + || ' range INT8 NOT NULL,' + || ' event_delimiter INT8 NOT NULL,' + || ' cumulative_number INT8 NOT NULL,' + || ' PRIMARY KEY (imeta_serial_id, range)' + || ')', s); + + -- merchant_statistic_interval_meta + EXECUTE format('CREATE TABLE %I.merchant_statistic_interval_meta (' + || ' imeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' slug TEXT NOT NULL,' + || ' description TEXT NOT NULL,' + || ' stype merchant.statistic_type NOT NULL,' + || ' ranges INT8[] NOT NULL,' + || ' precisions INT8[] NOT NULL,' + || ' CONSTRAINT equal_array_LENGTH CHECK ((array_LENGTH(ranges, 1) = array_LENGTH(precisions, 1))),' + || ' CONSTRAINT merchant_statistic_interval_meta_precisions_check CHECK ((array_LENGTH(precisions, 1) > 0)),' + || ' CONSTRAINT merchant_statistic_interval_meta_ranges_check CHECK ((array_LENGTH(ranges, 1) > 0)),' + || ' UNIQUE (slug, stype)' + || ')', s); + + -- merchant_template + EXECUTE format('CREATE TABLE %I.merchant_template (' + || ' template_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' template_id TEXT NOT NULL UNIQUE,' + || ' template_description TEXT NOT NULL,' + || ' otp_device_id INT8,' + || ' template_contract JSONB NOT NULL,' + || ' editable_defaults JSONB' + || ')', s); + + -- merchant_token_families + EXECUTE format($DDL$CREATE TABLE %I.merchant_token_families ( + token_family_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT, + description_i18n JSONB NOT NULL, + valid_after INT8 NOT NULL, + valid_before INT8 NOT NULL, + duration INT8 NOT NULL, + kind TEXT NOT NULL, + issued INT8 DEFAULT 0, + used INT8 DEFAULT 0, + validity_granularity INT8 DEFAULT '2592000000000'::INT8 NOT NULL, + start_offset INT8 DEFAULT 0 NOT NULL, + cipher_choice TEXT DEFAULT 'rsa(2048)'::text NOT NULL, + extra_data JSONB, + CONSTRAINT merchant_token_families_kind_check + CHECK ((kind = ANY (ARRAY['subscription'::text, 'discount'::text]))), + CONSTRAINT merchant_token_families_validity_granularity_check + CHECK ((validity_granularity = ANY (ARRAY[(60000000)::INT8, + '3600000000'::INT8, '86400000000'::INT8, '604800000000'::INT8, + '2592000000000'::INT8, '7776000000000'::INT8, + '31536000000000'::INT8]))) + )$DDL$, s); + + -- merchant_token_family_keys + EXECUTE format('CREATE TABLE %I.merchant_token_family_keys (' + || ' token_family_key_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' token_family_serial INT8,' + || ' pub BYTEA NOT NULL,' + || ' h_pub BYTEA NOT NULL UNIQUE,' + || ' priv BYTEA,' + || ' cipher TEXT NOT NULL,' + || ' signature_validity_start INT8 DEFAULT 0 NOT NULL,' + || ' signature_validity_end INT8 DEFAULT 0 NOT NULL,' + || ' private_key_deleted_at INT8 DEFAULT 0 NOT NULL,' + || ' private_key_created_at INT8 DEFAULT 0 NOT NULL,' + || ' CONSTRAINT h_pub_LENGTH_check CHECK ((LENGTH(h_pub) = 64)),' + || $DDL$ CONSTRAINT merchant_token_family_keys_cipher_check CHECK ((cipher = ANY (ARRAY['rsa'::text, 'cs'::text])))$DDL$ + || ')', s); + + -- merchant_transfer_signatures + EXECUTE format('CREATE TABLE %I.merchant_transfer_signatures (' + || ' expected_credit_serial INT8 NOT NULL PRIMARY KEY,' + || ' signkey_serial INT8 NOT NULL,' + || ' wire_fee merchant.taler_amount_currency NOT NULL,' + || ' credit_amount merchant.taler_amount_currency NOT NULL,' + || ' execution_time INT8 NOT NULL,' + || ' exchange_sig BYTEA NOT NULL,' + || ' CONSTRAINT merchant_transfer_signatures_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64))' + || ')', s); + + -- merchant_transfers + EXECUTE format('CREATE TABLE %I.merchant_transfers (' + || ' credit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' exchange_url TEXT NOT NULL,' + || ' wtid BYTEA,' + || ' credit_amount merchant.taler_amount_currency NOT NULL,' + || ' account_serial INT8 NOT NULL,' + || ' bank_serial_id INT8,' + || ' expected BOOLEAN DEFAULT FALSE,' + || ' execution_time INT8 DEFAULT 0,' + || ' CONSTRAINT merchant_transfers_wtid_check CHECK ((LENGTH(wtid) = 32)),' + || ' CONSTRAINT merchant_transfers_unique UNIQUE (wtid, exchange_url, account_serial, bank_serial_id)' + || ')', s); + + -- merchant_unclaim_signatures + EXECUTE format('CREATE TABLE %I.merchant_unclaim_signatures (' + || ' unclaim_serial INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE,' + || ' h_contract_terms BYTEA NOT NULL,' + || ' unclaim_sig BYTEA NOT NULL PRIMARY KEY,' + || ' expiration_time INT8 NOT NULL,' + || ' CONSTRAINT merchant_unclaim_signatures_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64)),' + || ' CONSTRAINT merchant_unclaim_signatures_unclaim_sig_check CHECK ((LENGTH(unclaim_sig) = 64))' + || ')', s); + + -- merchant_webhook + EXECUTE format('CREATE TABLE %I.merchant_webhook (' + || ' webhook_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,' + || ' webhook_id TEXT NOT NULL UNIQUE,' + || ' event_type TEXT NOT NULL,' + || ' url TEXT NOT NULL,' + || ' http_method TEXT NOT NULL,' + || ' header_template TEXT,' + || ' body_template TEXT' + || ')', s); + + -- tan_challenges + EXECUTE format('CREATE TABLE %I.tan_challenges (' + || ' challenge_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE,' + || ' h_body BYTEA NOT NULL,' + || ' salt BYTEA NOT NULL,' + || ' op merchant.op_enum NOT NULL,' + || ' code TEXT NOT NULL,' + || ' creation_date INT8 NOT NULL,' + || ' expiration_date INT8 NOT NULL,' + || ' retransmission_date INT8 DEFAULT 0 NOT NULL,' + || ' confirmation_date INT8,' + || ' retry_counter INT4 NOT NULL,' + || ' tan_channel merchant.tan_enum NOT NULL,' + || ' required_address TEXT NOT NULL,' + || ' CONSTRAINT tan_challenges_h_body_check CHECK ((LENGTH(h_body) = 32)),' + || ' CONSTRAINT tan_challenges_salt_check CHECK ((LENGTH(salt) = 16))' + || ')', s); + + -- ------------------------------------------------------------------- + -- 3. CREATE INDEX statements (per-instance only). + -- Index names are bare; PostgreSQL scopes them by schema, so the + -- same logical name in each instance schema is fine. + -- merchant_serial leading columns are dropped from the column list. + -- ------------------------------------------------------------------- + + -- merchant_product_categories + EXECUTE format('CREATE INDEX merchant_categories_by_category' + || ' ON %I.merchant_product_categories USING btree (category_serial)', s); + EXECUTE format('CREATE INDEX merchant_categories_by_product' + || ' ON %I.merchant_product_categories USING btree (product_serial)', s); + + -- merchant_contract_terms + EXECUTE format('CREATE INDEX merchant_contract_terms_by_expiration' + || ' ON %I.merchant_contract_terms USING btree (paid, pay_deadline)', s); + EXECUTE format('CREATE INDEX merchant_contract_terms_by_merchant_and_expiration' + || ' ON %I.merchant_contract_terms USING btree (pay_deadline)', s); + EXECUTE format('CREATE INDEX merchant_contract_terms_by_merchant_and_payment' + || ' ON %I.merchant_contract_terms USING btree (paid)', s); + EXECUTE format('CREATE INDEX merchant_contract_terms_by_merchant_and_session' + || ' ON %I.merchant_contract_terms USING btree (session_id)', s); + EXECUTE format('CREATE INDEX merchant_contract_terms_by_merchant_session_and_fulfillment' + || ' ON %I.merchant_contract_terms USING btree (fulfillment_url, session_id)', s); + + -- merchant_deposit_confirmations + EXECUTE format('CREATE INDEX merchant_deposit_confirmations_by_pending_wire' + || ' ON %I.merchant_deposit_confirmations USING btree (exchange_url, wire_transfer_deadline)' + || ' WHERE wire_pending', s); + + -- merchant_deposits + EXECUTE format('CREATE INDEX merchant_deposits_by_deposit_confirmation' + || ' ON %I.merchant_deposits USING btree (deposit_confirmation_serial)', s); + + EXECUTE format('CREATE INDEX merchant_deposits_by_settlement_open' + || ' ON %I.merchant_deposits USING btree (settlement_retry_time)' + || ' WHERE settlement_retry_needed', s); + + -- merchant_expected_transfers + EXECUTE format('CREATE INDEX merchant_expected_transfers_by_open' + || ' ON %I.merchant_expected_transfers USING btree (retry_time)' + || ' WHERE ((NOT confirmed) OR retry_needed)', s); + + -- merchant_inventory + EXECUTE format('CREATE INDEX merchant_inventory_by_image_hash' + || ' ON %I.merchant_inventory USING btree (image_hash)', s); + + -- merchant_inventory_locks + EXECUTE format('CREATE INDEX merchant_inventory_locks_by_expiration' + || ' ON %I.merchant_inventory_locks USING btree (expiration)', s); + EXECUTE format('CREATE INDEX merchant_inventory_locks_by_uuid' + || ' ON %I.merchant_inventory_locks USING btree (lock_uuid)', s); + + -- merchant_kyc + EXECUTE format('CREATE INDEX merchant_kyc_by_next_kyc_poll' + || ' ON %I.merchant_kyc USING btree (next_kyc_poll)', s); + + -- merchant_login_tokens + EXECUTE format('CREATE INDEX merchant_login_tokens_by_expiration' + || ' ON %I.merchant_login_tokens USING btree (expiration_time)', s); + + -- merchant_orders + EXECUTE format('CREATE INDEX merchant_orders_by_creation_time' + || ' ON %I.merchant_orders USING btree (creation_time)', s); + EXECUTE format('CREATE INDEX merchant_orders_by_expiration' + || ' ON %I.merchant_orders USING btree (pay_deadline)', s); + EXECUTE format('CREATE INDEX merchant_orders_by_merchant_and_fullfilment_and_session' + || ' ON %I.merchant_orders USING btree (fulfillment_url, session_id)', s); + EXECUTE format('CREATE INDEX merchant_orders_by_merchant_and_session' + || ' ON %I.merchant_orders USING btree (session_id)', s); + + -- merchant_order_locks + EXECUTE format('CREATE INDEX merchant_orders_locks_by_order_and_product' + || ' ON %I.merchant_order_locks USING btree (order_serial, product_serial)', s); + + -- merchant_refunds + EXECUTE format('CREATE INDEX merchant_refunds_by_coin_and_order' + || ' ON %I.merchant_refunds USING btree (coin_pub, order_serial)', s); + + -- merchant_expected_transfer_to_coin + EXECUTE format('CREATE INDEX merchant_transfers_by_credit' + || ' ON %I.merchant_expected_transfer_to_coin USING btree (expected_credit_serial)', s); + + -- merchant_unclaim_signatures + EXECUTE format('CREATE INDEX merchant_unclaim_signatures_by_expiration' + || ' ON %I.merchant_unclaim_signatures USING btree (expiration_time)', s); + + -- tan_challenges + EXECUTE format('CREATE INDEX tan_challenges_expiration_index' + || ' ON %I.tan_challenges USING btree (expiration_date)', s); + + -- trigram indexes + EXECUTE format('CREATE INDEX trgm_idx_categories_by_name' + || ' ON %I.merchant_categories USING gin (lower(category_name) public.gin_trgm_ops)', s); + EXECUTE format($DDL$CREATE INDEX trgm_idx_contract_summaries + ON %I.merchant_contract_terms USING gin (lower((contract_terms ->> 'summary'::text)) public.gin_trgm_ops)$DDL$, s); + EXECUTE format($DDL$CREATE INDEX trgm_idx_order_summaries + ON %I.merchant_orders USING gin (lower((contract_terms ->> 'summary'::text)) public.gin_trgm_ops)$DDL$, s); + EXECUTE format('CREATE INDEX trgm_idx_products_by_description' + || ' ON %I.merchant_inventory USING gin (lower(description) public.gin_trgm_ops)', s); + EXECUTE format('CREATE INDEX trgm_idx_products_by_name' + || ' ON %I.merchant_inventory USING gin (lower(product_name) public.gin_trgm_ops)', s); + + -- ------------------------------------------------------------------- + -- 4. ALTER TABLE for FKs *between per-instance tables* (intra-schema). + -- Both sides live in `s`; use format(..., s, s). + -- ------------------------------------------------------------------- + + -- merchant_deposit_confirmations -> merchant_accounts + EXECUTE format('ALTER TABLE %I.merchant_deposit_confirmations' + || ' ADD CONSTRAINT merchant_deposit_confirmations_account_serial_fkey' + || ' FOREIGN KEY (account_serial)' + || ' REFERENCES %I.merchant_accounts(account_serial) ON DELETE CASCADE', s, s); + + -- merchant_deposit_confirmations -> merchant_contract_terms + EXECUTE format('ALTER TABLE %I.merchant_deposit_confirmations' + || ' ADD CONSTRAINT merchant_deposit_confirmations_order_serial_fkey' + || ' FOREIGN KEY (order_serial)' + || ' REFERENCES %I.merchant_contract_terms(order_serial) ON DELETE CASCADE', s, s); + + -- merchant_deposits -> merchant_deposit_confirmations + EXECUTE format('ALTER TABLE %I.merchant_deposits' + || ' ADD CONSTRAINT merchant_deposits_deposit_confirmation_serial_fkey' + || ' FOREIGN KEY (deposit_confirmation_serial)' + || ' REFERENCES %I.merchant_deposit_confirmations(deposit_confirmation_serial) ON DELETE CASCADE', s, s); + + -- merchant_deposits -> merchant_expected_transfers (no ON DELETE clause originally) + EXECUTE format('ALTER TABLE %I.merchant_deposits' + || ' ADD CONSTRAINT merchant_deposits_settlement_expected_credit_serial_fkey' + || ' FOREIGN KEY (settlement_expected_credit_serial)' + || ' REFERENCES %I.merchant_expected_transfers(expected_credit_serial)', s, s); + + -- merchant_expected_transfer_to_coin -> merchant_deposits + EXECUTE format('ALTER TABLE %I.merchant_expected_transfer_to_coin' + || ' ADD CONSTRAINT merchant_expected_transfer_to_coin_deposit_serial_fkey' + || ' FOREIGN KEY (deposit_serial)' + || ' REFERENCES %I.merchant_deposits(deposit_serial) ON DELETE CASCADE', s, s); + + -- merchant_expected_transfer_to_coin -> merchant_expected_transfers + EXECUTE format('ALTER TABLE %I.merchant_expected_transfer_to_coin' + || ' ADD CONSTRAINT merchant_expected_transfer_to_coin_expected_credit_serial_fkey' + || ' FOREIGN KEY (expected_credit_serial)' + || ' REFERENCES %I.merchant_expected_transfers(expected_credit_serial) ON DELETE CASCADE', s, s); + + -- merchant_expected_transfers -> merchant_accounts + EXECUTE format('ALTER TABLE %I.merchant_expected_transfers' + || ' ADD CONSTRAINT merchant_expected_transfers_account_serial_fkey' + || ' FOREIGN KEY (account_serial)' + || ' REFERENCES %I.merchant_accounts(account_serial) ON DELETE CASCADE', s, s); + + -- merchant_inventory -> merchant_money_pots + EXECUTE format('ALTER TABLE %I.merchant_inventory' + || ' ADD CONSTRAINT merchant_inventory_money_pot_serial_fkey' + || ' FOREIGN KEY (money_pot_serial)' + || ' REFERENCES %I.merchant_money_pots(money_pot_serial) ON DELETE SET NULL', s, s); + + -- merchant_inventory -> merchant_product_groups + EXECUTE format('ALTER TABLE %I.merchant_inventory' + || ' ADD CONSTRAINT merchant_inventory_product_group_serial_fkey' + || ' FOREIGN KEY (product_group_serial)' + || ' REFERENCES %I.merchant_product_groups(product_group_serial) ON DELETE SET NULL', s, s); + + -- merchant_inventory_locks -> merchant_inventory + EXECUTE format('ALTER TABLE %I.merchant_inventory_locks' + || ' ADD CONSTRAINT merchant_inventory_locks_product_serial_fkey' + || ' FOREIGN KEY (product_serial)' + || ' REFERENCES %I.merchant_inventory(product_serial) ON DELETE CASCADE', s, s); + + -- merchant_issued_tokens -> merchant_token_family_keys + EXECUTE format('ALTER TABLE %I.merchant_issued_tokens' + || ' ADD CONSTRAINT merchant_issued_tokens_token_family_key_serial_fkey' + || ' FOREIGN KEY (token_family_key_serial)' + || ' REFERENCES %I.merchant_token_family_keys(token_family_key_serial) ON DELETE CASCADE', s, s); + + -- merchant_kyc -> merchant_accounts + EXECUTE format('ALTER TABLE %I.merchant_kyc' + || ' ADD CONSTRAINT merchant_kyc_account_serial_fkey' + || ' FOREIGN KEY (account_serial)' + || ' REFERENCES %I.merchant_accounts(account_serial) ON DELETE CASCADE', s, s); + + -- merchant_order_locks -> merchant_orders + EXECUTE format('ALTER TABLE %I.merchant_order_locks' + || ' ADD CONSTRAINT merchant_order_locks_order_serial_fkey' + || ' FOREIGN KEY (order_serial)' + || ' REFERENCES %I.merchant_orders(order_serial) ON DELETE CASCADE', s, s); + + -- merchant_order_locks -> merchant_inventory (no ON DELETE clause originally) + EXECUTE format('ALTER TABLE %I.merchant_order_locks' + || ' ADD CONSTRAINT merchant_order_locks_product_serial_fkey' + || ' FOREIGN KEY (product_serial)' + || ' REFERENCES %I.merchant_inventory(product_serial)', s, s); + + -- merchant_order_token_blinded_sigs -> merchant_contract_terms + EXECUTE format('ALTER TABLE %I.merchant_order_token_blinded_sigs' + || ' ADD CONSTRAINT merchant_order_token_blinded_sigs_order_serial_fkey' + || ' FOREIGN KEY (order_serial)' + || ' REFERENCES %I.merchant_contract_terms(order_serial) ON DELETE CASCADE', s, s); + + -- merchant_pending_webhooks -> merchant_webhook + EXECUTE format('ALTER TABLE %I.merchant_pending_webhooks' + || ' ADD CONSTRAINT merchant_pending_webhooks_webhook_serial_fkey' + || ' FOREIGN KEY (webhook_serial)' + || ' REFERENCES %I.merchant_webhook(webhook_serial) ON DELETE CASCADE', s, s); + + -- merchant_product_categories -> merchant_categories + EXECUTE format('ALTER TABLE %I.merchant_product_categories' + || ' ADD CONSTRAINT merchant_product_categories_category_serial_fkey' + || ' FOREIGN KEY (category_serial)' + || ' REFERENCES %I.merchant_categories(category_serial) ON DELETE CASCADE', s, s); + + -- merchant_product_categories -> merchant_inventory + EXECUTE format('ALTER TABLE %I.merchant_product_categories' + || ' ADD CONSTRAINT merchant_product_categories_product_serial_fkey' + || ' FOREIGN KEY (product_serial)' + || ' REFERENCES %I.merchant_inventory(product_serial) ON DELETE CASCADE', s, s); + + -- merchant_refund_proofs -> merchant_refunds + EXECUTE format('ALTER TABLE %I.merchant_refund_proofs' + || ' ADD CONSTRAINT merchant_refund_proofs_refund_serial_fkey' + || ' FOREIGN KEY (refund_serial)' + || ' REFERENCES %I.merchant_refunds(refund_serial) ON DELETE CASCADE', s, s); + + -- merchant_refunds -> merchant_contract_terms + EXECUTE format('ALTER TABLE %I.merchant_refunds' + || ' ADD CONSTRAINT merchant_refunds_order_serial_fkey' + || ' FOREIGN KEY (order_serial)' + || ' REFERENCES %I.merchant_contract_terms(order_serial) ON DELETE CASCADE', s, s); + + -- merchant_used_tokens -> merchant_token_family_keys + EXECUTE format('ALTER TABLE %I.merchant_used_tokens' + || ' ADD CONSTRAINT merchant_spent_tokens_token_family_key_serial_fkey' + || ' FOREIGN KEY (token_family_key_serial)' + || ' REFERENCES %I.merchant_token_family_keys(token_family_key_serial) ON DELETE CASCADE', s, s); + + -- merchant_statistic_amount_event -> merchant_statistic_interval_meta + EXECUTE format('ALTER TABLE %I.merchant_statistic_amount_event' + || ' ADD CONSTRAINT merchant_statistic_amount_event_imeta_serial_id_fkey' + || ' FOREIGN KEY (imeta_serial_id)' + || ' REFERENCES %I.merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE', s, s); + + -- merchant_statistic_bucket_amount -> merchant_statistic_bucket_meta + EXECUTE format('ALTER TABLE %I.merchant_statistic_bucket_amount' + || ' ADD CONSTRAINT merchant_statistic_bucket_amount_bmeta_serial_id_fkey' + || ' FOREIGN KEY (bmeta_serial_id)' + || ' REFERENCES %I.merchant_statistic_bucket_meta(bmeta_serial_id) ON DELETE CASCADE', s, s); + + -- merchant_statistic_bucket_counter -> merchant_statistic_bucket_meta + EXECUTE format('ALTER TABLE %I.merchant_statistic_bucket_counter' + || ' ADD CONSTRAINT merchant_statistic_bucket_counter_bmeta_serial_id_fkey' + || ' FOREIGN KEY (bmeta_serial_id)' + || ' REFERENCES %I.merchant_statistic_bucket_meta(bmeta_serial_id) ON DELETE CASCADE', s, s); + + -- merchant_statistic_counter_event -> merchant_statistic_interval_meta + EXECUTE format('ALTER TABLE %I.merchant_statistic_counter_event' + || ' ADD CONSTRAINT merchant_statistic_counter_event_imeta_serial_id_fkey' + || ' FOREIGN KEY (imeta_serial_id)' + || ' REFERENCES %I.merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE', s, s); + + -- merchant_statistic_interval_amount -> merchant_statistic_amount_event + EXECUTE format('ALTER TABLE %I.merchant_statistic_interval_amount' + || ' ADD CONSTRAINT merchant_statistic_interval_amount_event_delimiter_fkey' + || ' FOREIGN KEY (event_delimiter)' + || ' REFERENCES %I.merchant_statistic_amount_event(aevent_serial_id) ON DELETE RESTRICT', s, s); + + -- merchant_statistic_interval_amount -> merchant_statistic_interval_meta + EXECUTE format('ALTER TABLE %I.merchant_statistic_interval_amount' + || ' ADD CONSTRAINT merchant_statistic_interval_amount_imeta_serial_id_fkey' + || ' FOREIGN KEY (imeta_serial_id)' + || ' REFERENCES %I.merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE', s, s); + + -- merchant_statistic_interval_counter -> merchant_statistic_counter_event + EXECUTE format('ALTER TABLE %I.merchant_statistic_interval_counter' + || ' ADD CONSTRAINT merchant_statistic_interval_counter_event_delimiter_fkey' + || ' FOREIGN KEY (event_delimiter)' + || ' REFERENCES %I.merchant_statistic_counter_event(nevent_serial_id) ON DELETE RESTRICT', s, s); + + -- merchant_statistic_interval_counter -> merchant_statistic_interval_meta + EXECUTE format('ALTER TABLE %I.merchant_statistic_interval_counter' + || ' ADD CONSTRAINT merchant_statistic_interval_counter_imeta_serial_id_fkey' + || ' FOREIGN KEY (imeta_serial_id)' + || ' REFERENCES %I.merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE', s, s); + + -- merchant_template -> merchant_otp_devices + EXECUTE format('ALTER TABLE %I.merchant_template' + || ' ADD CONSTRAINT merchant_template_otp_device_id_fkey' + || ' FOREIGN KEY (otp_device_id)' + || ' REFERENCES %I.merchant_otp_devices(otp_serial) ON DELETE SET NULL', s, s); + + -- merchant_token_family_keys -> merchant_token_families + EXECUTE format('ALTER TABLE %I.merchant_token_family_keys' + || ' ADD CONSTRAINT merchant_token_family_keys_token_family_serial_fkey' + || ' FOREIGN KEY (token_family_serial)' + || ' REFERENCES %I.merchant_token_families(token_family_serial) ON DELETE CASCADE', s, s); + + -- merchant_transfer_signatures -> merchant_expected_transfers + EXECUTE format('ALTER TABLE %I.merchant_transfer_signatures' + || ' ADD CONSTRAINT merchant_transfer_signatures_expected_credit_serial_fkey' + || ' FOREIGN KEY (expected_credit_serial)' + || ' REFERENCES %I.merchant_expected_transfers(expected_credit_serial) ON DELETE CASCADE', s, s); + + -- merchant_transfers -> merchant_accounts + EXECUTE format('ALTER TABLE %I.merchant_transfers' + || ' ADD CONSTRAINT merchant_transfers_account_serial_fkey' + || ' FOREIGN KEY (account_serial)' + || ' REFERENCES %I.merchant_accounts(account_serial) ON DELETE CASCADE', s, s); + + -- ------------------------------------------------------------------- + -- 5. ALTER TABLE for FKs to *global* merchant.* tables. + -- Right-hand side keeps `merchant.` qualification. + -- ------------------------------------------------------------------- + + -- merchant_builtin_unit_overrides -> merchant.merchant_builtin_units + EXECUTE format('ALTER TABLE %I.merchant_builtin_unit_overrides' + || ' ADD CONSTRAINT merchant_builtin_unit_overrides_builtin_unit_serial_fkey' + || ' FOREIGN KEY (builtin_unit_serial)' + || ' REFERENCES merchant.merchant_builtin_units(unit_serial) ON DELETE CASCADE', s); + + -- merchant_deposit_confirmations -> merchant.merchant_exchange_signing_keys + EXECUTE format('ALTER TABLE %I.merchant_deposit_confirmations' + || ' ADD CONSTRAINT merchant_deposit_confirmations_signkey_serial_fkey' + || ' FOREIGN KEY (signkey_serial)' + || ' REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE', s); + + -- merchant_deposits -> merchant.merchant_exchange_signing_keys + EXECUTE format('ALTER TABLE %I.merchant_deposits' + || ' ADD CONSTRAINT merchant_deposits_signkey_serial_fkey' + || ' FOREIGN KEY (signkey_serial)' + || ' REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE', s); + + -- merchant_expected_transfers -> merchant.merchant_exchange_signing_keys + EXECUTE format('ALTER TABLE %I.merchant_expected_transfers' + || ' ADD CONSTRAINT merchant_expected_transfers_signkey_serial_fkey' + || ' FOREIGN KEY (signkey_serial)' + || ' REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE', s); + + -- merchant_refund_proofs -> merchant.merchant_exchange_signing_keys + EXECUTE format('ALTER TABLE %I.merchant_refund_proofs' + || ' ADD CONSTRAINT merchant_refund_proofs_signkey_serial_fkey' + || ' FOREIGN KEY (signkey_serial)' + || ' REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE', s); + + -- merchant_transfer_signatures -> merchant.merchant_exchange_signing_keys + EXECUTE format('ALTER TABLE %I.merchant_transfer_signatures' + || ' ADD CONSTRAINT merchant_transfer_signatures_signkey_serial_fkey' + || ' FOREIGN KEY (signkey_serial)' + || ' REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE', s); + + -- ===================================================================== + -- (FKs back to merchant.merchant_instances are intentionally dropped: + -- the existence of the per-instance schema implies that relationship.) + -- ===================================================================== + +END +$$; + +INSERT INTO merchant.instance_fixups + (migration_name + ,version) + VALUES + ('merchant_0036_init' + ,36); diff --git a/src/backenddb/sql-schema/merchant-0036-setval.sql.fragment b/src/backenddb/sql-schema/merchant-0036-setval.sql.fragment @@ -0,0 +1,33 @@ + -- Advance every IDENTITY sequence in the new instance schema past the + -- maximum value just copied so future inserts do not collide. + PERFORM setval(pg_get_serial_sequence(s || '.merchant_accounts', 'account_serial'), COALESCE((SELECT MAX(account_serial) FROM merchant.merchant_accounts WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_categories', 'category_serial'), COALESCE((SELECT MAX(category_serial) FROM merchant.merchant_categories WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_contract_terms', 'order_serial'), COALESCE((SELECT MAX(order_serial) FROM merchant.merchant_contract_terms WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_custom_units', 'unit_serial'), COALESCE((SELECT MAX(unit_serial) FROM merchant.merchant_custom_units WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_donau_instances', 'donau_instances_serial'), COALESCE((SELECT MAX(donau_instances_serial) FROM merchant.merchant_donau_instances WHERE merchant_instance_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_inventory', 'product_serial'), COALESCE((SELECT MAX(product_serial) FROM merchant.merchant_inventory WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_login_tokens', 'serial'), COALESCE((SELECT MAX(serial) FROM merchant.merchant_login_tokens WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_money_pots', 'money_pot_serial'), COALESCE((SELECT MAX(money_pot_serial) FROM merchant.merchant_money_pots WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_orders', 'order_serial'), COALESCE((SELECT MAX(order_serial) FROM merchant.merchant_orders WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_otp_devices', 'otp_serial'), COALESCE((SELECT MAX(otp_serial) FROM merchant.merchant_otp_devices WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_product_groups', 'product_group_serial'), COALESCE((SELECT MAX(product_group_serial) FROM merchant.merchant_product_groups WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_reports', 'report_serial'), COALESCE((SELECT MAX(report_serial) FROM merchant.merchant_reports WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_statistic_amount_event', 'aevent_serial_id'), COALESCE((SELECT MAX(aevent_serial_id) FROM merchant.merchant_statistic_amount_event WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_statistic_bucket_meta', 'bmeta_serial_id'), COALESCE((SELECT MAX(bmeta_serial_id) FROM merchant.merchant_statistic_bucket_meta), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_statistic_counter_event', 'nevent_serial_id'), COALESCE((SELECT MAX(nevent_serial_id) FROM merchant.merchant_statistic_counter_event WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_statistic_interval_meta', 'imeta_serial_id'), COALESCE((SELECT MAX(imeta_serial_id) FROM merchant.merchant_statistic_interval_meta), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_template', 'template_serial'), COALESCE((SELECT MAX(template_serial) FROM merchant.merchant_template WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_token_families', 'token_family_serial'), COALESCE((SELECT MAX(token_family_serial) FROM merchant.merchant_token_families WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_webhook', 'webhook_serial'), COALESCE((SELECT MAX(webhook_serial) FROM merchant.merchant_webhook WHERE merchant_serial = rec.merchant_serial), 0) + 1, false); + -- Indirect-FK tables (owning instance via JOIN) + PERFORM setval(pg_get_serial_sequence(s || '.merchant_kyc', 'kyc_serial_id'), COALESCE((SELECT MAX(k.kyc_serial_id) FROM merchant.merchant_kyc k JOIN merchant.merchant_accounts a ON k.account_serial = a.account_serial WHERE a.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_deposit_confirmations', 'deposit_confirmation_serial'), COALESCE((SELECT MAX(dc.deposit_confirmation_serial) FROM merchant.merchant_deposit_confirmations dc JOIN merchant.merchant_accounts a ON dc.account_serial = a.account_serial WHERE a.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_expected_transfers', 'expected_credit_serial'), COALESCE((SELECT MAX(et.expected_credit_serial) FROM merchant.merchant_expected_transfers et JOIN merchant.merchant_accounts a ON et.account_serial = a.account_serial WHERE a.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_transfers', 'credit_serial'), COALESCE((SELECT MAX(t.credit_serial) FROM merchant.merchant_transfers t JOIN merchant.merchant_accounts a ON t.account_serial = a.account_serial WHERE a.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_deposits', 'deposit_serial'), COALESCE((SELECT MAX(d.deposit_serial) FROM merchant.merchant_deposits d JOIN merchant.merchant_deposit_confirmations dc ON d.deposit_confirmation_serial = dc.deposit_confirmation_serial JOIN merchant.merchant_accounts a ON dc.account_serial = a.account_serial WHERE a.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_refunds', 'refund_serial'), COALESCE((SELECT MAX(r.refund_serial) FROM merchant.merchant_refunds r JOIN merchant.merchant_contract_terms ct ON r.order_serial = ct.order_serial WHERE ct.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_token_family_keys', 'token_family_key_serial'), COALESCE((SELECT MAX(tfk.token_family_key_serial) FROM merchant.merchant_token_family_keys tfk JOIN merchant.merchant_token_families tf ON tfk.token_family_serial = tf.token_family_serial WHERE tf.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_issued_tokens', 'issued_token_serial'), COALESCE((SELECT MAX(it.issued_token_serial) FROM merchant.merchant_issued_tokens it JOIN merchant.merchant_token_family_keys tfk ON it.token_family_key_serial = tfk.token_family_key_serial JOIN merchant.merchant_token_families tf ON tfk.token_family_serial = tf.token_family_serial WHERE tf.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_used_tokens', 'spent_token_serial'), COALESCE((SELECT MAX(ut.spent_token_serial) FROM merchant.merchant_used_tokens ut JOIN merchant.merchant_token_family_keys tfk ON ut.token_family_key_serial = tfk.token_family_key_serial JOIN merchant.merchant_token_families tf ON tfk.token_family_serial = tf.token_family_serial WHERE tf.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_unclaim_signatures', 'unclaim_serial'), COALESCE((SELECT MAX(us.unclaim_serial) FROM merchant.merchant_unclaim_signatures us JOIN merchant.merchant_contract_terms ct ON us.h_contract_terms = ct.h_contract_terms WHERE ct.merchant_serial = rec.merchant_serial), 0) + 1, false); + PERFORM setval(pg_get_serial_sequence(s || '.merchant_order_token_blinded_sigs','order_token_bs_serial'), COALESCE((SELECT MAX(otbs.order_token_bs_serial) FROM merchant.merchant_order_token_blinded_sigs otbs JOIN merchant.merchant_contract_terms ct ON otbs.order_serial = ct.order_serial WHERE ct.merchant_serial = rec.merchant_serial), 0) + 1, false); diff --git a/src/backenddb/sql-schema/merchant-0036.sql.in b/src/backenddb/sql-schema/merchant-0036.sql.in @@ -0,0 +1,124 @@ +-- +-- 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-0036.sql.in +-- @brief Move per-instance tables into per-instance schemas +-- merchant_instance_<merchant_serial>. Each existing row of +-- merchant_instances is converted by calling +-- merchant.create_instance_schema(serial), copying the rows of every +-- per-instance table into the new schema (dropping the merchant_serial +-- column), and finally dropping the now-empty merchant.<table>. +-- @author Christian Grothoff + +BEGIN; + +SELECT _v.register_patch('merchant-0036', NULL, NULL); + +SET search_path TO merchant; + +-- --------------------------------------------------------------------- +-- Step 0: Fold merchant_keys.merchant_priv into merchant_instances as a +-- nullable column. Setting merchant_priv to NULL replaces deleting the +-- key row. The merchant_keys table itself is dropped in step 4. +-- --------------------------------------------------------------------- +ALTER TABLE merchant.merchant_instances + ADD COLUMN merchant_priv BYTEA + CHECK (merchant_priv IS NULL OR LENGTH(merchant_priv) = 32); +COMMENT ON COLUMN merchant.merchant_instances.merchant_priv + IS 'private key of the instance, NULL once the key has been "deleted"'; +UPDATE merchant.merchant_instances mi + SET merchant_priv = mk.merchant_priv + FROM merchant.merchant_keys mk + WHERE mk.merchant_serial = mi.merchant_serial; + +CREATE TABLE merchant.instance_fixups + (instance_fixup_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,migration_name TEXT NOT NULL + ,version INT8 NOT NULL); +COMMENT ON TABLE instance_fixups + IS 'Fixups to setup the per-instance tables'; +COMMENT ON COLUMN instance_fixups.migration_name + IS 'Name of the stored procedure to run to fix up the schema of the instance'; +COMMENT ON COLUMN instance_fixups.version + IS 'Version of the DB in which the given fix up should happen'; + +CREATE INDEX instance_fixups_by_version + ON instance_fixups (version); +COMMENT ON INDEX instance_fixups_by_version + IS 'Used by create_instance_schema (and in the future on import)'; + +-- --------------------------------------------------------------------- +-- Step 1: Install the schema constructor and actual fix-ups. +-- procedures.sql will re-install the same definition (DROP IF EXISTS), +-- so installing it here is harmless and lets the migration call it. +-- --------------------------------------------------------------------- +#include "../create_tables.sql" +#include "../pg_create_instance_schema.sql" +#include "merchant-0036-init.sql.fragment" + +-- --------------------------------------------------------------------- +-- Step 2: Build a per-instance schema for every existing instance. +-- --------------------------------------------------------------------- +DO $MIG$ +DECLARE + rec RECORD; +BEGIN + FOR rec IN + SELECT merchant_serial + FROM merchant.merchant_instances + LOOP + PERFORM merchant.create_instance_schema(rec.merchant_serial); + END LOOP; +END $MIG$; + +-- --------------------------------------------------------------------- +-- Step 3: Migrate row data per instance. +-- Tables are copied in FK-dependency order. Every copy drops the +-- merchant_serial column. After each table is copied, its IDENTITY +-- sequence (if any) is advanced past MAX(serial). +-- Indirect-FK tables (merchant_kyc, merchant_deposits, ...) are copied +-- by JOINing back to the table that owns the merchant_serial. +-- --------------------------------------------------------------------- +-- Suppress USER triggers during the bulk copy: per-instance triggers +-- (e.g. merchant_kyc_insert_trigger) would otherwise fire on every +-- inserted row and CALL into per-instance procedures that may rely on +-- runtime invariants we do not have during migration. This setting is +-- transaction-local and does not affect normal callers. +SET session_replication_role = replica; + +DO $MIG$ +DECLARE + rec RECORD; + s TEXT; +BEGIN + FOR rec IN + SELECT merchant_serial + FROM merchant.merchant_instances + LOOP + s := 'merchant_instance_' || rec.merchant_serial::TEXT; +#include "merchant-0036-copy.sql.fragment" +#include "merchant-0036-setval.sql.fragment" + END LOOP; +END $MIG$; + +SET session_replication_role = origin; + +-- --------------------------------------------------------------------- +-- Step 4: Drop old per-instance tables from the merchant schema, in +-- reverse FK-dependency order so CASCADE is not needed. +-- --------------------------------------------------------------------- +#include "merchant-0036-drop.sql.fragment" + +COMMIT; diff --git a/src/backenddb/sql-schema/meson.build b/src/backenddb/sql-schema/meson.build @@ -1,86 +1,138 @@ sqldir = get_option('datadir') / 'taler-merchant' / 'sql' -# Input files for the amalgamated procedures.sql. +# Input files for the amalgamated global_procedures.sql. # The order matters. -sql_procedures = [ - '../pg_insert_deposit_to_transfer.sql', - '../pg_insert_product.sql', - '../pg_insert_issued_token.sql', - '../pg_insert_spent_token.sql', - '../pg_insert_transfer_details.sql', - '../pg_update_product.sql', - '../pg_solve_mfa_challenge.sql', - '../pg_account_kyc_set_status.sql', - '../pg_account_kyc_set_failed.sql', - '../pg_statistics_helpers.sql', - '../pg_do_handle_inventory_changes.sql', - '../pg_do_handle_category_changes.sql', - '../pg_update_product_group.sql', - '../pg_update_money_pot.sql', - '../pg_increment_money_pots.sql', - '../pg_account_kyc_get_status.sql', - '../pg_insert_transfer.sql', - '../pg_insert_deposit_confirmation.sql', +sql_global_procedures = [ + '../pg_interval_to_start.sql', '../pg_base32_crockford.sql', '../pg_uri_escape.sql', - '../pg_merchant_kyc_trigger.sql', - '../pg_merchant_send_kyc_notification.sql', - '../pg_activate_account.sql', - '../pg_inactivate_account.sql', + '../pg_replace_placeholder.sql', + '../check_report.sql', + '../create_tables.sql', + '../pg_fixup_instance_schema.sql', + '../account_kyc_get_outdated.sql', + '../select_wirewatch_accounts.sql', + '../select_open_transfers.sql', + '../select_all_donau_instances.sql', + '../solve_mfa_challenge.sql', + '../lookup_pending_deposits.sql', + '../lookup_reports_pending.sql', + '../pg_select_accounts.sql', + '../pg_create_instance_schema.sql', + '../pg_create_instance_trigger.sql', + '../gc.sql', ] -sqlfiles = [ - 'drop.sql', - 'merchant-0001.sql', - 'merchant-0002.sql', - 'merchant-0003.sql', - 'merchant-0004.sql', - 'merchant-0005.sql', - 'merchant-0006.sql', - 'merchant-0007.sql', - 'merchant-0008.sql', - 'merchant-0009.sql', - 'merchant-0010.sql', - 'merchant-0011.sql', - 'merchant-0012.sql', - 'merchant-0013.sql', - 'merchant-0014.sql', - 'merchant-0015.sql', - 'merchant-0016.sql', - 'merchant-0017.sql', - 'merchant-0018.sql', - 'merchant-0019.sql', - 'merchant-0020.sql', - 'merchant-0021.sql', - 'merchant-0022.sql', - 'merchant-0023.sql', - 'merchant-0024.sql', - 'merchant-0025.sql', - 'merchant-0026.sql', - 'merchant-0027.sql', - 'merchant-0028.sql', - 'merchant-0029.sql', - 'merchant-0030.sql', - 'merchant-0031.sql', - 'merchant-0032.sql', - 'merchant-0033.sql', - 'merchant-0034.sql', - 'merchant-0035.sql', - 'versioning.sql', -] +gprocedures_sql = custom_target('sql_global_procedures', + input: sql_global_procedures, + output: 'global_procedures.sql', + capture: true, + command: ['./gen-procedures.sh', 'merchant', '@INPUT@'], + install: true, + install_dir: sqldir) +sql_instance_procedures = [ + '../pg_statistics_helpers.sql', + '../pg_do_handle_inventory_changes.sql', + '../pg_do_handle_category_changes.sql', + '../delete_unit.sql', + '../insert_unit.sql', + '../update_unit.sql', + '../lookup_statistics_amount_by_interval.sql', + '../lookup_statistics_counter_by_interval.sql', + '../insert_deposit_to_transfer.sql', + '../insert_product.sql', + '../insert_issued_token.sql', + '../insert_spent_token.sql', + '../insert_transfer_details.sql', + '../update_product.sql', + '../account_kyc_set_status.sql', + '../account_kyc_set_failed.sql', + '../update_product_group.sql', + '../update_money_pot.sql', + '../increment_money_pots.sql', + '../account_kyc_get_status.sql', + '../insert_transfer.sql', + '../insert_unclaim_signature.sql', + '../insert_deposit_confirmation.sql', + '../activate_account.sql', + '../inactivate_account.sql', + '../expire_locks.sql', + '../pg_merchant_send_kyc_notification.sql', + '../pg_merchant_kyc_trigger.sql', +] -sql_procs = custom_target('sql_procedures', - input: sql_procedures, - output: 'procedures.sql', +iprocedures_sql = custom_target('instance_procedures', + input: sql_instance_procedures, + output: 'instance_procedures.sql', capture: true, - command: ['./gen-procedures.sh', '@INPUT@'], + command: ['./gen-procedures.sh', 'merchant_instances', '@INPUT@'], install: true, install_dir: sqldir) -install_data(sources: sqlfiles, install_dir: sqldir) +generated_sql = [ + ['drop.sql'], + ['versioning.sql'], + ['merchant-0001.sql'], + ['merchant-0002.sql'], + ['merchant-0003.sql'], + ['merchant-0004.sql'], + ['merchant-0005.sql'], + ['merchant-0006.sql'], + ['merchant-0007.sql'], + ['merchant-0008.sql'], + ['merchant-0009.sql'], + ['merchant-0010.sql'], + ['merchant-0011.sql'], + ['merchant-0012.sql'], + ['merchant-0013.sql'], + ['merchant-0014.sql'], + ['merchant-0015.sql'], + ['merchant-0016.sql'], + ['merchant-0017.sql'], + ['merchant-0018.sql'], + ['merchant-0019.sql'], + ['merchant-0020.sql'], + ['merchant-0021.sql'], + ['merchant-0022.sql'], + ['merchant-0023.sql'], + ['merchant-0024.sql'], + ['merchant-0025.sql'], + ['merchant-0026.sql'], + ['merchant-0027.sql'], + ['merchant-0028.sql'], + ['merchant-0029.sql'], + ['merchant-0030.sql'], + ['merchant-0031.sql'], + ['merchant-0032.sql'], + ['merchant-0033.sql'], + ['merchant-0034.sql'], + ['merchant-0035.sql'], +] + +foreach g : generated_sql + custom_target( + 'gen-merchantdb-' + g[0], + input: g[0], + output: g[0], + capture: true, + command: ['./amalgamate-sql.sh', '@INPUT@'], + install: true, + install_dir: sqldir, + ) +endforeach + -# This makes meson copy the files into the build directory for testing -#foreach f : sqlfiles -# configure_file(input: f, output: f, copy: true, install_mode: 'rw-r--r--') -#endforeach +custom_target( + 'gen-merchantdb-merchant_0036.sql', + input: ['merchant-0036.sql.in', + 'merchant-0036-init.sql.fragment', + 'merchant-0036-copy.sql.fragment', + 'merchant-0036-drop.sql.fragment', + 'merchant-0036-setval.sql.fragment'], + output: ['merchant-0036.sql'], + capture: true, + command: ['./preprocess-sql.sh', '@INPUT@'], + install: true, + install_dir: sqldir, + ) +\ No newline at end of file diff --git a/src/backenddb/sql-schema/preprocess-sql.sh b/src/backenddb/sql-schema/preprocess-sql.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +# 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/> + +set -eu + +if [ $# -lt 1 ]; then + echo "Usage: $0 SQLFILES..." >&2 + exit 1 +fi + +cat <<'EOF' +-- +-- This file is part of TALER +-- Copyright (C) 2014--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/> +-- +EOF + +echo "-- generated by" $(basename $0) +for n in $@ +do + echo "-- generated from" $(basename $n) +done +echo "--" + +# Pre-process with cpp and remove SQL comments and empty lines +gcc -I $(dirname $1) -E -P -undef - < $1 2>/tmp/null | sed -e "s/--.*//" | awk 'NF' diff --git a/src/backenddb/start.c b/src/backenddb/start.c @@ -105,7 +105,8 @@ TALER_MERCHANTDB_rollback ( enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_commit (struct TALER_MERCHANTDB_PostgresContext *pg) +TALER_MERCHANTDB_commit ( + struct TALER_MERCHANTDB_PostgresContext *pg) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_end diff --git a/src/backenddb/store_wire_fee_by_exchange.c b/src/backenddb/store_wire_fee_by_exchange.c @@ -19,21 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/store_wire_fee_by_exchange.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_store_wire_fee_by_exchange (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MasterPublicKeyP *master_pub, - const struct GNUNET_HashCode *h_wire_method, - const struct TALER_WireFeeSet *fees, - struct GNUNET_TIME_Timestamp start_date, - struct GNUNET_TIME_Timestamp end_date, - const struct TALER_MasterSignatureP *master_sig) +TALER_MERCHANTDB_store_wire_fee_by_exchange ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MasterPublicKeyP *master_pub, + const struct GNUNET_HashCode *h_wire_method, + const struct TALER_WireFeeSet *fees, + struct GNUNET_TIME_Timestamp start_date, + struct GNUNET_TIME_Timestamp end_date, + const struct TALER_MasterSignatureP *master_sig) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (master_pub), @@ -51,7 +50,7 @@ TALER_MERCHANTDB_store_wire_fee_by_exchange (struct TALER_MERCHANTDB_PostgresCon /* no preflight check here, run in its own transaction by the caller */ PREPARE (pg, "insert_wire_fee", - "INSERT INTO merchant_exchange_wire_fees" + "INSERT INTO merchant.merchant_exchange_wire_fees" "(master_pub" ",h_wire_method" ",wire_fee" diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -67,6 +67,7 @@ #include "merchant-database/lookup_order_by_fulfillment.h" #include "merchant-database/lookup_order_status.h" #include "merchant-database/lookup_orders.h" +#include "merchant-database/lookup_all_webhooks.h" #include "merchant-database/lookup_pending_webhooks.h" #include "merchant-database/lookup_product.h" #include "merchant-database/lookup_products.h" @@ -86,6 +87,7 @@ #include "merchant-database/mark_contract_paid.h" #include "merchant-database/mark_order_wired.h" #include "merchant-database/refund_coin.h" +#include "merchant-database/set_instance.h" #include "merchant-database/store_wire_fee_by_exchange.h" #include "merchant-database/unlock_inventory.h" #include "merchant-database/update_contract_terms.h" @@ -138,6 +140,46 @@ static struct TALER_MERCHANTDB_PostgresContext *pg; return 1; \ ) +/** + * Activate the per-instance schema for @a iid before invoking any + * per-instance MERCHANTDB function. Returns 1 from the enclosing + * test helper if routing fails for an unexpected reason. + * + * If @a iid does not resolve to an existing instance, set_instance + * returns NO_RESULTS. In that case the underlying per-instance call + * could never have run, so we treat it as the test outcome: when the + * caller expected NO_RESULTS, return 0 (pass); otherwise return 1. + * + * @param iid C-string instance identifier + * @param expected expected GNUNET_DB_QueryStatus from the wrapped call + */ +#define TEST_SET_INSTANCE(iid, expected) \ + do { \ + enum GNUNET_DB_QueryStatus _si_qs; \ + _si_qs = TALER_MERCHANTDB_set_instance (pg, (iid)); \ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == _si_qs) \ + { \ + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == (expected)) \ + return 0; \ + GNUNET_break (0); \ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ + "set_instance(%s): instance missing, " \ + "expected qs=%d\n", \ + (iid), \ + (int) (expected)); \ + return 1; \ + } \ + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != _si_qs) \ + { \ + GNUNET_break (0); \ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, \ + "set_instance(%s) failed (qs=%d)\n", \ + (iid), \ + (int) _si_qs); \ + return 1; \ + } \ + } while (0) + /* ********** Instances ********** */ @@ -475,6 +517,7 @@ static int test_delete_instance_private_key (const struct InstanceData *instance, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_delete_instance_private_key ( pg, @@ -495,6 +538,7 @@ static int test_purge_instance (const struct InstanceData *instance, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_purge_instance (pg, instance->instance.id), @@ -516,6 +560,7 @@ test_insert_account (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_account (pg, account), @@ -536,6 +581,7 @@ test_inactivate_account (const struct InstanceData *instance, const struct TALER_MERCHANTDB_AccountDetails *account, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_inactivate_account (pg, instance->instance.id, @@ -901,11 +947,19 @@ test_insert_product (const struct InstanceData *instance, ssize_t expected_no_cat) { bool conflict; - bool no_instance; ssize_t no_cat; bool no_group; bool no_pot; + if (expect_no_instance) + { + TEST_COND_RET_ON_FAIL ( + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == + TALER_MERCHANTDB_set_instance (pg, instance->instance.id), + "Expected missing instance, but set_instance succeeded\n"); + return 0; + } + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_product (pg, instance->instance.id, @@ -913,7 +967,6 @@ test_insert_product (const struct InstanceData *instance, &product->product, num_cats, cats, - &no_instance, &conflict, &no_cat, &no_group, @@ -921,8 +974,6 @@ test_insert_product (const struct InstanceData *instance, "Insert product failed\n"); if (expected_result > 0) { - TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance, - "No instance wrong"); TEST_COND_RET_ON_FAIL (conflict == expect_conflict, "Conflict wrong"); TEST_COND_RET_ON_FAIL (no_cat == expected_no_cat, @@ -954,7 +1005,6 @@ test_update_product (const struct InstanceData *instance, ssize_t expected_no_cat) { - bool no_instance; ssize_t no_cat; bool no_product; bool lost_reduced; @@ -963,6 +1013,15 @@ test_update_product (const struct InstanceData *instance, bool no_group; bool no_pot; + if (expect_no_instance) + { + TEST_COND_RET_ON_FAIL ( + GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == + TALER_MERCHANTDB_set_instance (pg, instance->instance.id), + "Expected missing instance, but set_instance succeeded\n"); + return 0; + } + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL ( expected_result == TALER_MERCHANTDB_update_product (pg, @@ -971,7 +1030,6 @@ test_update_product (const struct InstanceData *instance, &product->product, num_cats, cats, - &no_instance, &no_cat, &no_product, &lost_reduced, @@ -982,8 +1040,6 @@ test_update_product (const struct InstanceData *instance, "Update product failed\n"); if (expected_result > 0) { - TEST_COND_RET_ON_FAIL (no_instance == expect_no_instance, - "No instance wrong"); TEST_COND_RET_ON_FAIL (no_product == expect_no_product, "No product wrong"); TEST_COND_RET_ON_FAIL (lost_reduced == expect_lost_reduced, @@ -1014,6 +1070,7 @@ test_lookup_product (const struct InstanceData *instance, size_t num_categories = 0; uint64_t *categories = NULL; + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_product (pg, instance->instance.id, product->id, @@ -1120,6 +1177,7 @@ test_lookup_products (const struct InstanceData *instance, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * products_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_products (pg, instance->instance.id, 0, @@ -1167,6 +1225,7 @@ test_delete_product (const struct InstanceData *instance, const struct ProductData *product, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_delete_product (pg, instance->instance.id, @@ -1608,6 +1667,7 @@ test_insert_order (const struct InstanceData *instance, memset (&h_post, 42, sizeof (h_post)); + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_order (pg, instance->instance.id, @@ -1643,6 +1703,7 @@ test_lookup_order (const struct InstanceData *instance, memset (&wh, 42, sizeof (wh)); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_MERCHANTDB_lookup_order (pg, instance->instance.id, @@ -1760,6 +1821,7 @@ test_lookup_orders (const struct InstanceData *instance, memset (results_match, 0, sizeof (bool) * orders_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_orders (pg, instance->instance.id, filter, @@ -1855,6 +1917,9 @@ get_order_serial (const struct InstanceData *instance, .delta = 256 }; + GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (pg, + instance->instance.id)); GNUNET_assert (0 < TALER_MERCHANTDB_lookup_orders (pg, instance->instance.id, &filter, @@ -1879,6 +1944,7 @@ test_delete_order (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_delete_order (pg, instance->instance.id, @@ -1904,6 +1970,7 @@ test_insert_contract_terms (const struct InstanceData *instance, { uint64_t os; + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_contract_terms (pg, instance->instance.id, @@ -1928,6 +1995,7 @@ test_update_contract_terms (const struct InstanceData *instance, const struct OrderData *order, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_update_contract_terms (pg, instance->instance.id, @@ -1952,6 +2020,7 @@ test_lookup_contract_terms (const struct InstanceData *instance, json_t *contract = NULL; uint64_t order_serial; + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_MERCHANTDB_lookup_contract_terms (pg, instance->instance.id, @@ -1993,6 +2062,7 @@ test_delete_contract_terms (const struct InstanceData *instance, struct GNUNET_TIME_Relative legal_expiration, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_delete_contract_terms (pg, instance->instance.id, @@ -2021,6 +2091,7 @@ test_mark_contract_paid (const struct InstanceData *instance, GNUNET_assert (GNUNET_OK == TALER_JSON_contract_hash (order->contract, &h_contract_terms)); + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_mark_contract_paid (pg, instance->instance.id, @@ -2049,6 +2120,7 @@ test_lookup_order_status (const struct InstanceData *instance, struct TALER_PrivateContractHashP h_contract_terms; bool order_paid = false; + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_MERCHANTDB_lookup_order_status (pg, instance->instance.id, @@ -2093,6 +2165,7 @@ test_lookup_order_by_fulfillment (const struct InstanceData *instance, json_string_value (json_object_get (order->contract, "fulfillment_url")); GNUNET_assert (NULL != fulfillment_url); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != TALER_MERCHANTDB_lookup_order_by_fulfillment (pg, instance->instance.id, @@ -2141,6 +2214,7 @@ test_lookup_payment_status (const char *instance_id, uint64_t os; int16_t choice_index; + TEST_SET_INSTANCE (instance_id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); TEST_COND_RET_ON_FAIL (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == TALER_MERCHANTDB_lookup_contract_terms3 (pg, instance_id, @@ -2810,6 +2884,7 @@ test_insert_deposit (const struct InstanceData *instance, TALER_amount_subtract (&awf, &deposit->amount_with_fee, &deposit->deposit_fee)); + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL ( GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == TALER_MERCHANTDB_insert_deposit_confirmation (pg, @@ -2945,6 +3020,7 @@ test_lookup_deposits (const struct InstanceData *instance, memset (results_matching, 0, sizeof (unsigned int) * deposits_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); TEST_COND_RET_ON_FAIL (0 <= TALER_MERCHANTDB_lookup_deposits (pg, instance->instance.id, @@ -3072,6 +3148,7 @@ test_lookup_deposits_contract_and_coin ( memset (results_matching, 0, sizeof (unsigned int) * deposits_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); TEST_COND_RET_ON_FAIL ( GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == TALER_MERCHANTDB_lookup_deposits_by_contract_and_coin ( @@ -4211,6 +4288,7 @@ test_lookup_transfers (const struct InstanceData *instance, memset (results_matching, 0, sizeof (unsigned int) * transfers_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (1 != TALER_MERCHANTDB_lookup_transfers (pg, instance->instance.id, account->payto_uri, @@ -4260,21 +4338,21 @@ test_insert_transfer (const struct InstanceData *instance, const struct TransferData *transfer, enum GNUNET_DB_QueryStatus expected_result) { - bool no_instance; bool no_account; bool conflict; + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == - TALER_MERCHANTDB_insert_transfer (pg, - instance->instance.id, - transfer->exchange_url, - &transfer->wtid, - &transfer->data.total_amount, - account->payto_uri, - transfer->confirmed, - &no_instance, - &no_account, - &conflict), + TALER_MERCHANTDB_insert_transfer ( + pg, + instance->instance.id, + transfer->exchange_url, + &transfer->wtid, + &transfer->data.total_amount, + account->payto_uri, + transfer->confirmed, + &no_account, + &conflict), "Insert transfer failed\n"); return 0; } @@ -4307,9 +4385,12 @@ test_insert_deposit_to_transfer (const struct InstanceData *instance, .execution_time = transfer->data.execution_time, .coin_contribution = deposit->amount_with_fee }; - uint64_t deposit_serial = get_deposit_serial (instance, - order, - deposit); + uint64_t deposit_serial; + + TEST_SET_INSTANCE (instance->instance.id, expected_result); + deposit_serial = get_deposit_serial (instance, + order, + deposit); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_deposit_to_transfer ( @@ -4339,6 +4420,7 @@ test_insert_transfer_details ( const struct TransferData *transfer, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_transfer_details ( pg, @@ -4699,6 +4781,7 @@ test_lookup_refunds (const struct InstanceData *instance, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * refunds_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (1 != TALER_MERCHANTDB_lookup_refunds (pg, instance->instance.id, h_contract_terms, @@ -4908,6 +4991,7 @@ test_lookup_refunds_detailed ( .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * refunds_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_refunds_detailed (pg, instance->instance.id, h_contract_terms, @@ -5018,6 +5102,9 @@ get_refund_serial (const struct InstanceData *instance, .serial = 0 }; + GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (pg, + instance->instance.id)); GNUNET_assert (0 < TALER_MERCHANTDB_lookup_refunds_detailed (pg, instance->instance.id, h_contract_terms, @@ -5899,6 +5986,7 @@ test_insert_template (const struct InstanceData *instance, const struct TemplateData *template, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_template (pg, instance->instance.id, @@ -5923,6 +6011,7 @@ test_update_template (const struct InstanceData *instance, const struct TemplateData *template, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_update_template (pg, instance->instance.id, @@ -5948,6 +6037,7 @@ test_lookup_template (const struct InstanceData *instance, = &template->template; struct TALER_MERCHANTDB_TemplateDetails lookup_result; + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_template (pg, instance->instance.id, template->id, @@ -6050,6 +6140,7 @@ test_lookup_templates (const struct InstanceData *instance, memset (results_matching, 0, sizeof (unsigned int) * templates_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_templates (pg, instance->instance.id, &lookup_templates_cb, @@ -6091,6 +6182,7 @@ test_delete_template (const struct InstanceData *instance, const struct TemplateData *template, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_delete_template (pg, instance->instance.id, @@ -6346,6 +6438,7 @@ test_insert_webhook (const struct InstanceData *instance, const struct WebhookData *webhook, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_webhook (pg, instance->instance.id, @@ -6369,6 +6462,7 @@ test_update_webhook (const struct InstanceData *instance, const struct WebhookData *webhook, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_update_webhook (pg, instance->instance.id, @@ -6393,6 +6487,7 @@ test_lookup_webhook (const struct InstanceData *instance, const struct TALER_MERCHANTDB_WebhookDetails *to_cmp = &webhook->webhook; struct TALER_MERCHANTDB_WebhookDetails lookup_result; + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_webhook (pg, instance->instance.id, webhook->id, @@ -6491,6 +6586,7 @@ test_lookup_webhooks (const struct InstanceData *instance, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * webhooks_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_webhooks (pg, instance->instance.id, &lookup_webhooks_cb, @@ -6576,6 +6672,7 @@ test_lookup_webhook_by_event (const struct InstanceData *instance, .results_length = 0 }; memset (results_matching, 0, sizeof (unsigned int) * webhooks_length); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_webhook_by_event (pg, instance->instance.id, webhooks->webhook.event_type, @@ -6619,6 +6716,7 @@ test_delete_webhook (const struct InstanceData *instance, const struct WebhookData *webhook, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_delete_webhook (pg, instance->instance.id, @@ -6854,6 +6952,7 @@ test_insert_pending_webhook (const struct InstanceData *instance, enum GNUNET_DB_QueryStatus expected_result) { + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_insert_pending_webhook (pg, instance->instance.id, @@ -6887,6 +6986,7 @@ test_update_pending_webhook (const struct InstanceData *instance, pwebhook->pwebhook.next_attempt = GNUNET_TIME_relative_to_absolute ( GNUNET_TIME_UNIT_HOURS); pwebhook->pwebhook.retries++; + TEST_SET_INSTANCE (instance->instance.id, expected_result); TEST_COND_RET_ON_FAIL (expected_result == TALER_MERCHANTDB_update_pending_webhook (pg, pwebhook-> @@ -6973,6 +7073,9 @@ get_pending_serial (const struct InstanceData *instance, .webhook_pending_serial = 0 }; + GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == + TALER_MERCHANTDB_set_instance (pg, + instance->instance.id)); GNUNET_assert (0 < TALER_MERCHANTDB_lookup_all_webhooks (pg, instance->instance.id, @@ -7174,6 +7277,7 @@ test_lookup_all_webhooks (const struct InstanceData *instance, memset (results_matching, 0, sizeof (results_matching)); + TEST_SET_INSTANCE (instance->instance.id, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT); if (0 > TALER_MERCHANTDB_lookup_all_webhooks (pg, instance->instance.id, min_row, @@ -7236,6 +7340,14 @@ struct TestPendingWebhooks_Closure struct InstanceData instance; /** + * Webhooks that the pending webhooks reference via foreign key. + * Each per-instance schema has its own webhook_serial sequence + * starting at 1, so we must populate webhooks before inserting + * pending webhooks. + */ + struct WebhookData webhooks[4]; + + /** * The array of pending webhooks. */ struct PendingWebhookData pwebhooks[2]; @@ -7254,6 +7366,12 @@ pre_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) make_instance ("test_inst_pending_webhooks", &cls->instance); + /* Webhooks the pending webhooks reference (serials 1..4) */ + make_webhook ("test_pwh_wb_0", &cls->webhooks[0]); + make_webhook ("test_pwh_wb_1", &cls->webhooks[1]); + make_webhook ("test_pwh_wb_2", &cls->webhooks[2]); + make_webhook ("test_pwh_wb_3", &cls->webhooks[3]); + /* Webhooks */ make_pending_webhook (1, &cls->pwebhooks[0]); @@ -7300,6 +7418,13 @@ run_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) TEST_RET_ON_FAIL (test_insert_instance (&cls->instance, GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + /* Insert the referenced webhooks so pending webhook FKs resolve. */ + for (unsigned int i = 0; i < 4; i++) + TEST_RET_ON_FAIL ( + test_insert_webhook (&cls->instance, + &cls->webhooks[i], + GNUNET_DB_STATUS_SUCCESS_ONE_RESULT)); + /* Test inserting a pending webhook */ TEST_RET_ON_FAIL (test_insert_pending_webhook (&cls->instance, &cls->pwebhooks[0], diff --git a/src/backenddb/unlock_inventory.c b/src/backenddb/unlock_inventory.c @@ -19,28 +19,28 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/unlock_inventory.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_unlock_inventory (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct GNUNET_Uuid *uuid) +TALER_MERCHANTDB_unlock_inventory ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct GNUNET_Uuid *uuid) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (uuid), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "unlock_inventory", - "DELETE" - " FROM merchant_inventory_locks" - " WHERE lock_uuid=$1"); + TMH_PQ_prepare_anon (pg, + "DELETE" + " FROM merchant_inventory_locks" + " WHERE lock_uuid=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "unlock_inventory", + "", params); } diff --git a/src/backenddb/update_account.c b/src/backenddb/update_account.c @@ -19,23 +19,21 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_account.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_account (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - const struct TALER_MerchantWireHashP *h_wire, - const char *extra_wire_subject_metadata, - const char *credit_facade_url, - const json_t *credit_facade_credentials) +TALER_MERCHANTDB_update_account ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + const struct TALER_MerchantWireHashP *h_wire, + const char *extra_wire_subject_metadata, + const char *credit_facade_url, + const json_t *credit_facade_credentials) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (id), GNUNET_PQ_query_param_auto_from_type (h_wire), NULL == credit_facade_url ? GNUNET_PQ_query_param_null () @@ -49,20 +47,18 @@ TALER_MERCHANTDB_update_account (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_account", - "UPDATE merchant_accounts SET" - " credit_facade_url=$3" - ",credit_facade_credentials=" - " COALESCE($4::TEXT::JSONB, credit_facade_credentials)" - ",extra_wire_subject_metadata=$5" - " WHERE h_wire=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1);"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_accounts SET" + " credit_facade_url=$2" + ",credit_facade_credentials=" + " COALESCE($3::TEXT::JSONB, credit_facade_credentials)" + ",extra_wire_subject_metadata=$4" + " WHERE h_wire=$1;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_account", + "", params); } diff --git a/src/backenddb/update_category.c b/src/backenddb/update_category.c @@ -19,40 +19,36 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_category.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_category (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t category_id, - const char *category_name, - const json_t *category_name_i18n) +TALER_MERCHANTDB_update_category ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&category_id), GNUNET_PQ_query_param_string (category_name), TALER_PQ_query_param_json (category_name_i18n), GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_category", - "UPDATE merchant_categories SET" - " category_name=$3" - ",category_name_i18n=$4::TEXT::JSONB" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND category_serial=$2"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_categories SET" + " category_name=$2" + ",category_name_i18n=$3::TEXT::JSONB" + " WHERE category_serial=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_category", + "", params); } diff --git a/src/backenddb/update_contract_session.c b/src/backenddb/update_contract_session.c @@ -19,23 +19,21 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_contract_session.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_contract_session (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_PrivateContractHashP *h_contract_terms, - const char *session_id, - char **fulfillment_url, - bool *refunded) +TALER_MERCHANTDB_update_contract_session ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const struct TALER_PrivateContractHashP *h_contract_terms, + const char *session_id, + char **fulfillment_url, + bool *refunded) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_auto_from_type (h_contract_terms), GNUNET_PQ_query_param_string (session_id), GNUNET_PQ_query_param_end @@ -52,6 +50,9 @@ TALER_MERCHANTDB_update_contract_session (struct TALER_MERCHANTDB_PostgresContex /* Session ID must always be given by the caller. */ GNUNET_assert (NULL != session_id); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); /* no preflight check here, run in transaction by caller! */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, @@ -59,24 +60,19 @@ TALER_MERCHANTDB_update_contract_session (struct TALER_MERCHANTDB_PostgresContex GNUNET_h2s (&h_contract_terms->hash), instance_id, session_id); - PREPARE (pg, - "update_contract_session", - "UPDATE merchant_contract_terms mct SET" - " session_id=$3" - " WHERE h_contract_terms=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " RETURNING" - " contract_terms->>'fulfillment_url' AS fulfillment_url" - " ,EXISTS (SELECT 1" - " FROM merchant_refunds mr" - " WHERE order_serial=mct.order_serial" - " ) AS refunded"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_contract_terms mct SET" + " session_id=$2" + " WHERE h_contract_terms=$1" + " RETURNING" + " contract_terms->>'fulfillment_url' AS fulfillment_url" + " ,EXISTS (SELECT 1" + " FROM merchant_refunds mr" + " WHERE order_serial=mct.order_serial" + " ) AS refunded"); *fulfillment_url = NULL; return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_contract_session", + "", params, rs); } diff --git a/src/backenddb/update_contract_terms.c b/src/backenddb/update_contract_terms.c @@ -19,17 +19,17 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_contract_terms.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_contract_terms (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *order_id, - json_t *contract_terms) +TALER_MERCHANTDB_update_contract_terms ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + json_t *contract_terms) { struct GNUNET_TIME_Timestamp pay_deadline; struct GNUNET_TIME_Timestamp refund_deadline; @@ -71,10 +71,12 @@ TALER_MERCHANTDB_update_contract_terms (struct TALER_MERCHANTDB_PostgresContext } } + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (order_id), TALER_PQ_query_param_json (contract_terms), GNUNET_PQ_query_param_auto_from_type (&h_contract_terms), @@ -85,21 +87,17 @@ TALER_MERCHANTDB_update_contract_terms (struct TALER_MERCHANTDB_PostgresContext : GNUNET_PQ_query_param_string (fulfillment_url), GNUNET_PQ_query_param_end }; - PREPARE (pg, - "update_contract_terms", - "UPDATE merchant_contract_terms SET" - " contract_terms=$3::TEXT::JSONB" - ",h_contract_terms=$4" - ",pay_deadline=$5" - ",refund_deadline=$6" - ",fulfillment_url=$7" - " WHERE order_id=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_contract_terms SET" + " contract_terms=$2::TEXT::JSONB" + ",h_contract_terms=$3" + ",pay_deadline=$4" + ",refund_deadline=$5" + ",fulfillment_url=$6" + " WHERE order_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_contract_terms", + "", params); } } diff --git a/src/backenddb/update_deposit_confirmation_status.c b/src/backenddb/update_deposit_confirmation_status.c @@ -19,21 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_deposit_confirmation_status.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_deposit_confirmation_status (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t deposit_serial, - bool retry_needed, - struct GNUNET_TIME_Timestamp retry_time, - uint32_t last_http_status, - enum TALER_ErrorCode last_ec, - const char *last_detail) +TALER_MERCHANTDB_update_deposit_confirmation_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t deposit_serial, + bool retry_needed, + struct GNUNET_TIME_Timestamp retry_time, + uint32_t last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_detail) { uint32_t ec32 = (uint32_t) last_ec; struct GNUNET_PQ_QueryParam params[] = { @@ -48,18 +47,18 @@ TALER_MERCHANTDB_update_deposit_confirmation_status (struct TALER_MERCHANTDB_Pos GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "update_deposit_confirmation_status", - "UPDATE merchant_deposits SET" - " settlement_retry_needed=$2" - ",settlement_retry_time=$3" - ",settlement_last_http_status=$4" - ",settlement_last_ec=$5" - ",settlement_last_detail=$6" - " WHERE deposit_serial=$1;"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_deposits SET" + " settlement_retry_needed=$2" + ",settlement_retry_time=$3" + ",settlement_last_http_status=$4" + ",settlement_last_ec=$5" + ",settlement_last_detail=$6" + " WHERE deposit_serial=$1;"); return GNUNET_PQ_eval_prepared_non_select ( pg->conn, - "update_deposit_confirmation_status", + "", params); } diff --git a/src/backenddb/update_donau_instance.c b/src/backenddb/update_donau_instance.c @@ -20,8 +20,6 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "donau/donau_service.h" #include "merchant-database/update_donau_instance.h" @@ -29,15 +27,15 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_donau_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *donau_url, - const struct DONAU_Charity *charity, - uint64_t charity_id) +TALER_MERCHANTDB_update_donau_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *donau_url, + const struct DONAU_Charity *charity, + uint64_t charity_id) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (donau_url), GNUNET_PQ_query_param_string (charity->name), - GNUNET_PQ_query_param_auto_from_type (&charity->charity_pub), GNUNET_PQ_query_param_uint64 (&charity_id), TALER_PQ_query_param_amount_with_currency (pg->conn, &charity->max_per_year), @@ -47,23 +45,24 @@ TALER_MERCHANTDB_update_donau_instance (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_static_assert (sizeof (charity->charity_pub) == + sizeof (pg->current_merchant_pub)); + GNUNET_assert (0 == + memcmp (&charity->charity_pub, + &pg->current_merchant_pub, + sizeof (charity->charity_pub))); check_connection (pg); - PREPARE (pg, - "update_existing_donau_instance", - "UPDATE merchant_donau_instances SET" - " charity_name = $2," - " charity_max_per_year = $5," - " charity_receipts_to_date = $6," - " current_year = $7" - " WHERE charity_id = $4" - " AND merchant_instance_serial " - " = (SELECT merchant_serial" - " FROM merchant_instances mi" - " WHERE mi.merchant_pub = $3)" - " AND donau_url = $1;"); - + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_donau_instances SET" + " charity_name = $2," + " charity_max_per_year = $4," + " charity_receipts_to_date = $5," + " current_year = $6" + " WHERE charity_id = $3" + " AND donau_url = $1;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_existing_donau_instance", + "", params); } diff --git a/src/backenddb/update_donau_instance_receipts_amount.c b/src/backenddb/update_donau_instance_receipts_amount.c @@ -21,17 +21,16 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_donau_instance_receipts_amount.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_donau_instance_receipts_amount (struct TALER_MERCHANTDB_PostgresContext *pg - , - uint64_t *donau_instances_serial, - const struct TALER_Amount *new_amount) +TALER_MERCHANTDB_update_donau_instance_receipts_amount ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t *donau_instances_serial, + const struct TALER_Amount *new_amount) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (donau_instances_serial), @@ -40,15 +39,12 @@ TALER_MERCHANTDB_update_donau_instance_receipts_amount (struct TALER_MERCHANTDB_ GNUNET_PQ_query_param_end }; - check_connection (pg); - - PREPARE (pg, - "update_donau_instance_receipts", - "UPDATE merchant_donau_instances " - "SET charity_receipts_to_date = $2 " - "WHERE donau_instances_serial = $1;"); - + GNUNET_assert (NULL != pg->current_merchant_id); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_donau_instances " + "SET charity_receipts_to_date = $2 " + "WHERE donau_instances_serial = $1;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_donau_instance_receipts", + "", params); } diff --git a/src/backenddb/update_instance.c b/src/backenddb/update_instance.c @@ -20,16 +20,15 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_instance.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_instance (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct TALER_MERCHANTDB_InstanceSettings *is) +TALER_MERCHANTDB_update_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct TALER_MERCHANTDB_InstanceSettings *is) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (is->id), @@ -66,7 +65,7 @@ TALER_MERCHANTDB_update_instance (struct TALER_MERCHANTDB_PostgresContext *pg, check_connection (pg); PREPARE (pg, "update_instance", - "UPDATE merchant_instances SET" + "UPDATE merchant.merchant_instances SET" " merchant_name=$2" ",address=$3::TEXT::JSONB" ",jurisdiction=$4::TEXT::JSONB" diff --git a/src/backenddb/update_instance_auth.c b/src/backenddb/update_instance_auth.c @@ -19,16 +19,16 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_instance_auth.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_instance_auth (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *merchant_id, - const struct TALER_MERCHANTDB_InstanceAuthSettings *is) +TALER_MERCHANTDB_update_instance_auth ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *merchant_id, + const struct TALER_MERCHANTDB_InstanceAuthSettings *is) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (merchant_id), @@ -40,7 +40,7 @@ TALER_MERCHANTDB_update_instance_auth (struct TALER_MERCHANTDB_PostgresContext * check_connection (pg); PREPARE (pg, "update_instance_auth", - "UPDATE merchant_instances SET" + "UPDATE merchant.merchant_instances SET" " auth_hash=$2" ",auth_salt=$3" " WHERE merchant_id=$1"); diff --git a/src/backenddb/update_mfa_challenge.c b/src/backenddb/update_mfa_challenge.c @@ -19,20 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_mfa_challenge.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_mfa_challenge (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t challenge_id, - const char *code, - uint32_t retry_counter, - struct GNUNET_TIME_Absolute expiration_date, - struct GNUNET_TIME_Absolute retransmission_date) +TALER_MERCHANTDB_update_mfa_challenge ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t challenge_id, + const char *code, + uint32_t retry_counter, + struct GNUNET_TIME_Absolute expiration_date, + struct GNUNET_TIME_Absolute retransmission_date) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&challenge_id), @@ -45,7 +44,7 @@ TALER_MERCHANTDB_update_mfa_challenge (struct TALER_MERCHANTDB_PostgresContext * PREPARE (pg, "update_mfa_challenge", - "UPDATE tan_challenges" + "UPDATE merchant.tan_challenges" " SET" " code=$2" " ,retry_counter=$3" diff --git a/src/backenddb/update_money_pot.c b/src/backenddb/update_money_pot.c @@ -19,28 +19,26 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_money_pot.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t money_pot_id, - const char *name, - const char *description, - size_t opt_len, - const struct TALER_Amount *old_pot_totals, - size_t npt_len, - const struct TALER_Amount *new_pot_totals, - bool *conflict_total, - bool *conflict_name) +TALER_MERCHANTDB_update_money_pot ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t money_pot_id, + const char *name, + const char *description, + size_t opt_len, + const struct TALER_Amount *old_pot_totals, + size_t npt_len, + const struct TALER_Amount *new_pot_totals, + bool *conflict_total, + bool *conflict_name) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&money_pot_id), GNUNET_PQ_query_param_string (name), GNUNET_PQ_query_param_string (description), @@ -68,17 +66,19 @@ TALER_MERCHANTDB_update_money_pot (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_money_pot", - "SELECT" - " out_conflict_name AS conflict_name" - ",out_conflict_total AS conflict_total" - ",out_not_found AS not_found" - " FROM merchant_do_update_money_pot" - "($1,$2,$3,$4,$5,$6);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_conflict_name AS conflict_name" + ",out_conflict_total AS conflict_total" + ",out_not_found AS not_found" + " FROM merchant_do_update_money_pot" + "($1,$2,$3,$4,$5);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_money_pot", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/update_money_pot.sql b/src/backenddb/update_money_pot.sql @@ -0,0 +1,63 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_update_money_pot; +CREATE FUNCTION merchant_do_update_money_pot ( + IN in_money_pot_serial INT8, + IN in_name TEXT, + IN in_description TEXT, + IN in_old_totals merchant.taler_amount_currency[], -- can be NULL! + IN in_new_totals merchant.taler_amount_currency[], -- can be NULL! + OUT out_conflict_total BOOL, + OUT out_conflict_name BOOL, + OUT out_not_found BOOL) +LANGUAGE plpgsql +AS $$ +BEGIN + +BEGIN + UPDATE merchant_money_pots SET + money_pot_name=in_name + ,money_pot_description=in_description + ,pot_totals=COALESCE(in_new_totals, pot_totals) + WHERE money_pot_serial=in_money_pot_serial + AND ( (in_old_totals IS NULL) OR (pot_totals=in_old_totals) ); + IF NOT FOUND + THEN + -- Check if pot_total was the problem + PERFORM FROM merchant_money_pots + WHERE money_pot_serial=in_money_pot_serial; + out_conflict_total = FOUND; + out_not_found = NOT FOUND; + out_conflict_name = FALSE; + ELSE + out_conflict_total = FALSE; + out_not_found = FALSE; + out_conflict_name = FALSE; + END IF; + RETURN; +EXCEPTION + -- money_pot_name already used + WHEN unique_violation + THEN + out_conflict_name = TRUE; + out_conflict_total = FALSE; + out_not_found = FALSE; + RETURN; +END; + +END $$; diff --git a/src/backenddb/update_otp.c b/src/backenddb/update_otp.c @@ -19,22 +19,20 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_otp.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_otp (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *otp_id, - const struct TALER_MERCHANTDB_OtpDeviceDetails *td) +TALER_MERCHANTDB_update_otp ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *otp_id, + const struct TALER_MERCHANTDB_OtpDeviceDetails *td) { uint32_t pos32 = (uint32_t) td->otp_algorithm; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (otp_id), GNUNET_PQ_query_param_string (td->otp_description), GNUNET_PQ_query_param_uint32 (&pos32), @@ -45,20 +43,18 @@ TALER_MERCHANTDB_update_otp (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_otp", - "UPDATE merchant_otp_devices SET" - " otp_description=$3" - ",otp_algorithm=$4" - ",otp_ctr=$5" - ",otp_key=COALESCE($6,otp_key)" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND otp_id=$2"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_otp_devices SET" + " otp_description=$2" + ",otp_algorithm=$3" + ",otp_ctr=$4" + ",otp_key=COALESCE($5,otp_key)" + " WHERE otp_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_otp", + "", params); } diff --git a/src/backenddb/update_pending_webhook.c b/src/backenddb/update_pending_webhook.c @@ -19,16 +19,16 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_pending_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_pending_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - uint64_t webhook_pending_serial, - struct GNUNET_TIME_Absolute next_attempt) +TALER_MERCHANTDB_update_pending_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + uint64_t webhook_pending_serial, + struct GNUNET_TIME_Absolute next_attempt) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&webhook_pending_serial), @@ -39,11 +39,10 @@ TALER_MERCHANTDB_update_pending_webhook (struct TALER_MERCHANTDB_PostgresContext check_connection (pg); PREPARE (pg, "update_pending_webhook", - "UPDATE merchant_pending_webhooks SET" + "UPDATE merchant.merchant_pending_webhooks SET" " retries=retries+1" ",next_attempt=$2" " WHERE webhook_pending_serial=$1"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, "update_pending_webhook", params); diff --git a/src/backenddb/update_product.c b/src/backenddb/update_product.c @@ -20,67 +20,62 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_product.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_product (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd, - size_t num_cats, - const uint64_t *cats, - bool *no_instance, - ssize_t *no_cat, - bool *no_product, - bool *lost_reduced, - bool *sold_reduced, - bool *stocked_reduced, - bool *no_group, - bool *no_pot) +TALER_MERCHANTDB_update_product ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + ssize_t *no_cat, + bool *no_product, + bool *lost_reduced, + bool *sold_reduced, + bool *stocked_reduced, + bool *no_group, + bool *no_pot) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), /* $1 */ - GNUNET_PQ_query_param_string (product_id), + GNUNET_PQ_query_param_string (product_id), /* $1 */ GNUNET_PQ_query_param_string (pd->description), - TALER_PQ_query_param_json (pd->description_i18n), /* $4 */ + TALER_PQ_query_param_json (pd->description_i18n), /* $3 */ GNUNET_PQ_query_param_string (pd->unit), - GNUNET_PQ_query_param_string (pd->image), /* $6 */ - TALER_PQ_query_param_json (pd->taxes), /* $7 */ + GNUNET_PQ_query_param_string (pd->image), /* $5 */ + TALER_PQ_query_param_json (pd->taxes), /* $6 */ TALER_PQ_query_param_array_amount_with_currency ( pd->price_array_length, pd->price_array, - pg->conn), /* $8 */ - GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $9 */ - GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $10 */ - GNUNET_PQ_query_param_bool (pd->allow_fractional_quantity), /* $11 */ + pg->conn), /* $7 */ + GNUNET_PQ_query_param_uint64 (&pd->total_stock), /* $8 */ + GNUNET_PQ_query_param_uint32 (&pd->total_stock_frac), /* $9 */ + GNUNET_PQ_query_param_bool (pd->allow_fractional_quantity), /* $10 */ GNUNET_PQ_query_param_uint32 (&pd->fractional_precision_level), GNUNET_PQ_query_param_uint64 (&pd->total_lost), - TALER_PQ_query_param_json (pd->address), /* $14 */ + TALER_PQ_query_param_json (pd->address), /* $13 */ GNUNET_PQ_query_param_timestamp (&pd->next_restock), GNUNET_PQ_query_param_uint32 (&pd->minimum_age), GNUNET_PQ_query_param_array_uint64 (num_cats, cats, - pg->conn), /* $17 */ - GNUNET_PQ_query_param_string (pd->product_name), /* $18 */ + pg->conn), /* $16 */ + GNUNET_PQ_query_param_string (pd->product_name), /* $17 */ (0 == pd->product_group_id) ? GNUNET_PQ_query_param_null () : GNUNET_PQ_query_param_uint64 (&pd->product_group_id), (0 == pd->money_pot_id) ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), /* $20 */ + : GNUNET_PQ_query_param_uint64 (&pd->money_pot_id), /* $19 */ GNUNET_PQ_query_param_bool (pd->price_is_net), GNUNET_PQ_query_param_end }; uint64_t ncat; bool cats_found = true; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("no_instance", - no_instance), GNUNET_PQ_result_spec_bool ("no_product", no_product), GNUNET_PQ_result_spec_bool ("lost_reduced", @@ -108,24 +103,25 @@ TALER_MERCHANTDB_update_product (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_break (0); return GNUNET_DB_STATUS_HARD_ERROR; } + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_product", - "SELECT" - " out_lost_reduced AS lost_reduced" - ",out_sold_reduced AS sold_reduced" - ",out_stocked_reduced AS stocked_reduced" - ",out_no_product AS no_product" - ",out_no_cat AS no_cat" - ",out_no_instance AS no_instance" - ",out_no_group AS no_group" - ",out_no_pot AS no_pot" - " FROM merchant_do_update_product" - "($1,$2,$3,$4::TEXT::JSONB,$5,$6,$7::TEXT::JSONB,$8,$9" - ",$10,$11,$12,$13,$14::TEXT::JSONB,$15,$16,$17,$18" - ",$19,$20,$21);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_lost_reduced AS lost_reduced" + ",out_sold_reduced AS sold_reduced" + ",out_stocked_reduced AS stocked_reduced" + ",out_no_product AS no_product" + ",out_no_cat AS no_cat" + ",out_no_group AS no_group" + ",out_no_pot AS no_pot" + " FROM merchant_do_update_product" + "($1,$2,$3::TEXT::JSONB,$4,$5,$6::TEXT::JSONB,$7,$8" + ",$9,$10,$11,$12,$13::TEXT::JSONB,$14,$15,$16,$17" + ",$18,$19,$20);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_product", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/update_product.sql b/src/backenddb/update_product.sql @@ -0,0 +1,179 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024, 2025 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/> +-- + + +DROP FUNCTION IF EXISTS merchant_do_update_product; +CREATE FUNCTION merchant_do_update_product ( + IN in_product_id TEXT, + IN in_description TEXT, + IN in_description_i18n JSONB, -- $3 + IN in_unit TEXT, + IN in_image TEXT, + IN in_taxes JSONB, -- $6 + IN ina_price_list merchant.taler_amount_currency[], + IN in_total_stock INT8, + IN in_total_stock_frac INT4, + IN in_allow_fractional_quantity BOOL, + IN in_fractional_precision_level INT4, + IN in_total_lost INT8, -- NOTE: not in insert_product + IN in_address JSONB, -- $13 + IN in_next_restock INT8, + IN in_minimum_age INT4, + IN ina_categories INT8[], -- $16 + IN in_product_name TEXT, + IN in_product_group_id INT8, -- NULL for default + IN in_money_pot_id INT8, -- NULL for none + IN in_price_is_net BOOL, -- $20 + OUT out_no_product BOOL, + OUT out_lost_reduced BOOL, + OUT out_sold_reduced BOOL, + OUT out_stocked_reduced BOOL, + OUT out_no_cat INT8, + OUT out_no_group BOOL, + OUT out_no_pot BOOL) +LANGUAGE plpgsql +AS $$ +DECLARE + my_product_serial INT8; + i INT8; + ini_cat INT8; + rec RECORD; +BEGIN + +out_no_group = FALSE; +out_no_pot = FALSE; +out_no_product=FALSE; +out_lost_reduced=FALSE; +out_sold_reduced=FALSE; -- We currently don't allow updating 'sold', hence always FALSE +out_stocked_reduced=FALSE; +out_no_cat=NULL; + +IF in_product_group_id IS NOT NULL +THEN + PERFORM FROM merchant_product_groups + WHERE product_group_serial=in_product_group_id; + IF NOT FOUND + THEN + out_no_group=TRUE; + RETURN; + END IF; +END IF; + +IF in_money_pot_id IS NOT NULL +THEN + PERFORM FROM merchant_money_pots + WHERE money_pot_serial=in_money_pot_id; + IF NOT FOUND + THEN + out_no_pot=TRUE; + RETURN; + END IF; +END IF; + +-- Check existing entry satisfies constraints +SELECT total_stock + ,total_stock_frac + ,total_lost + ,allow_fractional_quantity + ,product_serial + INTO rec + FROM merchant_inventory + WHERE product_id=in_product_id; + +IF NOT FOUND +THEN + out_no_product=TRUE; + RETURN; +END IF; + +my_product_serial = rec.product_serial; + +IF rec.total_stock > in_total_stock +THEN + out_stocked_reduced=TRUE; + RETURN; +END IF; + +IF rec.total_lost > in_total_lost +THEN + out_lost_reduced=TRUE; + RETURN; +END IF; +IF rec.allow_fractional_quantity + AND (NOT in_allow_fractional_quantity) +THEN + DELETE + FROM merchant_inventory_locks + WHERE product_serial = my_product_serial + AND total_locked_frac <> 0; +END IF; + +-- Remove old categories +DELETE FROM merchant_product_categories + WHERE product_serial=my_product_serial; + +-- Add new categories +FOR i IN 1..COALESCE(array_length(ina_categories,1),0) +LOOP + ini_cat=ina_categories[i]; + + INSERT INTO merchant_product_categories + (product_serial + ,category_serial) + VALUES + (my_product_serial + ,ini_cat) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_no_cat=i; + RETURN; + END IF; +END LOOP; + +UPDATE merchant_inventory SET + description=in_description + ,description_i18n=in_description_i18n + ,product_name=in_product_name + ,unit=in_unit + ,image=in_image + ,image_hash=CASE + WHEN (in_image IS NULL) OR (in_image = '') + THEN NULL + ELSE encode(public.digest(convert_to(in_image, 'UTF8'), + 'sha256'), + 'hex') + END + ,taxes=in_taxes + ,price_array=ina_price_list + ,total_stock=in_total_stock + ,total_stock_frac=in_total_stock_frac + ,allow_fractional_quantity=in_allow_fractional_quantity + ,fractional_precision_level=in_fractional_precision_level + ,total_lost=in_total_lost + ,address=in_address + ,next_restock=in_next_restock + ,minimum_age=in_minimum_age + ,product_group_serial=in_product_group_id + ,money_pot_serial=in_money_pot_id + ,price_is_net=in_price_is_net + WHERE product_serial=my_product_serial; -- could also match on product_id + +ASSERT FOUND,'SELECTED it earlier, should UPDATE it now'; + +-- Success! +END $$; diff --git a/src/backenddb/update_product_group.c b/src/backenddb/update_product_group.c @@ -19,8 +19,6 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_product_group.h" #include "helper.h" @@ -36,7 +34,6 @@ TALER_MERCHANTDB_update_product_group ( bool *conflict) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&product_group_id), GNUNET_PQ_query_param_string (name), GNUNET_PQ_query_param_string (description), @@ -52,17 +49,19 @@ TALER_MERCHANTDB_update_product_group ( }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_product_group", - "SELECT" - " out_conflict AS conflict" - ",out_not_found AS not_found" - " FROM merchant_do_update_product_group" - "($1,$2,$3,$4);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_conflict AS conflict" + ",out_not_found AS not_found" + " FROM merchant_do_update_product_group" + "($1,$2,$3);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_product_group", + "", params, rs); if (qs <= 0) diff --git a/src/backenddb/update_product_group.sql b/src/backenddb/update_product_group.sql @@ -0,0 +1,44 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_update_product_group; +CREATE FUNCTION merchant_do_update_product_group ( + IN in_product_group_serial INT8, + IN in_name TEXT, + IN in_description TEXT, + OUT out_conflict BOOL, + OUT out_not_found BOOL) +LANGUAGE plpgsql +AS $$ +BEGIN + +BEGIN + UPDATE merchant_product_groups SET + product_group_name=in_name + ,product_group_description=in_description + WHERE product_group_serial=in_product_group_serial; + out_not_found = NOT FOUND; + out_conflict = FALSE; + RETURN; +EXCEPTION + WHEN unique_violation + THEN + out_conflict = TRUE; + RETURN; +END; + + +END $$; diff --git a/src/backenddb/update_report.c b/src/backenddb/update_report.c @@ -19,28 +19,26 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_report.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_report (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t report_id, - const char *report_program_section, - const char *report_description, - const char *mime_type, - const char *data_source, - const char *target_address, - struct GNUNET_TIME_Relative frequency, - struct GNUNET_TIME_Relative frequency_shift) +TALER_MERCHANTDB_update_report ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t report_id, + const char *report_program_section, + const char *report_description, + const char *mime_type, + const char *data_source, + const char *target_address, + struct GNUNET_TIME_Relative frequency, + struct GNUNET_TIME_Relative frequency_shift) { struct GNUNET_TIME_Timestamp start; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&report_id), GNUNET_PQ_query_param_string (report_program_section), GNUNET_PQ_query_param_string (report_description), @@ -53,6 +51,9 @@ TALER_MERCHANTDB_update_report (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); start = GNUNET_TIME_absolute_to_timestamp ( GNUNET_TIME_absolute_add ( @@ -62,23 +63,18 @@ TALER_MERCHANTDB_update_report (struct TALER_MERCHANTDB_PostgresContext *pg, frequency_shift))); check_connection (pg); - PREPARE (pg, - "update_report", - "UPDATE merchant_reports SET" - " report_program_section=$3" - " ,report_description=$4" - " ,mime_type=$5" - " ,data_source=$6" - " ,target_address=$7" - " ,frequency=$8" - " ,frequency_shift=$9" - " ,next_transmission=$10" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND report_serial=$2;"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_reports SET" + " report_program_section=$2" + " ,report_description=$3" + " ,mime_type=$4" + " ,data_source=$5" + " ,target_address=$6" + " ,frequency=$7" + " ,frequency_shift=$8" + " ,next_transmission=$9" + " WHERE report_serial=$1;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_report", + "", params); } diff --git a/src/backenddb/update_report_status.c b/src/backenddb/update_report_status.c @@ -19,24 +19,22 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_report_status.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_report_status (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - uint64_t report_id, - struct GNUNET_TIME_Timestamp next_transmission, - enum TALER_ErrorCode last_error_code, - const char *last_error_detail) +TALER_MERCHANTDB_update_report_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t report_id, + struct GNUNET_TIME_Timestamp next_transmission, + enum TALER_ErrorCode last_error_code, + const char *last_error_detail) { uint32_t ec = (uint32_t) last_error_code; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_uint64 (&report_id), GNUNET_PQ_query_param_timestamp (&next_transmission), TALER_EC_NONE == ec @@ -48,19 +46,17 @@ TALER_MERCHANTDB_update_report_status (struct TALER_MERCHANTDB_PostgresContext * GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_report_status", - "UPDATE merchant_reports SET" - " next_transmission=$3" - ",last_error_code=$4" - ",last_error_detail=$5" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND report_serial=$2;"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_reports SET" + " next_transmission=$2" + ",last_error_code=$3" + ",last_error_detail=$4" + " WHERE report_serial=$1;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_report_status", + "", params); } diff --git a/src/backenddb/update_template.c b/src/backenddb/update_template.c @@ -19,21 +19,19 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_template.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_template (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *template_id, - const struct TALER_MERCHANTDB_TemplateDetails *td) +TALER_MERCHANTDB_update_template ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *template_id, + const struct TALER_MERCHANTDB_TemplateDetails *td) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (template_id), GNUNET_PQ_query_param_string (td->template_description), (NULL == td->otp_id) @@ -47,31 +45,25 @@ TALER_MERCHANTDB_update_template (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_template", - "WITH mid AS (" - " SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - ",otp AS (" - " SELECT otp_serial" - " FROM merchant_otp_devices" - " JOIN mid USING (merchant_serial)" - " WHERE otp_id=$4)" - "UPDATE merchant_template SET" - " template_description=$3" - ",otp_device_id=" - " COALESCE((SELECT otp_serial" - " FROM otp), NULL)" - ",template_contract=$5::TEXT::JSONB" - ",editable_defaults=$6::TEXT::JSONB" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM mid)" - " AND template_id=$2"); + TMH_PQ_prepare_anon (pg, + "WITH otp AS (" + " SELECT otp_serial" + " FROM merchant_otp_devices" + " WHERE otp_id=$3)" + "UPDATE merchant_template SET" + " template_description=$2" + ",otp_device_id=" + " COALESCE((SELECT otp_serial" + " FROM otp), NULL)" + ",template_contract=$4::TEXT::JSONB" + ",editable_defaults=$5::TEXT::JSONB" + " WHERE template_id=$1"); qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_template", + "", params); GNUNET_PQ_cleanup_query_params_closures (params); return qs; diff --git a/src/backenddb/update_token_family.c b/src/backenddb/update_token_family.c @@ -27,13 +27,13 @@ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_token_family (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *token_family_slug, - const struct TALER_MERCHANTDB_TokenFamilyDetails *details) +TALER_MERCHANTDB_update_token_family ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *token_family_slug, + const struct TALER_MERCHANTDB_TokenFamilyDetails *details) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (token_family_slug), GNUNET_PQ_query_param_string (details->name), GNUNET_PQ_query_param_string (details->description), @@ -46,22 +46,20 @@ TALER_MERCHANTDB_update_token_family (struct TALER_MERCHANTDB_PostgresContext *p GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_token_family", - "UPDATE merchant_token_families SET" - " name=$3" - ",description=$4" - ",description_i18n=$5::TEXT::JSONB" - ",extra_data=$6::TEXT::JSONB" - ",valid_after=$7" - ",valid_before=$8" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND slug=$2"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_token_families SET" + " name=$2" + ",description=$3" + ",description_i18n=$4::TEXT::JSONB" + ",extra_data=$5::TEXT::JSONB" + ",valid_after=$6" + ",valid_before=$7" + " WHERE slug=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_token_family", + "", params); } diff --git a/src/backenddb/update_transfer_status.c b/src/backenddb/update_transfer_status.c @@ -19,22 +19,21 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_transfer_status.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_transfer_status (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct GNUNET_TIME_Absolute next_attempt, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *detail, - bool needs_retry) +TALER_MERCHANTDB_update_transfer_status ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct GNUNET_TIME_Absolute next_attempt, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *detail, + bool needs_retry) { uint32_t hs32 = (uint32_t) http_status; uint32_t ec32 = (uint32_t) ec; @@ -51,19 +50,19 @@ TALER_MERCHANTDB_update_transfer_status (struct TALER_MERCHANTDB_PostgresContext GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); check_connection (pg); - PREPARE (pg, - "update_transfer_status", - "UPDATE merchant_expected_transfers SET" - " last_http_status=$3" - ",last_ec=$4" - ",last_detail=$5" - ",retry_needed=$6" - ",retry_time=$7" - " WHERE wtid=$1" - " AND exchange_url=$2"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_expected_transfers SET" + " last_http_status=$3" + ",last_ec=$4" + ",last_detail=$5" + ",retry_needed=$6" + ",retry_time=$7" + " WHERE wtid=$1" + " AND exchange_url=$2"); return GNUNET_PQ_eval_prepared_non_select ( pg->conn, - "update_transfer_status", + "", params); } diff --git a/src/backenddb/update_unit.c b/src/backenddb/update_unit.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025 Taler Systems SA + Copyright (C) 2025 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 @@ -19,30 +19,27 @@ * @author Bohdan Potuzhnyi */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_unit.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_unit (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *unit_id, - const char *unit_name_long, - const json_t *unit_name_long_i18n, - const char *unit_name_short, - const json_t *unit_name_short_i18n, - const bool *unit_allow_fraction, - const uint32_t *unit_precision_level, - const bool *unit_active, - bool *no_instance, - bool *no_unit, - bool *builtin_conflict) +TALER_MERCHANTDB_update_unit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *unit_id, + const char *unit_name_long, + const json_t *unit_name_long_i18n, + const char *unit_name_short, + const json_t *unit_name_short_i18n, + const bool *unit_allow_fraction, + const uint32_t *unit_precision_level, + const bool *unit_active, + bool *no_unit, + bool *builtin_conflict) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (unit_id), (NULL == unit_name_long) ? GNUNET_PQ_query_param_null () @@ -68,8 +65,6 @@ TALER_MERCHANTDB_update_unit (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("out_no_instance", - no_instance), GNUNET_PQ_result_spec_bool ("out_no_unit", no_unit), GNUNET_PQ_result_spec_bool ("out_builtin_conflict", @@ -78,17 +73,18 @@ TALER_MERCHANTDB_update_unit (struct TALER_MERCHANTDB_PostgresContext *pg, }; enum GNUNET_DB_QueryStatus qs; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_unit", - "SELECT" - " out_no_instance" - " ,out_no_unit" - " ,out_builtin_conflict" - " FROM merchant_do_update_unit" - "($1,$2,$3,$4,$5,$6,$7,$8,$9);"); + TMH_PQ_prepare_anon (pg, + "SELECT" + " out_no_unit" + " ,out_builtin_conflict" + " FROM merchant_do_update_unit" + "($1,$2,$3,$4,$5,$6,$7,$8);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "update_unit", + "", params, rs); GNUNET_PQ_cleanup_query_params_closures (params); diff --git a/src/backenddb/update_unit.sql b/src/backenddb/update_unit.sql @@ -0,0 +1,172 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 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 update_unit.sql +-- @brief SQL for updating units +-- @author Bohdan Potuzhnyi + +DROP FUNCTION IF EXISTS merchant_do_update_unit; +CREATE FUNCTION merchant_do_update_unit ( + IN in_unit_id TEXT, + IN in_unit_name_long TEXT, + IN in_unit_name_long_i18n BYTEA, + IN in_unit_name_short TEXT, + IN in_unit_name_short_i18n BYTEA, + IN in_unit_allow_fraction BOOL, + IN in_unit_precision_level INT4, + IN in_unit_active BOOL, + OUT out_no_unit BOOL, + OUT out_builtin_conflict BOOL) + LANGUAGE plpgsql +AS $$ +DECLARE + my_custom RECORD; + my_builtin RECORD; + my_override RECORD; + new_unit_name_long TEXT; + new_unit_name_short TEXT; + new_unit_name_long_i18n BYTEA; + new_unit_name_short_i18n BYTEA; + new_unit_allow_fraction BOOL; + new_unit_precision_level INT4; + new_unit_active BOOL; + old_unit_allow_fraction BOOL; + old_unit_precision_level INT4; + old_unit_active BOOL; +BEGIN + out_no_unit := FALSE; + out_builtin_conflict := FALSE; + + SELECT * + INTO my_custom + FROM merchant_custom_units + WHERE unit = in_unit_id + FOR UPDATE; + + IF FOUND THEN + old_unit_allow_fraction := my_custom.unit_allow_fraction; + old_unit_precision_level := my_custom.unit_precision_level; + old_unit_active := my_custom.unit_active; + + new_unit_name_long := COALESCE (in_unit_name_long, my_custom.unit_name_long); + new_unit_name_short := COALESCE (in_unit_name_short, my_custom.unit_name_short); + new_unit_name_long_i18n := COALESCE (in_unit_name_long_i18n, + my_custom.unit_name_long_i18n); + new_unit_name_short_i18n := COALESCE (in_unit_name_short_i18n, + my_custom.unit_name_short_i18n); + new_unit_allow_fraction := COALESCE (in_unit_allow_fraction, + my_custom.unit_allow_fraction); + new_unit_precision_level := COALESCE (in_unit_precision_level, + my_custom.unit_precision_level); + IF NOT new_unit_allow_fraction THEN + new_unit_precision_level := 0; + END IF; + + new_unit_active := COALESCE (in_unit_active, my_custom.unit_active); + + UPDATE merchant_custom_units SET + unit_name_long = new_unit_name_long + ,unit_name_long_i18n = new_unit_name_long_i18n + ,unit_name_short = new_unit_name_short + ,unit_name_short_i18n = new_unit_name_short_i18n + ,unit_allow_fraction = new_unit_allow_fraction + ,unit_precision_level = new_unit_precision_level + ,unit_active = new_unit_active + WHERE unit_serial = my_custom.unit_serial; + + ASSERT FOUND,'SELECTED it earlier, should UPDATE it now'; + + IF old_unit_allow_fraction IS DISTINCT FROM new_unit_allow_fraction + OR old_unit_precision_level IS DISTINCT FROM new_unit_precision_level + THEN + UPDATE merchant_inventory SET + allow_fractional_quantity = new_unit_allow_fraction + , fractional_precision_level = new_unit_precision_level + WHERE unit = in_unit_id + AND allow_fractional_quantity = old_unit_allow_fraction + AND fractional_precision_level = old_unit_precision_level; + END IF; + RETURN; + END IF; + + -- Try builtin with overrides. + SELECT * + INTO my_builtin + FROM merchant.merchant_builtin_units + WHERE unit = in_unit_id; + + IF NOT FOUND THEN + out_no_unit := TRUE; + RETURN; + END IF; + + SELECT * + INTO my_override + FROM merchant_builtin_unit_overrides + WHERE builtin_unit_serial = my_builtin.unit_serial + FOR UPDATE; + + old_unit_allow_fraction := COALESCE (my_override.override_allow_fraction, + my_builtin.unit_allow_fraction); + old_unit_precision_level := COALESCE (my_override.override_precision_level, + my_builtin.unit_precision_level); + old_unit_active := COALESCE (my_override.override_active, + my_builtin.unit_active); + + -- Only policy flags can change for builtin units. + IF in_unit_name_long IS NOT NULL + OR in_unit_name_short IS NOT NULL + OR in_unit_name_long_i18n IS NOT NULL + OR in_unit_name_short_i18n IS NOT NULL THEN + out_builtin_conflict := TRUE; + RETURN; + END IF; + + new_unit_allow_fraction := COALESCE (in_unit_allow_fraction, + old_unit_allow_fraction); + new_unit_precision_level := COALESCE (in_unit_precision_level, + old_unit_precision_level); + IF NOT new_unit_allow_fraction THEN + new_unit_precision_level := 0; + END IF; + new_unit_active := COALESCE (in_unit_active, old_unit_active); + + INSERT INTO merchant_builtin_unit_overrides ( + builtin_unit_serial, + override_allow_fraction, + override_precision_level, + override_active) + VALUES (my_builtin.unit_serial, + new_unit_allow_fraction, + new_unit_precision_level, + new_unit_active) + ON CONFLICT (merchant_serial, builtin_unit_serial) + DO UPDATE SET override_allow_fraction = EXCLUDED.override_allow_fraction + , override_precision_level = EXCLUDED.override_precision_level + , override_active = EXCLUDED.override_active; + + IF old_unit_allow_fraction IS DISTINCT FROM new_unit_allow_fraction + OR old_unit_precision_level IS DISTINCT FROM new_unit_precision_level + THEN + UPDATE merchant_inventory SET + allow_fractional_quantity = new_unit_allow_fraction + , fractional_precision_level = new_unit_precision_level + WHERE unit = in_unit_id + AND allow_fractional_quantity = old_unit_allow_fraction + AND fractional_precision_level = old_unit_precision_level; + END IF; + + RETURN; +END $$; diff --git a/src/backenddb/update_webhook.c b/src/backenddb/update_webhook.c @@ -19,20 +19,19 @@ * @author Iván Ávalos */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_webhook.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *webhook_id, - const struct TALER_MERCHANTDB_WebhookDetails *wb) +TALER_MERCHANTDB_update_webhook ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *webhook_id, + const struct TALER_MERCHANTDB_WebhookDetails *wb) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (webhook_id), GNUNET_PQ_query_param_string (wb->event_type), GNUNET_PQ_query_param_string (wb->url), @@ -46,22 +45,20 @@ TALER_MERCHANTDB_update_webhook (struct TALER_MERCHANTDB_PostgresContext *pg, GNUNET_PQ_query_param_end }; + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); check_connection (pg); - PREPARE (pg, - "update_webhook", - "UPDATE merchant_webhook SET" - " event_type=$3" - ",url=$4" - ",http_method=$5" - ",header_template=$6" - ",body_template=$7" - " WHERE merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)" - " AND webhook_id=$2"); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_webhook SET" + " event_type=$2" + ",url=$3" + ",http_method=$4" + ",header_template=$5" + ",body_template=$6" + " WHERE webhook_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_webhook", + "", params); } diff --git a/src/backenddb/update_wirewatch_progress.c b/src/backenddb/update_wirewatch_progress.c @@ -19,38 +19,34 @@ * @author Christian Grothoff */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "merchant-database/update_wirewatch_progress.h" #include "helper.h" enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_wirewatch_progress (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance, - struct TALER_FullPayto payto_uri, - uint64_t last_serial) +TALER_MERCHANTDB_update_wirewatch_progress ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance, + struct TALER_FullPayto payto_uri, + uint64_t last_serial) { struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance), GNUNET_PQ_query_param_string (payto_uri.full_payto), GNUNET_PQ_query_param_uint64 (&last_serial), GNUNET_PQ_query_param_end }; - PREPARE (pg, - "update_wirewatch_progress", - "UPDATE merchant_accounts" - " SET last_bank_serial=$3" - " WHERE REGEXP_REPLACE(payto_uri,'\\?.*','')" - " =REGEXP_REPLACE(CAST ($2 AS TEXT),'\\?.*','')" - " AND merchant_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance, + pg->current_merchant_id)); check_connection (pg); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_accounts" + " SET last_bank_serial=$2" + " WHERE REGEXP_REPLACE(payto_uri,'\\?.*','')" + " =REGEXP_REPLACE(CAST ($1 AS TEXT),'\\?.*','')"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "update_wirewatch_progress", + "", params); } diff --git a/src/backenddb/upsert_donau_keys.c b/src/backenddb/upsert_donau_keys.c @@ -20,17 +20,17 @@ * @author Vlada Svirsh */ #include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> #include <taler/taler_pq_lib.h> #include "donau/donau_service.h" #include "merchant-database/upsert_donau_keys.h" #include "helper.h" + enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_upsert_donau_keys (struct TALER_MERCHANTDB_PostgresContext *pg, - const struct DONAU_Keys *keys, - struct GNUNET_TIME_Absolute first_retry) +TALER_MERCHANTDB_upsert_donau_keys ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const struct DONAU_Keys *keys, + struct GNUNET_TIME_Absolute first_retry) { enum GNUNET_DB_QueryStatus qs; json_t *jkeys = DONAU_keys_to_json (keys); @@ -47,6 +47,7 @@ TALER_MERCHANTDB_upsert_donau_keys (struct TALER_MERCHANTDB_PostgresContext *pg, }; check_connection (pg); + // FIXME: combine these into a stored procedure PREPARE (pg, "insert_donau_keys", "INSERT INTO merchant_donau_keys" diff --git a/src/include/merchant-database/delete_unit.h b/src/include/merchant-database/delete_unit.h @@ -24,14 +24,12 @@ #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Delete a measurement unit. * * @param pg database context * @param instance_id instance to delete unit from * @param unit_id symbolic identifier - * @param[out] no_instance set to true if @a instance_id is unknown * @param[out] no_unit set to true if the unit does not exist * @param[out] builtin_conflict set to true if the unit cannot be deleted * @return database result code @@ -40,7 +38,6 @@ enum GNUNET_DB_QueryStatus TALER_MERCHANTDB_delete_unit (struct TALER_MERCHANTDB_PostgresContext *pg, const char *instance_id, const char *unit_id, - bool *no_instance, bool *no_unit, bool *builtin_conflict); diff --git a/src/include/merchant-database/insert_product.h b/src/include/merchant-database/insert_product.h @@ -22,11 +22,9 @@ #define MERCHANT_DATABASE_INSERT_PRODUCT_H #include <taler/taler_util.h> -#include <taler/taler_json_lib.h> #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Insert details about a particular product. * @@ -36,7 +34,6 @@ struct TALER_MERCHANTDB_PostgresContext; * @param pd the product details to insert * @param num_cats length of @a cats array * @param cats array of categories the product is in - * @param[out] no_instance set to true if @a instance_id is unknown * @param[out] conflict set to true if a conflicting * product already exists in the database * @param[out] no_cat set to index of non-existing category from @a cats, or -1 if all @a cats were found @@ -45,17 +42,17 @@ struct TALER_MERCHANTDB_PostgresContext; * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_product (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd, - size_t num_cats, - const uint64_t *cats, - bool *no_instance, - bool *conflict, - ssize_t *no_cat, - bool *no_group, - bool *no_pot); +TALER_MERCHANTDB_insert_product ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + bool *conflict, + ssize_t *no_cat, + bool *no_group, + bool *no_pot); #endif diff --git a/src/include/merchant-database/insert_transfer.h b/src/include/merchant-database/insert_transfer.h @@ -22,11 +22,9 @@ #define MERCHANT_DATABASE_INSERT_TRANSFER_H #include <taler/taler_util.h> -#include <taler/taler_json_lib.h> #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Insert information about a wire transfer the merchant has received. * Idempotent, will do nothing if the same wire transfer is already known. @@ -47,16 +45,16 @@ struct TALER_MERCHANTDB_PostgresContext; * @return transaction status */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_transfer (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *credit_amount, - struct TALER_FullPayto payto_uri, - uint64_t bank_serial_id, - bool *no_instance, - bool *no_account, - bool *conflict); +TALER_MERCHANTDB_insert_transfer ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_Amount *credit_amount, + struct TALER_FullPayto payto_uri, + uint64_t bank_serial_id, + bool *no_account, + bool *conflict); #endif diff --git a/src/include/merchant-database/insert_unclaim_signature.h b/src/include/merchant-database/insert_unclaim_signature.h @@ -22,17 +22,14 @@ #define MERCHANT_DATABASE_INSERT_UNCLAIM_SIGNATURE_H #include <taler/taler_util.h> -#include <taler/taler_json_lib.h> #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Insert unclaim signature into DB and unclaim the order. * * @param pg database context * @param instance_id instance of the operation (checked) - * @param merchant_pub public key of the instance (for notifications) * @param order_id identifies the order to unclaim * @param nonce claim nonce of the original claim * @param h_contract hash of the contract to unclaim @@ -41,12 +38,12 @@ struct TALER_MERCHANTDB_PostgresContext; * and @a h_contract was not found; 1 if the request is idempotent */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_unclaim_signature (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const char *order_id, - const struct GNUNET_CRYPTO_EddsaPublicKey *nonce, - const struct GNUNET_HashCode *h_contract, - const struct GNUNET_CRYPTO_EddsaSignature *nsig); +TALER_MERCHANTDB_insert_unclaim_signature ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *order_id, + const struct GNUNET_CRYPTO_EddsaPublicKey *nonce, + const struct GNUNET_HashCode *h_contract, + const struct GNUNET_CRYPTO_EddsaSignature *nsig); #endif diff --git a/src/include/merchant-database/insert_unit.h b/src/include/merchant-database/insert_unit.h @@ -22,28 +22,25 @@ #define MERCHANT_DATABASE_INSERT_UNIT_H #include <taler/taler_util.h> -#include <taler/taler_json_lib.h> #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Insert a measurement unit definition. * * @param pg database context * @param instance_id instance to insert unit for * @param ud unit definition to store (unit_serial ignored) - * @param[out] no_instance set to true if the instance is unknown * @param[out] conflict set to true if a conflicting unit already exists * @param[out] unit_serial set to the generated serial on success (or the existing serial for idempotent inserts) * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_insert_unit (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const struct TALER_MERCHANTDB_UnitDetails *ud, - bool *no_instance, - bool *conflict, - uint64_t *unit_serial); +TALER_MERCHANTDB_insert_unit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const struct TALER_MERCHANTDB_UnitDetails *ud, + bool *conflict, + uint64_t *unit_serial); #endif diff --git a/src/include/merchant-database/lookup_all_webhooks.h b/src/include/merchant-database/lookup_all_webhooks.h @@ -0,0 +1,73 @@ +/* + This file is part of TALER + Copyright (C) 2023 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 src/include/merchant-database/lookup_all_webhooks.h + * @brief implementation of the lookup_all_webhooks function for Postgres + * @author Iván Ávalos + */ +#ifndef MERCHANT_DATABASE_LOOKUP_ALL_WEBHOOKS_H +#define MERCHANT_DATABASE_LOOKUP_ALL_WEBHOOKS_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "merchantdb_lib.h" + + +/** + * Typically called by `lookup_all_webhooks`. + * + * @param cls a `json_t *` JSON array to build + * @param webhook_serial reference to the configured webhook template. + * @param next_attempt is the time we should make the next request to the webhook. + * @param retries how often have we tried this request to the webhook. + * @param url to make request to + * @param http_method use for the webhook + * @param header of the webhook + * @param body of the webhook + */ +typedef void +(*TALER_MERCHANTDB_AllWebhooksCallback)( + void *cls, + uint64_t webhook_serial, + struct GNUNET_TIME_Absolute + next_attempt, + uint32_t retries, + const char *url, + const char *http_method, + const char *header, + const char *body); + + +/** + * Lookup all the webhooks in the ALL webhook. + * Use by the administrator + * + * @param pg database context + * @param instance_id to lookup webhooks for this instance particularly + * @param min_row to see the list of the ALL webhook that it is started with this minimum row. + * @param max_results to see the list of the ALL webhook that it is end with this max results. + * @param cb ALL webhook callback + * @param cb_cls callback closure + */ +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_lookup_all_webhooks (struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + uint64_t min_row, + uint32_t max_results, + TALER_MERCHANTDB_AllWebhooksCallback cb, + void *cb_cls); + +#endif diff --git a/src/include/merchant-database/lookup_order_charity.h b/src/include/merchant-database/lookup_order_charity.h @@ -42,7 +42,6 @@ * @param instance_id the instance ID of the merchant * @param donau_url base URL of the Donau instance * @param[out] charity_id set to the `charity_id` from `merchant_donau_instances` - * @param[out] charity_priv set to the private key (32 bytes) * @param[out] charity_max_per_year set to the maximum amount that can be donated * per year to this charity * @param[out] charity_receipts_to_date set to the total amount of receipts @@ -56,7 +55,6 @@ TALER_MERCHANTDB_lookup_order_charity ( const char *instance_id, const char *donau_url, uint64_t *charity_id, - struct DONAU_CharityPrivateKeyP *charity_priv, struct TALER_Amount *charity_max_per_year, struct TALER_Amount *charity_receipts_to_date, json_t **donau_keys_json, diff --git a/src/include/merchant-database/select_accounts.h b/src/include/merchant-database/select_accounts.h @@ -26,8 +26,6 @@ #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; -/* Callback typedefs */ /** * Callback invoked with information about a bank account. * @@ -41,19 +39,19 @@ typedef void const struct TALER_MerchantPrivateKeyP *merchant_priv, const struct TALER_MERCHANTDB_AccountDetails *ad); + /** * Obtain information about an instance's accounts. * * @param pg database context - * @param id identifier of the instance * @param cb function to call on each account * @param cb_cls closure for @a cb * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_select_accounts (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *id, - TALER_MERCHANTDB_AccountCallback cb, - void *cb_cls); +TALER_MERCHANTDB_select_accounts ( + struct TALER_MERCHANTDB_PostgresContext *pg, + TALER_MERCHANTDB_AccountCallback cb, + void *cb_cls); #endif diff --git a/src/include/merchant-database/select_accounts_by_instance.h b/src/include/merchant-database/select_accounts_by_instance.h @@ -0,0 +1,58 @@ +/* + This file is part of TALER + Copyright (C) 2022 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 src/include/merchant-database/select_accounts_by_instance.h + * @brief implementation of the select_accounts_by_instance function for Postgres + * @author Christian Grothoff + */ +#ifndef MERCHANT_DATABASE_SELECT_ACCOUNTS_BY_INSTANCE_H +#define MERCHANT_DATABASE_SELECT_ACCOUNTS_BY_INSTANCE_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "merchantdb_lib.h" + + +/** + * Callback invoked with information about a bank account. + * + * @param cls closure + * @param merchant_priv private key of the merchant instance + * @param ad details about the account + */ +typedef void +(*TALER_MERCHANTDB_AccountCallback)( + void *cls, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_AccountDetails *ad); + +/** + * Obtain information about an instance's accounts. + * + * @param pg database context + * @param id identifier of the instance + * @param cb function to call on each account + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_select_accounts_by_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *id, + TALER_MERCHANTDB_AccountCallback cb, + void *cb_cls); + +#endif diff --git a/src/include/merchant-database/set_instance.h b/src/include/merchant-database/set_instance.h @@ -0,0 +1,51 @@ +/* + 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 src/include/merchant-database/set_instance.h + * @brief Select the per-instance schema active on the connection + * @author Christian Grothoff + */ +#ifndef MERCHANT_DATABASE_SET_INSTANCE_H +#define MERCHANT_DATABASE_SET_INSTANCE_H + +#include <taler/taler_util.h> +#include "merchantdb_lib.h" + + +/** + * Resolve @a instance_id to its merchant_serial and route subsequent + * per-instance queries on @a pg to the corresponding + * `merchant_instance_<serial>` schema. Updates the connection's + * search_path to "merchant_instance_<serial>, merchant" and stores + * @a instance_id / serial in the context for use by per-instance + * call sites and the #PREPARE_INSTANCE macro. + * + * Idempotent: a call with an already-active @a instance_id is a no-op. + * + * @param pg database connection + * @param instance_id instance to activate (must be NUL-terminated) + * @return #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the instance was found + * and the search_path was updated; + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if no such instance exists; + * a hard or soft error code otherwise + */ +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_set_instance ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id); + + +#endif diff --git a/src/include/merchant-database/update_product.h b/src/include/merchant-database/update_product.h @@ -22,11 +22,9 @@ #define MERCHANT_DATABASE_UPDATE_PRODUCT_H #include <taler/taler_util.h> -#include <taler/taler_json_lib.h> #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Update details about a particular product. Note that the * transaction must enforce that the sold/stocked/lost counters @@ -37,7 +35,6 @@ struct TALER_MERCHANTDB_PostgresContext; * @param product_id product to lookup * @param num_cats length of @a cats array * @param cats number of categories the product is in - * @param[out] no_instance the update failed as the instance is unknown * @param[out] no_cat set to -1 on success, otherwise the update failed and this is set * to the index of a category in @a cats that is unknown * @param[out] no_product the @a product_id is unknown @@ -55,19 +52,19 @@ struct TALER_MERCHANTDB_PostgresContext; * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_product (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd, - size_t num_cats, - const uint64_t *cats, - bool *no_instance, - ssize_t *no_cat, - bool *no_product, - bool *lost_reduced, - bool *sold_reduced, - bool *stocked_reduced, - bool *no_group, - bool *no_pot); +TALER_MERCHANTDB_update_product ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_cats, + const uint64_t *cats, + ssize_t *no_cat, + bool *no_product, + bool *lost_reduced, + bool *sold_reduced, + bool *stocked_reduced, + bool *no_group, + bool *no_pot); #endif diff --git a/src/include/merchant-database/update_unit.h b/src/include/merchant-database/update_unit.h @@ -22,11 +22,9 @@ #define MERCHANT_DATABASE_UPDATE_UNIT_H #include <taler/taler_util.h> -#include <taler/taler_json_lib.h> #include "merchantdb_lib.h" -struct TALER_MERCHANTDB_PostgresContext; /** * Update an existing measurement unit definition. * @@ -40,24 +38,23 @@ struct TALER_MERCHANTDB_PostgresContext; * @param unit_allow_fraction optional new fractional toggle * @param unit_precision_level optional new fractional precision * @param unit_active optional new visibility flag - * @param[out] no_instance set if instance unknown * @param[out] no_unit set if unit unknown * @param[out] builtin_conflict set if immutable builtin fields touched * @return database result code */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_unit (struct TALER_MERCHANTDB_PostgresContext *pg, - const char *instance_id, - const char *unit_id, - const char *unit_name_long, - const json_t *unit_name_long_i18n, - const char *unit_name_short, - const json_t *unit_name_short_i18n, - const bool *unit_allow_fraction, - const uint32_t *unit_precision_level, - const bool *unit_active, - bool *no_instance, - bool *no_unit, - bool *builtin_conflict); +TALER_MERCHANTDB_update_unit ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *unit_id, + const char *unit_name_long, + const json_t *unit_name_long_i18n, + const char *unit_name_short, + const json_t *unit_name_short_i18n, + const bool *unit_allow_fraction, + const uint32_t *unit_precision_level, + const bool *unit_active, + bool *no_unit, + bool *builtin_conflict); #endif diff --git a/src/testing/test_merchant_accounts.sh b/src/testing/test_merchant_accounts.sh @@ -42,10 +42,11 @@ echo -n "Configuring 'admin' instance ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/management/instances \ -d '{"auth":{"method":"token","password":"secret-token:new_value"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204, instance created. got: $STATUS" >&2 fi @@ -67,6 +68,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi @@ -82,6 +84,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X GET \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi @@ -89,6 +92,7 @@ ACTIVE=$(jq -r .active $LAST_RESPONSE) if [ "$ACTIVE" != "true" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account active." fi @@ -97,10 +101,11 @@ echo -n "deleting account ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ -H 'Authorization: Bearer secret-token:new_value' \ http://localhost:9966/private/accounts/$ACCOUNT_ID \ - -w "%{http_code}" -s ) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204 OK. Got: $STATUS" fi @@ -117,6 +122,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi @@ -129,15 +135,18 @@ ACTIVE=$(jq -r .active $LAST_RESPONSE) if [ "$ACTIVE" != "true" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account active." fi echo "OK" >&2 ## -# Using different name should not conflict with previous account. +# Using different name should conflict with previous account. ## +ACCOUNT_ID=$(jq -r .h_wire $LAST_RESPONSE) + echo -n "creating same account with different name ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ @@ -147,14 +156,12 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -w "%{http_code}" -s -o "$LAST_RESPONSE") -if [ "$STATUS" != "200" ] +if [ "$STATUS" != "409" ] then - exit_fail "Expected 200 OK. Got: $STATUS" + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 409 Conflict. Got: $STATUS" fi -ACCOUNT_ID=$(jq -r .h_wire $LAST_RESPONSE) - - STATUS=$(curl -H "Content-Type: application/json" -X GET \ -H 'Authorization: Bearer secret-token:new_value' \ http://localhost:9966/private/accounts/$ACCOUNT_ID \ @@ -164,11 +171,14 @@ ACTIVE=$(jq -r .active $LAST_RESPONSE) if [ "$ACTIVE" != "true" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account active." fi echo "OK" >&2 + + echo -n "deleting the account ..." >&2 STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ @@ -178,6 +188,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204 OK. Got: $STATUS" fi @@ -194,9 +205,12 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi +ACCOUNT_ID=$(jq -r .h_wire $LAST_RESPONSE) + STATUS=$(curl -H "Content-Type: application/json" -X GET \ -H 'Authorization: Bearer secret-token:new_value' \ http://localhost:9966/private/accounts/$ACCOUNT_ID \ @@ -206,6 +220,7 @@ ACTIVE=$(jq -r .active $LAST_RESPONSE) if [ "$ACTIVE" != "true" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account active." fi @@ -227,6 +242,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi @@ -242,6 +258,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X GET \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi @@ -249,6 +266,7 @@ ACTIVE=$(jq -r .active $LAST_RESPONSE) if [ "$ACTIVE" != "true" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account active." fi @@ -261,6 +279,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204 OK. Got: $STATUS" fi @@ -277,6 +296,7 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ if [ "$STATUS" != "200" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi @@ -285,17 +305,19 @@ STATUS=$(curl -H "Content-Type: application/json" -X GET \ http://localhost:9966/private/accounts/$ACCOUNT_ID \ -w "%{http_code}" -s -o "$LAST_RESPONSE") -ACTIVE=$(jq -r .active $LAST_RESPONSE) +ACTIVE=$(jq -r .active "$LAST_RESPONSE") if [ "$ACTIVE" != "true" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account active." fi -FACADE=$(jq -r .credit_facade_url $LAST_RESPONSE) +FACADE=$(jq -r .credit_facade_url "$LAST_RESPONSE") if [ "$FACADE" != "http://asd.com/" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected account with facade http://asd.com/." fi @@ -317,7 +339,12 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ if [ "$STATUS" != "409" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 409 Conflict. Got: $STATUS" fi echo "OK" >&2 + +echo "Test PASSED" + +exit 0 diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh @@ -100,7 +100,7 @@ taler-merchant-exchangekeyupdate \ -c "${CONF}" \ -L DEBUG \ -t \ - 2> taler-merchant-exchangekeyupdate2.log + 2> taler-merchant-exchangekeyupdate2.log taler-merchant-passwd \ -c "${CONF}" \ -L DEBUG \ @@ -196,15 +196,15 @@ PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE") echo "OK order ${ORDER_ID} with ${ORD_TOKEN} and ${PAY_URL}" >&2 echo -n "Configuring 'second' instance ..." >&2 - STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Bearer '"$TOKEN" \ http://localhost:9966/management/instances \ -d '{"auth":{"method":"token","password":"second"},"id":"second","name":"second","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204, instance created. got: $STATUS" fi @@ -216,10 +216,11 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Bearer '"$TOKEN" \ http://localhost:9966/management/instances \ -d '{"auth":{"method":"token","password":"third"},"id":"third","name":"third","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204, instance created. got: $STATUS" fi @@ -231,10 +232,11 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Bearer '"$TOKEN" \ http://localhost:9966/management/instances/second/auth \ -d '{"method":"token","password":"new_one"}' \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then + cat "$LAST_RESPONSE" >&2 exit_fail "Expected 204, instance auth token changed. got: $STATUS" fi NEW_SECRET="new_one" @@ -304,7 +306,7 @@ BASIC_AUTH2=$(echo -n second:again | base64) if [ "$STATUS" != "204" ] then - cat $LAST_RESPONSE + cat $LAST_RESPONSE >&2 exit_fail "Expected 204, instance not authorized. got: $STATUS" fi @@ -316,10 +318,11 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Bearer '"$RWTOKEN" \ http://localhost:9966/management/instances/third/auth \ -d '{"method":"token","password":"new_one"}' \ - -w "%{http_code}" -s -o /dev/null) + -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "401" ] then + cat $LAST_RESPONSE >&2 exit_fail "Expected 401, instance not authorized. got: $STATUS" fi diff --git a/src/testing/test_merchant_mfa.sh b/src/testing/test_merchant_mfa.sh @@ -56,6 +56,7 @@ then exit_fail "Expected 200 OK. Got: $STATUS" fi +echo " OK" echo -n "Self-provision instance ..." STATUS=$(curl -H "Content-Type: application/json" -X POST \ @@ -69,6 +70,7 @@ then jq < "$LAST_RESPONSE" exit_fail "Expected 202 Accepted. Got: $STATUS" fi +echo " OK" C1=$(jq -r .challenges[0].challenge_id < "$LAST_RESPONSE") C2=$(jq -r .challenges[1].challenge_id < "$LAST_RESPONSE") diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh @@ -723,4 +723,5 @@ then fi echo " OK" +echo "Test PASSED" exit 0 diff --git a/src/testing/test_merchant_product_creation.sh b/src/testing/test_merchant_product_creation.sh @@ -316,4 +316,6 @@ then fi echo " OK" +echo "Test PASSED" + exit 0 diff --git a/src/testing/test_merchant_wirewatch.sh b/src/testing/test_merchant_wirewatch.sh @@ -391,4 +391,6 @@ then fi echo " OK" +echo "Test PASSED" + exit 0