commit dbd27ecb078bd523de99118ed5a1ea0846de8cbb parent 1eb401b1b76ec25a21fd7360630209bb0adb8ff5 Author: Christian Grothoff <christian@grothoff.org> Date: Mon, 29 Dec 2025 22:14:32 +0100 add money pot update logic to payment, add default_money_pot support to order/contract Diffstat:
15 files changed, 468 insertions(+), 10 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -121,6 +121,11 @@ enum PayPhase PP_CONTRACT_PAID, /** + * Compute money pot changes. + */ + PP_COMPUTE_MONEY_POTS, + + /** * Execute payment transaction. */ PP_PAY_TRANSACTION, @@ -696,6 +701,26 @@ struct PayContext } validate_tokens; + + struct + { + /** + * Length of the @a pots and @a increments arrays. + */ + unsigned int num_pots; + + /** + * Serial IDs of money pots to increment. + */ + uint64_t *pots; + + /** + * Increment for the respective money pot. + */ + struct TALER_Amount *increments; + + } compute_money_pots; + /** * Results from the phase_execute_pay_transaction() */ @@ -1222,7 +1247,7 @@ batch_deposit_cb ( dr); if (0 == pc->batch_deposits.pending_at_eg) { - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_COMPUTE_MONEY_POTS; pay_resume (pc); } return; @@ -1244,7 +1269,7 @@ batch_deposit_cb ( } if (0 == pc->batch_deposits.pending_at_eg) { - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_COMPUTE_MONEY_POTS; pay_resume (pc); } return; @@ -1566,7 +1591,7 @@ AGE_FAIL: pc->batch_deposits.pending_at_eg); if (0 == pc->batch_deposits.pending_at_eg) { - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_COMPUTE_MONEY_POTS; pay_resume (pc); return; } @@ -1761,7 +1786,7 @@ phase_batch_deposits (struct PayContext *pc) } if (0 == pc->batch_deposits.pending_at_eg) { - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_COMPUTE_MONEY_POTS; pay_resume (pc); return; } @@ -2211,6 +2236,103 @@ phase_request_donation_receipt (struct PayContext *pc) /** + * Increment the money pot @a pot_id in @a pc by @a increment. + * + * @param[in,out] pc context to update + * @param pot_id money pot to increment + * @param increment amount to add + */ +static void +increment_pot (struct PayContext *pc, + uint64_t pot_id, + const struct TALER_Amount *increment) +{ + for (unsigned int i = 0; i<pc->compute_money_pots.num_pots; i++) + { + if (pot_id == pc->compute_money_pots.pots[i]) + { + struct TALER_Amount *p; + + p = &pc->compute_money_pots.increments[i]; + GNUNET_assert (0 <= + TALER_amount_add (p, + p, + increment)); + return; + } + } + GNUNET_array_append (pc->compute_money_pots.pots, + pc->compute_money_pots.num_pots, + pot_id); + pc->compute_money_pots.num_pots--; /* do not increment twice... */ + GNUNET_array_append (pc->compute_money_pots.increments, + pc->compute_money_pots.num_pots, + *increment); +} + + +/** + * Compute the total changes to money pots in preparation + * for the #PP_PAY_TRANSACTION phase. + * + * @param[in,out] pc payment context to transact + */ +static void +phase_compute_money_pots (struct PayContext *pc) +{ + const struct TALER_MERCHANT_Contract *contract + = pc->check_contract.contract_terms; + struct TALER_Amount unassigned; + + if (0 == pc->parse_pay.coins_cnt) + { + /* Did not pay with any coins, so no currency/amount involved, + hence no money pot update possible. */ + pc->phase++; + return; + } + + TALER_amount_set_zero (pc->parse_pay.dc[0].cdd.amount.currency, + &unassigned); + GNUNET_assert (NULL != contract); + for (size_t i = 0; i<contract->products_len; i++) + { + const struct TALER_MERCHANT_Product *product + = &contract->products[i]; + // FIXME: handle products with multiple prices! + const struct TALER_Amount *price = &product->price; + + if (GNUNET_OK != + TALER_amount_cmp_currency (&unassigned, + price)) + { + GNUNET_break (0); + continue; + } + if (0 == product->product_money_pot) + { + GNUNET_assert (0 <= + TALER_amount_add (&unassigned, + &unassigned, + price)); + } + else + { + increment_pot (pc, + product->product_money_pot, + price); + } + } + if ( (! TALER_amount_is_zero (&unassigned)) && + (0 != contract->default_money_pot) ) + increment_pot (pc, + contract->default_money_pot, + &unassigned); + pc->phase++; +} + + +/** * Function called with information about a coin that was deposited. * * @param cls closure @@ -2650,6 +2772,40 @@ phase_execute_pay_transaction (struct PayContext *pc) return; } + if (0 < pc->compute_money_pots.num_pots) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->increment_money_pots (TMH_db->cls, + instance_id, + pc->compute_money_pots.num_pots, + pc->compute_money_pots.pots, + pc->compute_money_pots.increments); + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + return; /* do it again */ + case GNUNET_DB_STATUS_HARD_ERROR: + /* Always report on hard error as well to enable diagnostics */ + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "increment_money_pots")); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* strange */ + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Good, proceed! */ + break; + } + + } + for (size_t i = 0; i<pc->parse_pay.tokens_cnt; i++) { struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; @@ -3541,7 +3697,7 @@ phase_validate_tokens (struct PayContext *pc) { case TALER_MERCHANT_CONTRACT_VERSION_0: /* No tokens to validate */ - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_COMPUTE_MONEY_POTS; pc->validate_tokens.max_fee = pc->check_contract.contract_terms->details.v0.max_fee; pc->validate_tokens.brutto @@ -3762,7 +3918,7 @@ phase_validate_tokens (struct PayContext *pc) } } - pc->phase = PP_PAY_TRANSACTION; + pc->phase = PP_COMPUTE_MONEY_POTS; } @@ -4908,6 +5064,8 @@ pay_context_cleanup (void *cls) pc_tail, pc); GNUNET_free (pc->check_contract.pos_key); + GNUNET_free (pc->compute_money_pots.pots); + GNUNET_free (pc->compute_money_pots.increments); #ifdef HAVE_DONAU_DONAU_SERVICE_H if (NULL != pc->parse_wallet_data.bkps) { @@ -4987,6 +5145,9 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, case PP_CONTRACT_PAID: phase_contract_paid (pc); break; + case PP_COMPUTE_MONEY_POTS: + phase_compute_money_pots (pc); + break; case PP_PAY_TRANSACTION: phase_execute_pay_transaction (pc); break; diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -415,6 +415,12 @@ struct OrderContext uint32_t minimum_age; /** + * Money pot to increment for whatever order payment amount + * is not yet assigned to a pot via the Product. + */ + uint64_t order_default_money_pot; + + /** * Version of the contract terms. */ enum TALER_MERCHANT_ContractVersion version; @@ -2333,6 +2339,9 @@ phase_serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_uint64 ("minimum_age", oc->parse_order.minimum_age)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_uint64 ("default_money_pot", + oc->parse_order.order_default_money_pot)), GNUNET_JSON_pack_array_incref ("products", oc->merge_inventory.products), GNUNET_JSON_pack_data_auto ("h_wire", @@ -3313,9 +3322,11 @@ uint64_cmp (const void *a, static void phase_merge_inventory (struct OrderContext *oc) { - uint64_t pots[GNUNET_NZL (oc->parse_order.products_len)]; + uint64_t pots[oc->parse_order.products_len + 1]; size_t pots_off = 0; + if (0 != oc->parse_order.order_default_money_pot) + pots[pots_off++] = oc->parse_order.order_default_money_pot; /** * parse_request.inventory_products => instructions to add products to contract terms * parse_order.products => contains products that are not from the backend-managed inventory. @@ -3954,6 +3965,10 @@ phase_parse_order (struct OrderContext *oc) GNUNET_JSON_spec_object_const ("extra", &oc->parse_order.extra), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("order_default_money_pot", + &oc->parse_order.order_default_money_pot), + NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue ret; diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -114,6 +114,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_helper.h pg_helper.c \ pg_inactivate_account.h pg_inactivate_account.c \ pg_increase_refund.h pg_increase_refund.c \ + pg_increment_money_pots.h pg_increment_money_pots.c \ pg_insert_account.h pg_insert_account.c \ pg_insert_category.h pg_insert_category.c \ pg_insert_unit.h pg_insert_unit.c \ diff --git a/src/backenddb/pg_check_money_pots.c b/src/backenddb/pg_check_money_pots.c @@ -46,6 +46,7 @@ TMH_PG_check_money_pots (void *cls, pot_missing), GNUNET_PQ_result_spec_end }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, @@ -61,9 +62,11 @@ TMH_PG_check_money_pots (void *cls, " AND mi.merchant_id=$1" " )" " LIMIT 1;"); - return GNUNET_PQ_eval_prepared_singleton_select ( + qs = GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, "check_money_pots", params, rs); + GNUNET_PQ_cleanup_query_params_closures (params); + return qs; } diff --git a/src/backenddb/pg_increment_money_pots.c b/src/backenddb/pg_increment_money_pots.c @@ -0,0 +1,76 @@ +/* + 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 backenddb/pg_increment_money_pots.c + * @brief Implementation of the increment_money_pots function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_increment_money_pots.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_increment_money_pots ( + void *cls, + const char *instance_id, + size_t money_pots_len, + const uint64_t *money_pot_ids, + const struct TALER_Amount *pot_increments) +{ + struct PostgresClosure *pg = cls; + 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), + TALER_PQ_query_param_array_amount_with_currency (money_pots_len, + pot_increments, + pg->conn), + GNUNET_PQ_query_param_end + }; + bool not_found; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("not_found", + ¬_found), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "increment_money_pots", + "SELECT" + " out_not_found AS not_found" + " FROM merchant_do_increment_money_pots" + "($1,$2,$3);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "increment_money_pots", + params, + rs); + GNUNET_PQ_cleanup_query_params_closures (params); + if (qs <= 0) + return qs; + if (not_found) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Some money pots could not be implemented because they no longer exist. This is not a bug and expected to happen if a merchant deletes money pots that were used in orders active at the time.\n"); + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +} diff --git a/src/backenddb/pg_increment_money_pots.h b/src/backenddb/pg_increment_money_pots.h @@ -0,0 +1,49 @@ +/* + 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 backenddb/pg_increment_money_pots.h + * @brief implementation of the increment_money_pots function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_INCREMENT_MONEY_POTS_H +#define PG_INCREMENT_MONEY_POTS_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Increment amounts in money pots. If a money pot does not exist, + * it is simply skipped (without causing an error!). + * + * @param cls closure + * @param instance_id instance to update money pot for + * @param money_pots_len length of the @a money_pots_ids + * and @a pot_increments arrays + * @param money_pot_ids serial numbers of the pots to increment + * @param pot_increments new amounts to add to the respective pot + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_increment_money_pots ( + void *cls, + const char *instance_id, + size_t money_pots_len, + const uint64_t *money_pot_ids, + const struct TALER_Amount *pot_increments); + + +#endif diff --git a/src/backenddb/pg_increment_money_pots.sql b/src/backenddb/pg_increment_money_pots.sql @@ -0,0 +1,109 @@ +-- +-- 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..array_length(ina_money_pots_ids, 1) + 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..array_length(my_totals, 1) + LOOP + IF (my_totals[j]).currency = (ini_current_increment).currency + 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_update_money_pot.c b/src/backenddb/pg_update_money_pot.c @@ -83,6 +83,7 @@ TMH_PG_update_money_pot ( "update_money_pot", params, rs); + GNUNET_PQ_cleanup_query_params_closures (params); if (qs <= 0) return qs; if (not_found) diff --git a/src/backenddb/pg_update_product_group.c b/src/backenddb/pg_update_product_group.c @@ -59,7 +59,7 @@ TMH_PG_update_product_group ( "update_product_group", "SELECT" " out_conflict AS conflict" - " out_not_found AS not_found" + ",out_not_found AS not_found" " FROM merchant_do_update_product_group" "($1,$2,$3,$4);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -117,6 +117,7 @@ #include "pg_lookup_orders.h" #include "pg_insert_order.h" #include "pg_insert_order_blinded_sigs.h" +#include "pg_increment_money_pots.h" #include "pg_unlock_inventory.h" #include "pg_insert_order_lock.h" #include "pg_select_order_blinded_sigs.h" @@ -779,6 +780,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_lookup_donau_keys; plugin->lookup_order_charity = &TMH_PG_lookup_order_charity; + plugin->increment_money_pots + = &TMH_PG_increment_money_pots; plugin->upsert_donau_keys = &TMH_PG_upsert_donau_keys; plugin->update_donau_instance diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in @@ -32,6 +32,7 @@ SET search_path TO merchant; #include "pg_do_handle_category_changes.sql" #include "pg_update_product_group.sql" #include "pg_update_money_pot.sql" +#include "pg_increment_money_pots.sql" DROP PROCEDURE IF EXISTS merchant_do_gc; CREATE PROCEDURE merchant_do_gc(in_now INT8) diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -821,7 +821,6 @@ struct TALER_MERCHANT_Contract */ struct GNUNET_TIME_Timestamp delivery_date; - /** * Merchant public key. */ @@ -866,6 +865,15 @@ struct TALER_MERCHANT_Contract uint8_t minimum_age; /** + * Default money pot to use for this product, applies to the + * amount remaining that was not claimed by money pots of + * products or taxes. Not useful to wallets, only for + * merchant-internal accounting. If zero, the remaining + * account is simply not accounted for in any money pot. + */ + uint64_t default_money_pot; + + /** * Specified version of the contract. */ enum TALER_MERCHANT_ContractVersion version; diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -5056,6 +5056,27 @@ struct TALER_MERCHANTDB_Plugin /** + * Increment amounts in money pots. If a money pot does not exist, + * it is simply skipped (without causing an error!). + * + * @param cls closure + * @param instance_id instance to update money pot for + * @param money_pots_len length of the @a money_pots_ids + * and @a pot_increments arrays + * @param money_pot_ids serial numbers of the pots to increment + * @param pot_increments new amounts to add to the respective pot + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*increment_money_pots)( + void *cls, + const char *instance_id, + size_t money_pots_len, + const uint64_t *money_pot_ids, + const struct TALER_Amount *pot_increments); + + + /** * Insert details about a particular pot. * * @param cls closure diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c @@ -1276,6 +1276,10 @@ TALER_MERCHANT_contract_parse (json_t *input, GNUNET_JSON_spec_uint8 ("minimum_age", &contract->minimum_age), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("default_money_pot", + &contract->default_money_pot), + NULL), GNUNET_JSON_spec_end () }; diff --git a/src/util/contract_serialize.c b/src/util/contract_serialize.c @@ -537,6 +537,12 @@ success: GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_uint64 ("minimum_age", input->minimum_age)), + (0 == input->default_money_pot) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("dummy", + NULL)) + : GNUNET_JSON_pack_uint64 ("default_money_pot", + input->default_money_pot), GNUNET_JSON_pack_object_steal (NULL, details)); }