/*
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_authorize_reward.c
* @brief Implementation of the authorize_reward function for Postgres
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include
#include
#include "pg_authorize_reward.h"
#include "pg_helper.h"
/**
* How often do we re-try if we run into a DB serialization error?
*/
#define MAX_RETRIES 3
/**
* Closure for #lookup_reserve_for_reward_cb().
*/
struct LookupReserveForRewardContext
{
/**
* Postgres context.
*/
struct PostgresClosure *pg;
/**
* Public key of the reserve we found.
*/
struct TALER_ReservePublicKeyP reserve_pub;
/**
* How much money must be left in the reserve.
*/
struct TALER_Amount required_amount;
/**
* Set to the expiration time of the reserve we found.
* #GNUNET_TIME_UNIT_FOREVER_ABS if we found none.
*/
struct GNUNET_TIME_Timestamp expiration;
/**
* Error status.
*/
enum TALER_ErrorCode ec;
/**
* Did we find a good reserve?
*/
bool ok;
};
/**
* How long must a reserve be at least still valid before we use
* it for a reward?
*/
#define MIN_EXPIRATION GNUNET_TIME_UNIT_HOURS
/**
* Function to be called with the results of a SELECT statement
* that has returned @a num_results results about accounts.
*
* @param[in,out] cls of type `struct LookupReserveForRewardContext *`
* @param result the postgres result
* @param num_results the number of results in @a result
*/
static void
lookup_reserve_for_reward_cb (void *cls,
PGresult *result,
unsigned int num_results)
{
struct LookupReserveForRewardContext *lac = cls;
for (unsigned int i = 0; i < num_results; i++)
{
struct TALER_ReservePublicKeyP reserve_pub;
struct TALER_Amount committed_amount;
struct TALER_Amount remaining;
struct TALER_Amount initial_balance;
struct GNUNET_TIME_Timestamp expiration;
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_auto_from_type ("reserve_pub",
&reserve_pub),
TALER_PQ_result_spec_amount_with_currency ("exchange_initial_balance",
&initial_balance),
TALER_PQ_result_spec_amount_with_currency ("rewards_committed",
&committed_amount),
GNUNET_PQ_result_spec_timestamp ("expiration",
&expiration),
GNUNET_PQ_result_spec_end
};
if (GNUNET_OK !=
GNUNET_PQ_extract_result (result,
rs,
i))
{
GNUNET_break (0);
lac->ec = TALER_EC_GENERIC_DB_FETCH_FAILED;
return;
}
if ( (GNUNET_YES !=
TALER_amount_cmp_currency (&initial_balance,
&committed_amount)) ||
(GNUNET_YES !=
TALER_amount_cmp_currency (&initial_balance,
&lac->required_amount)) )
{
/* insufficient balance */
if (lac->ok)
continue; /* got another reserve */
lac->ec = TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH;
continue;
}
if (0 >
TALER_amount_subtract (&remaining,
&initial_balance,
&committed_amount))
{
GNUNET_break (0);
continue;
}
if (0 >
TALER_amount_cmp (&remaining,
&lac->required_amount))
{
/* insufficient balance */
if (lac->ok)
continue; /* got another reserve */
lac->ec =
TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS;
continue;
}
if ( (! GNUNET_TIME_absolute_is_never (lac->expiration.abs_time)) &&
GNUNET_TIME_timestamp_cmp (expiration,
>,
lac->expiration) &&
GNUNET_TIME_relative_cmp (
GNUNET_TIME_absolute_get_remaining (lac->expiration.abs_time),
>,
MIN_EXPIRATION) )
{
/* reserve expired */
if (lac->ok)
continue; /* got another reserve */
lac->ec = TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED;
continue;
}
lac->ok = true;
lac->ec = TALER_EC_NONE;
lac->expiration = expiration;
lac->reserve_pub = reserve_pub;
}
}
enum TALER_ErrorCode
TMH_PG_authorize_reward (void *cls,
const char *instance_id,
const struct TALER_ReservePublicKeyP *reserve_pub,
const struct TALER_Amount *amount,
const char *justification,
const char *next_url,
struct TALER_RewardIdentifierP *reward_id,
struct GNUNET_TIME_Timestamp *expiration)
{
struct PostgresClosure *pg = cls;
unsigned int retries = 0;
enum GNUNET_DB_QueryStatus qs;
struct TALER_Amount rewards_committed;
struct TALER_Amount exchange_initial_balance;
const struct TALER_ReservePublicKeyP *reserve_pubp;
struct LookupReserveForRewardContext lac = {
.pg = pg,
.required_amount = *amount,
.expiration = GNUNET_TIME_UNIT_FOREVER_TS
};
check_connection (pg);
PREPARE (pg,
"lookup_reserve_for_reward",
"SELECT"
" reserve_pub"
",expiration"
",exchange_initial_balance"
",rewards_committed"
" FROM merchant_reward_reserves"
" WHERE"
" merchant_serial ="
" (SELECT merchant_serial"
" FROM merchant_instances"
" WHERE merchant_id=$1)");
PREPARE (pg,
"lookup_reserve_status",
"SELECT"
" expiration"
",exchange_initial_balance"
",rewards_committed"
" FROM merchant_reward_reserves"
" WHERE reserve_pub = $2"
" AND merchant_serial ="
" (SELECT merchant_serial"
" FROM merchant_instances"
" WHERE merchant_id=$1)");
PREPARE (pg,
"update_reserve_rewards_committed",
"UPDATE merchant_reward_reserves SET"
" rewards_committed=$3"
" WHERE reserve_pub=$2"
" AND merchant_serial ="
" (SELECT merchant_serial"
" FROM merchant_instances"
" WHERE merchant_id=$1)");
PREPARE (pg,
"insert_reward",
"INSERT INTO merchant_rewards"
"(reserve_serial"
",reward_id"
",justification"
",next_url"
",expiration"
",amount"
",picked_up"
") "
"SELECT"
" reserve_serial, $3, $4, $5, $6, $7, $8"
" FROM merchant_reward_reserves"
" WHERE reserve_pub=$2"
" AND merchant_serial = "
" (SELECT merchant_serial"
" FROM merchant_instances"
" WHERE merchant_id=$1)");
RETRY:
reserve_pubp = reserve_pub;
if (MAX_RETRIES < ++retries)
{
GNUNET_break (0);
return
TALER_EC_GENERIC_DB_SOFT_FAILURE;
}
if (GNUNET_OK !=
TMH_PG_start (pg,
"authorize reward"))
{
GNUNET_break (0);
return TALER_EC_GENERIC_DB_START_FAILED;
}
if (NULL == reserve_pubp)
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_string (instance_id),
GNUNET_PQ_query_param_end
};
qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
"lookup_reserve_for_reward",
params,
&lookup_reserve_for_reward_cb,
&lac);
switch (qs)
{
case GNUNET_DB_STATUS_SOFT_ERROR:
TMH_PG_rollback (pg);
goto RETRY;
case GNUNET_DB_STATUS_HARD_ERROR:
GNUNET_break (0);
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_DB_FETCH_FAILED;
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
TMH_PG_rollback (pg);
return TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
default:
break;
}
if (TALER_EC_NONE != lac.ec)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Enabling reward reserved failed with status %d\n",
lac.ec);
TMH_PG_rollback (pg);
return lac.ec;
}
GNUNET_assert (lac.ok);
reserve_pubp = &lac.reserve_pub;
}
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_string (instance_id),
GNUNET_PQ_query_param_auto_from_type (reserve_pubp),
GNUNET_PQ_query_param_end
};
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_timestamp ("expiration",
expiration),
TALER_PQ_result_spec_amount_with_currency ("rewards_committed",
&rewards_committed),
TALER_PQ_result_spec_amount_with_currency ("exchange_initial_balance",
&exchange_initial_balance),
GNUNET_PQ_result_spec_end
};
qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"lookup_reserve_status",
params,
rs);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
TMH_PG_rollback (pg);
goto RETRY;
}
if (qs < 0)
{
GNUNET_break (0);
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_DB_FETCH_FAILED;
}
if (0 == qs)
{
TMH_PG_rollback (pg);
return TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND;
}
}
{
struct TALER_Amount remaining;
if (0 >
TALER_amount_subtract (&remaining,
&exchange_initial_balance,
&rewards_committed))
{
GNUNET_break (0);
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_DB_INVARIANT_FAILURE;
}
if (GNUNET_YES !=
TALER_amount_cmp_currency (&remaining,
amount))
{
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_CURRENCY_MISMATCH;
}
if (0 >
TALER_amount_cmp (&remaining,
amount))
{
TMH_PG_rollback (pg);
return TALER_EC_MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS;
}
}
GNUNET_assert (0 <=
TALER_amount_add (&rewards_committed,
&rewards_committed,
amount));
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_string (instance_id),
GNUNET_PQ_query_param_auto_from_type (reserve_pubp),
TALER_PQ_query_param_amount_with_currency (pg->conn,
&rewards_committed),
GNUNET_PQ_query_param_end
};
qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
"update_reserve_rewards_committed",
params);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
TMH_PG_rollback (pg);
goto RETRY;
}
if (qs < 0)
{
GNUNET_break (0);
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_DB_STORE_FAILED;
}
}
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
reward_id,
sizeof (*reward_id));
{
struct TALER_Amount zero;
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_string (instance_id),
GNUNET_PQ_query_param_auto_from_type (reserve_pubp),
GNUNET_PQ_query_param_auto_from_type (reward_id),
GNUNET_PQ_query_param_string (justification),
GNUNET_PQ_query_param_string (next_url),
GNUNET_PQ_query_param_timestamp (expiration),
TALER_PQ_query_param_amount_with_currency (pg->conn,
amount),
TALER_PQ_query_param_amount_with_currency (pg->conn,
&zero),
GNUNET_PQ_query_param_end
};
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (amount->currency,
&zero));
qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
"insert_reward",
params);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
TMH_PG_rollback (pg);
goto RETRY;
}
if (qs < 0)
{
GNUNET_break (0);
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_DB_STORE_FAILED;
}
}
qs = TMH_PG_commit (pg);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
goto RETRY;
if (qs < 0)
{
GNUNET_break (0);
TMH_PG_rollback (pg);
return TALER_EC_GENERIC_DB_COMMIT_FAILED;
}
return TALER_EC_NONE;
}