/* 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 */ /** * @file backenddb/pg_increase_refund.c * @brief Implementation of the increase_refund function for Postgres * @author Christian Grothoff */ #include "platform.h" #include #include #include #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; ierr = 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; irs = 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 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; } }