summaryrefslogtreecommitdiff
path: root/src/exchange/taler-exchange-httpd_refund.c
diff options
context:
space:
mode:
authorChristian Grothoff <christian@grothoff.org>2017-06-19 16:07:34 +0200
committerChristian Grothoff <christian@grothoff.org>2017-06-19 16:07:34 +0200
commit92e6744ac032a3c4c4118ac6b251f769c5478aa6 (patch)
tree9098913719d9b1a70d0ed8d8a0ba8e500c96768a /src/exchange/taler-exchange-httpd_refund.c
parent3d701e8d2a39e766b8345e242c3ffb501f935a3e (diff)
downloadexchange-92e6744ac032a3c4c4118ac6b251f769c5478aa6.tar.gz
exchange-92e6744ac032a3c4c4118ac6b251f769c5478aa6.tar.bz2
exchange-92e6744ac032a3c4c4118ac6b251f769c5478aa6.zip
address #5010 for /refund
Diffstat (limited to 'src/exchange/taler-exchange-httpd_refund.c')
-rw-r--r--src/exchange/taler-exchange-httpd_refund.c357
1 files changed, 354 insertions, 3 deletions
diff --git a/src/exchange/taler-exchange-httpd_refund.c b/src/exchange/taler-exchange-httpd_refund.c
index a5787eea..9846c73b 100644
--- a/src/exchange/taler-exchange-httpd_refund.c
+++ b/src/exchange/taler-exchange-httpd_refund.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2014, 2015, 2016 Inria and GNUnet e.V.
+ Copyright (C) 2014-2017 Inria and GNUnet e.V.
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
@@ -37,6 +37,350 @@
/**
+ * Generate successful refund confirmation message.
+ *
+ * @param connection connection to the client
+ * @param refund details about the successful refund
+ * @return MHD result code
+ */
+static int
+reply_refund_success (struct MHD_Connection *connection,
+ const struct TALER_EXCHANGEDB_Refund *refund)
+{
+ struct TALER_RefundConfirmationPS rc;
+ struct TALER_ExchangePublicKeyP pub;
+ struct TALER_ExchangeSignatureP sig;
+
+ rc.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND);
+ rc.purpose.size = htonl (sizeof (struct TALER_RefundConfirmationPS));
+ rc.h_contract_terms = refund->h_contract_terms;
+ rc.coin_pub = refund->coin.coin_pub;
+ rc.merchant = refund->merchant_pub;
+ rc.rtransaction_id = GNUNET_htonll (refund->rtransaction_id);
+ TALER_amount_hton (&rc.refund_amount,
+ &refund->refund_amount);
+ TALER_amount_hton (&rc.refund_fee,
+ &refund->refund_fee);
+ TEH_KS_sign (&rc.purpose,
+ &pub,
+ &sig);
+ return TEH_RESPONSE_reply_json_pack (connection,
+ MHD_HTTP_OK,
+ "{s:s, s:o, s:o}",
+ "status", "REFUND_OK",
+ "sig", GNUNET_JSON_from_data_auto (&sig),
+ "pub", GNUNET_JSON_from_data_auto (&pub));
+}
+
+
+/**
+ * Generate generic refund failure message. All the details
+ * are in the @a response_code. The body can be empty.
+ *
+ * @param connection connection to the client
+ * @param response_code response code to generate
+ * @param ec taler error code to include
+ * @return MHD result code
+ */
+static int
+reply_refund_failure (struct MHD_Connection *connection,
+ unsigned int response_code,
+ enum TALER_ErrorCode ec)
+{
+ return TEH_RESPONSE_reply_json_pack (connection,
+ response_code,
+ "{s:s, s:I}",
+ "status", "refund failure",
+ "code", (json_int_t) ec);
+}
+
+
+/**
+ * Generate refund conflict failure message. Returns the
+ * transaction list @a tl with the details about the conflict.
+ *
+ * @param connection connection to the client
+ * @param tl transaction list showing the conflict
+ * @return MHD result code
+ */
+static int
+reply_refund_conflict (struct MHD_Connection *connection,
+ const struct TALER_EXCHANGEDB_TransactionList *tl)
+{
+ return TEH_RESPONSE_reply_json_pack (connection,
+ MHD_HTTP_CONFLICT,
+ "{s:s, s:I, s:o}",
+ "status", "conflicting refund",
+ "code", (json_int_t) TALER_EC_REFUND_CONFLICT,
+ "history", TEH_RESPONSE_compile_transaction_history (tl));
+}
+
+
+/**
+ * Execute a "/refund" transaction. Returns a confirmation that the
+ * refund was successful, or a failure if we are not aware of a
+ * matching /deposit or if it is too late to do the refund.
+ *
+ * IF it returns a non-error code, the transaction logic MUST
+ * NOT queue a MHD response. IF it returns an hard error, the
+ * transaction logic MUST queue a MHD response and set @a mhd_ret. IF
+ * it returns the soft error code, the function MAY be called again to
+ * retry and MUST not queue a MHD response.
+ *
+ * @param cls closure with a `const struct TALER_EXCHANGEDB_Refund *`
+ * @param connection MHD request which triggered the transaction
+ * @param session database session to use
+ * @param[out] mhd_ret set to MHD response status for @a connection,
+ * if transaction failed (!)
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+refund_transaction (void *cls,
+ struct MHD_Connection *connection,
+ struct TALER_EXCHANGEDB_Session *session,
+ int *mhd_ret)
+{
+ const struct TALER_EXCHANGEDB_Refund *refund = cls;
+ struct TALER_EXCHANGEDB_TransactionList *tl;
+ const struct TALER_EXCHANGEDB_Deposit *dep;
+ const struct TALER_EXCHANGEDB_Refund *ref;
+ struct TEH_KS_StateHandle *mks;
+ struct TALER_EXCHANGEDB_DenominationKeyIssueInformation *dki;
+ struct TALER_Amount expect_fee;
+ enum GNUNET_DB_QueryStatus qs;
+ int deposit_found;
+ int refund_found;
+ int fee_cmp;
+
+ dep = NULL;
+ ref = NULL;
+ tl = NULL;
+ qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls,
+ session,
+ &refund->coin.coin_pub,
+ &tl);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ *mhd_ret = reply_refund_failure (connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_REFUND_COIN_NOT_FOUND);
+ return qs;
+ }
+ deposit_found = GNUNET_NO;
+ refund_found = GNUNET_NO;
+ for (struct TALER_EXCHANGEDB_TransactionList *tlp = tl;
+ NULL != tlp;
+ tlp = tlp->next)
+ {
+ switch (tlp->type)
+ {
+ case TALER_EXCHANGEDB_TT_DEPOSIT:
+ if (GNUNET_NO == deposit_found)
+ {
+ if ( (0 == memcmp (&tlp->details.deposit->merchant_pub,
+ &refund->merchant_pub,
+ sizeof (struct TALER_MerchantPublicKeyP))) &&
+ (0 == memcmp (&tlp->details.deposit->h_contract_terms,
+ &refund->h_contract_terms,
+ sizeof (struct GNUNET_HashCode))) )
+ {
+ dep = tlp->details.deposit;
+ deposit_found = GNUNET_YES;
+ break;
+ }
+ }
+ break;
+ case TALER_EXCHANGEDB_TT_REFRESH_MELT:
+ /* Melts cannot be refunded, ignore here */
+ break;
+ case TALER_EXCHANGEDB_TT_REFUND:
+ if (GNUNET_NO == refund_found)
+ {
+ /* First, check if existing refund request is identical */
+ if ( (0 == memcmp (&tlp->details.refund->merchant_pub,
+ &refund->merchant_pub,
+ sizeof (struct TALER_MerchantPublicKeyP))) &&
+ (0 == memcmp (&tlp->details.refund->h_contract_terms,
+ &refund->h_contract_terms,
+ sizeof (struct GNUNET_HashCode))) &&
+ (tlp->details.refund->rtransaction_id == refund->rtransaction_id) )
+ {
+ ref = tlp->details.refund;
+ refund_found = GNUNET_YES;
+ break;
+ }
+ /* Second, check if existing refund request conflicts */
+ if ( (0 == memcmp (&tlp->details.refund->merchant_pub,
+ &refund->merchant_pub,
+ sizeof (struct TALER_MerchantPublicKeyP))) &&
+ (0 == memcmp (&tlp->details.refund->h_contract_terms,
+ &refund->h_contract_terms,
+ sizeof (struct GNUNET_HashCode))) &&
+ (tlp->details.refund->rtransaction_id != refund->rtransaction_id) )
+ {
+ GNUNET_break_op (0); /* conflicting refund found */
+ refund_found = GNUNET_SYSERR;
+ /* NOTE: Alternatively we could total up all existing
+ refunds and check if the sum still permits the
+ refund requested (thus allowing multiple, partial
+ refunds). Fow now, we keep it simple. */
+ break;
+ }
+ }
+ break;
+ case TALER_EXCHANGEDB_TT_PAYBACK:
+ /* Paybacks cannot be refunded, ignore here */
+ break;
+ }
+ }
+ /* handle if deposit was NOT found */
+ if (GNUNET_NO == deposit_found)
+ {
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ *mhd_ret = TEH_RESPONSE_reply_transaction_unknown (connection,
+ TALER_EC_REFUND_DEPOSIT_NOT_FOUND);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ /* handle if conflicting refund found */
+ if (GNUNET_SYSERR == refund_found)
+ {
+ *mhd_ret = reply_refund_conflict (connection,
+ tl);
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ /* handle if identical refund found */
+ if (GNUNET_YES == refund_found)
+ {
+ /* /refund already done, simply re-transmit confirmation */
+ *mhd_ret = reply_refund_success (connection,
+ ref);
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ /* check currency is compatible */
+ if ( (GNUNET_YES !=
+ TALER_amount_cmp_currency (&refund->refund_amount,
+ &dep->amount_with_fee)) ||
+ (GNUNET_YES !=
+ TALER_amount_cmp_currency (&refund->refund_fee,
+ &dep->deposit_fee)) )
+ {
+ GNUNET_break_op (0); /* currency missmatch */
+ *mhd_ret = reply_refund_failure (connection,
+ MHD_HTTP_PRECONDITION_FAILED,
+ TALER_EC_REFUND_CURRENCY_MISSMATCH);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ /* check if we already send the money for the /deposit */
+ // FIXME: DB API...
+ qs = TEH_plugin->test_deposit_done (TEH_plugin->cls,
+ session,
+ dep);
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ {
+ /* Internal error, we first had the deposit in the history,
+ but now it is gone? */
+ GNUNET_break (0);
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ *mhd_ret = TEH_RESPONSE_reply_internal_error (connection,
+ TALER_EC_REFUND_DB_INCONSISTENT,
+ "database inconsistent");
+ return qs;
+ }
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ return qs; /* go and retry */
+
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
+ {
+ /* money was already transferred to merchant, can no longer refund */
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ *mhd_ret = reply_refund_failure (connection,
+ MHD_HTTP_GONE,
+ TALER_EC_REFUND_MERCHANT_ALREADY_PAID);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ /* check refund amount is sufficiently low */
+ if (1 == TALER_amount_cmp (&refund->refund_amount,
+ &dep->amount_with_fee) )
+ {
+ GNUNET_break_op (0); /* cannot refund more than original value */
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ *mhd_ret = reply_refund_failure (connection,
+ MHD_HTTP_PRECONDITION_FAILED,
+ TALER_EC_REFUND_INSUFFICIENT_FUNDS);
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+
+ // FIXME: do this outside of transaction function?
+ /* Check refund fee matches fee of denomination key! */
+ mks = TEH_KS_acquire ();
+ dki = TEH_KS_denomination_key_lookup (mks,
+ &dep->coin.denom_pub,
+ TEH_KS_DKU_DEPOSIT);
+ if (NULL == dki)
+ {
+ /* DKI not found, but we do have a coin with this DK in our database;
+ not good... */
+ GNUNET_break (0);
+ TEH_KS_release (mks);
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ *mhd_ret = TEH_RESPONSE_reply_internal_error (connection,
+ TALER_EC_REFUND_DENOMINATION_KEY_NOT_FOUND,
+ "denomination key not found");
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ TALER_amount_ntoh (&expect_fee,
+ &dki->issue.properties.fee_refund);
+ fee_cmp = TALER_amount_cmp (&refund->refund_fee,
+ &expect_fee);
+ TEH_KS_release (mks);
+
+ if (-1 == fee_cmp)
+ {
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+ *mhd_ret = TEH_RESPONSE_reply_arg_invalid (connection,
+ TALER_EC_REFUND_FEE_TOO_LOW,
+ "refund_fee");
+ return GNUNET_DB_STATUS_HARD_ERROR;
+ }
+ if (1 == fee_cmp)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Refund fee proposed by merchant is higher than necessary.\n");
+ }
+ TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
+ tl);
+
+ /* Finally, store new refund data */
+ qs = TEH_plugin->insert_refund (TEH_plugin->cls,
+ session,
+ refund);
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs)
+ {
+ TALER_LOG_WARNING ("Failed to store /refund information in database\n");
+ *mhd_ret = TEH_RESPONSE_reply_internal_db_error (connection,
+ TALER_EC_REFUND_STORE_DB_ERROR);
+ return qs;
+ }
+ /* Success or soft failure */
+ return qs;
+}
+
+
+/**
* We have parsed the JSON information about the refund, do some basic
* sanity checks (especially that the signature on the coin is valid)
* and then execute the refund. Note that we need the DB to check
@@ -51,6 +395,7 @@ verify_and_execute_refund (struct MHD_Connection *connection,
const struct TALER_EXCHANGEDB_Refund *refund)
{
struct TALER_RefundRequestPS rr;
+ int mhd_ret;
rr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND);
rr.purpose.size = htonl (sizeof (struct TALER_RefundRequestPS));
@@ -90,8 +435,14 @@ verify_and_execute_refund (struct MHD_Connection *connection,
TALER_EC_REFUND_MERCHANT_SIGNATURE_INVALID,
"merchant_sig");
}
- return TEH_DB_execute_refund (connection,
- refund);
+ if (GNUNET_OK !=
+ TEH_DB_run_transaction (connection,
+ &mhd_ret,
+ &refund_transaction,
+ (void *) refund))
+ return mhd_ret;
+ return reply_refund_success (connection,
+ refund);
}