diff options
Diffstat (limited to 'src/backenddb/pg_increase_refund.c')
-rw-r--r-- | src/backenddb/pg_increase_refund.c | 507 |
1 files changed, 507 insertions, 0 deletions
diff --git a/src/backenddb/pg_increase_refund.c b/src/backenddb/pg_increase_refund.c new file mode 100644 index 00000000..eef7adc6 --- /dev/null +++ b/src/backenddb/pg_increase_refund.c @@ -0,0 +1,507 @@ +/* + 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 backenddb/pg_increase_refund.c + * @brief Implementation of the increase_refund 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_increase_refund.h" +#include "pg_helper.h" + + +/** + * Closure for #process_refund_cb(). + */ +struct FindRefundContext +{ + + /** + * Plugin context. + */ + struct PostgresClosure *pg; + + /** + * Updated to reflect total amount refunded so far. + */ + struct TALER_Amount refunded_amount; + + /** + * Set to the largest refund transaction ID encountered. + */ + uint64_t max_rtransaction_id; + + /** + * Set to true on hard errors. + */ + bool err; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure, our `struct FindRefundContext` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +process_refund_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct FindRefundContext *ictx = cls; + + for (unsigned int i = 0; i<num_results; i++) + { + /* Sum up existing refunds */ + struct TALER_Amount acc; + uint64_t rtransaction_id; + struct GNUNET_PQ_ResultSpec rs[] = { + TALER_PQ_result_spec_amount_with_currency ("refund_amount", + &acc), + GNUNET_PQ_result_spec_uint64 ("rtransaction_id", + &rtransaction_id), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + ictx->err = true; + return; + } + if (GNUNET_OK != + TALER_amount_cmp_currency (&ictx->refunded_amount, + &acc)) + { + GNUNET_break (0); + ictx->err = true; + return; + } + if (0 > + TALER_amount_add (&ictx->refunded_amount, + &ictx->refunded_amount, + &acc)) + { + GNUNET_break (0); + ictx->err = true; + return; + } + ictx->max_rtransaction_id = GNUNET_MAX (ictx->max_rtransaction_id, + rtransaction_id); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Found refund of %s\n", + TALER_amount2s (&acc)); + } +} + + +/** + * Closure for #process_deposits_for_refund_cb(). + */ +struct InsertRefundContext +{ + /** + * Used to provide a connection to the db + */ + struct PostgresClosure *pg; + + /** + * Amount to which increase the refund for this contract + */ + const struct TALER_Amount *refund; + + /** + * Human-readable reason behind this refund + */ + const char *reason; + + /** + * Transaction status code. + */ + enum TALER_MERCHANTDB_RefundStatus rs; +}; + + +/** + * Data extracted per coin. + */ +struct RefundCoinData +{ + + /** + * Public key of a coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Amount deposited for this coin. + */ + struct TALER_Amount deposited_with_fee; + + /** + * Amount refunded already for this coin. + */ + struct TALER_Amount refund_amount; + + /** + * Order serial (actually not really per-coin). + */ + uint64_t order_serial; + + /** + * Maximum rtransaction_id for this coin so far. + */ + uint64_t max_rtransaction_id; + +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls closure, our `struct InsertRefundContext` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +process_deposits_for_refund_cb ( + void *cls, + PGresult *result, + unsigned int num_results) +{ + struct InsertRefundContext *ctx = cls; + struct PostgresClosure *pg = ctx->pg; + struct TALER_Amount current_refund; + struct RefundCoinData rcd[GNUNET_NZL (num_results)]; + struct GNUNET_TIME_Timestamp now; + + now = GNUNET_TIME_timestamp_get (); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (ctx->refund->currency, + ¤t_refund)); + memset (rcd, 0, sizeof (rcd)); + /* Pass 1: Collect amount of existing refunds into current_refund. + * Also store existing refunded amount for each deposit in deposit_refund. */ + for (unsigned int i = 0; i<num_results; i++) + { + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("coin_pub", + &rcd[i].coin_pub), + GNUNET_PQ_result_spec_uint64 ("order_serial", + &rcd[i].order_serial), + TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", + &rcd[i].deposited_with_fee), + GNUNET_PQ_result_spec_end + }; + struct FindRefundContext ictx = { + .pg = pg + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + } + + if (0 != strcmp (rcd[i].deposited_with_fee.currency, + ctx->refund->currency)) + { + GNUNET_break_op (0); + ctx->rs = TALER_MERCHANTDB_RS_BAD_CURRENCY; + return; + } + + { + enum GNUNET_DB_QueryStatus ires; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (&rcd[i].coin_pub), + GNUNET_PQ_query_param_uint64 (&rcd[i].order_serial), + GNUNET_PQ_query_param_end + }; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (ctx->refund->currency, + &ictx.refunded_amount)); + ires = GNUNET_PQ_eval_prepared_multi_select (ctx->pg->conn, + "find_refunds_by_coin", + params, + &process_refund_cb, + &ictx); + if ( (ictx.err) || + (GNUNET_DB_STATUS_HARD_ERROR == ires) ) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + } + if (GNUNET_DB_STATUS_SOFT_ERROR == ires) + { + ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; + return; + } + } + if (0 > + TALER_amount_add (¤t_refund, + ¤t_refund, + &ictx.refunded_amount)) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + } + rcd[i].refund_amount = ictx.refunded_amount; + rcd[i].max_rtransaction_id = ictx.max_rtransaction_id; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Existing refund for coin %s is %s\n", + TALER_B2S (&rcd[i].coin_pub), + TALER_amount2s (&ictx.refunded_amount)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Total existing refund is %s\n", + TALER_amount2s (¤t_refund)); + + /* stop immediately if we are 'done' === amount already + * refunded. */ + if (0 >= TALER_amount_cmp (ctx->refund, + ¤t_refund)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Existing refund of %s at or above requested refund. Finished early.\n", + TALER_amount2s (¤t_refund)); + ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; + return; + } + + /* Phase 2: Try to increase current refund until it matches desired refund */ + for (unsigned int i = 0; i<num_results; i++) + { + const struct TALER_Amount *increment; + struct TALER_Amount left; + struct TALER_Amount remaining_refund; + + /* How much of the coin is left after the existing refunds? */ + if (0 > + TALER_amount_subtract (&left, + &rcd[i].deposited_with_fee, + &rcd[i].refund_amount)) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + } + + if ( (0 == left.value) && + (0 == left.fraction) ) + { + /* coin was fully refunded, move to next coin */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Coin %s fully refunded, moving to next coin\n", + TALER_B2S (&rcd[i].coin_pub)); + continue; + } + + rcd[i].max_rtransaction_id++; + /* How much of the refund is still to be paid back? */ + if (0 > + TALER_amount_subtract (&remaining_refund, + ctx->refund, + ¤t_refund)) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + } + + /* By how much will we increase the refund for this coin? */ + if (0 >= TALER_amount_cmp (&remaining_refund, + &left)) + { + /* remaining_refund <= left */ + increment = &remaining_refund; + } + else + { + increment = &left; + } + + if (0 > + TALER_amount_add (¤t_refund, + ¤t_refund, + increment)) + { + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + } + + /* actually run the refund */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Coin %s deposit amount is %s\n", + TALER_B2S (&rcd[i].coin_pub), + TALER_amount2s (&rcd[i].deposited_with_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Coin %s refund will be incremented by %s\n", + TALER_B2S (&rcd[i].coin_pub), + TALER_amount2s (increment)); + { + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_uint64 (&rcd[i].order_serial), + GNUNET_PQ_query_param_uint64 (&rcd[i].max_rtransaction_id), /* already inc'ed */ + GNUNET_PQ_query_param_timestamp (&now), + GNUNET_PQ_query_param_auto_from_type (&rcd[i].coin_pub), + GNUNET_PQ_query_param_string (ctx->reason), + TALER_PQ_query_param_amount_with_currency (pg->conn, + increment), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "insert_refund", + params); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR; + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR; + return; + default: + ctx->rs = (enum TALER_MERCHANTDB_RefundStatus) qs; + break; + } + } + + /* stop immediately if we are done */ + if (0 == TALER_amount_cmp (ctx->refund, + ¤t_refund)) + { + ctx->rs = TALER_MERCHANTDB_RS_SUCCESS; + return; + } + } + + /** + * We end up here if not all of the refund has been covered. + * Although this should be checked as the business should never + * issue a refund bigger than the contract's actual price, we cannot + * rely upon the frontend being correct. + */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "The refund of %s is bigger than the order's value\n", + TALER_amount2s (ctx->refund)); + ctx->rs = TALER_MERCHANTDB_RS_TOO_HIGH; +} + + +enum TALER_MERCHANTDB_RefundStatus +TMH_PG_increase_refund (void *cls, + const char *instance_id, + const char *order_id, + const struct TALER_Amount *refund, + const char *reason) +{ + struct PostgresClosure *pg = cls; + 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 InsertRefundContext ctx = { + .pg = pg, + .refund = refund, + .reason = reason + }; + + 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" + " 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_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); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* never paid, means we clearly cannot refund anything */ + return TALER_MERCHANTDB_RS_NO_SUCH_ORDER; + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MERCHANTDB_RS_SOFT_ERROR; + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MERCHANTDB_RS_HARD_ERROR; + default: + /* Got one or more deposits */ + return ctx.rs; + } +} |