summaryrefslogtreecommitdiff
path: root/src/backenddb/pg_increase_refund.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backenddb/pg_increase_refund.c')
-rw-r--r--src/backenddb/pg_increase_refund.c507
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,
+ &current_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 (&current_refund,
+ &current_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 (&current_refund));
+
+ /* stop immediately if we are 'done' === amount already
+ * refunded. */
+ if (0 >= TALER_amount_cmp (ctx->refund,
+ &current_refund))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Existing refund of %s at or above requested refund. Finished early.\n",
+ TALER_amount2s (&current_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,
+ &current_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 (&current_refund,
+ &current_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,
+ &current_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;
+ }
+}