/*
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;
}
}