gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant] branch master updated: fix backend double-pay issue (#7


From: gnunet
Subject: [taler-merchant] branch master updated: fix backend double-pay issue (#7244)
Date: Mon, 06 Jun 2022 19:43:09 +0200

This is an automated email from the git hooks/post-receive script.

grothoff pushed a commit to branch master
in repository merchant.

The following commit(s) were added to refs/heads/master by this push:
     new 89d4f643 fix backend double-pay issue (#7244)
89d4f643 is described below

commit 89d4f6430956129d9bfb29551fc2fa1c5c147676
Author: Christian Grothoff <christian@grothoff.org>
AuthorDate: Mon Jun 6 19:42:46 2022 +0200

    fix backend double-pay issue (#7244)
---
 src/backend/taler-merchant-httpd_get-orders-ID.c   |  40 +-
 .../taler-merchant-httpd_post-orders-ID-claim.c    |   2 +
 .../taler-merchant-httpd_post-orders-ID-paid.c     |   2 +
 .../taler-merchant-httpd_post-orders-ID-pay.c      | 951 ++++++++++++---------
 .../taler-merchant-httpd_post-orders-ID-refund.c   |   2 +
 ...taler-merchant-httpd_private-delete-orders-ID.c |  57 +-
 .../taler-merchant-httpd_private-get-orders-ID.c   |  17 +-
 .../taler-merchant-httpd_private-get-orders.c      |   2 +
 ...merchant-httpd_private-patch-orders-ID-forget.c |   2 +
 ...-merchant-httpd_private-post-orders-ID-refund.c |   4 +
 src/backenddb/plugin_merchantdb_postgres.c         |  60 +-
 src/backenddb/test_merchantdb.c                    |   8 +-
 src/include/taler_merchant_service.h               |  43 +-
 src/include/taler_merchantdb_plugin.h              |   6 +-
 src/lib/merchant_api_delete_order.c                |  10 +-
 src/lib/merchant_api_post_order_pay.c              | 169 ++--
 src/testing/test_merchant_api.c                    |  36 +-
 src/testing/testing_api_cmd_delete_order.c         |   1 +
 src/testing/testing_api_cmd_pay_order.c            |  21 +-
 19 files changed, 891 insertions(+), 542 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c 
b/src/backend/taler-merchant-httpd_get-orders-ID.c
index 76dfefd5..910d53fc 100644
--- a/src/backend/taler-merchant-httpd_get-orders-ID.c
+++ b/src/backend/taler-merchant-httpd_get-orders-ID.c
@@ -764,17 +764,17 @@ process_refunds_cb (void *cls,
               TALER_B2S (coin_pub),
               reason);
   god->refund_pending |= pending;
-  if (!pending) 
-  {      
+  if (! pending)
+  {
     GNUNET_assert (0 <=
-                  TALER_amount_add (&god->refund_taken,
-                                    &god->refund_taken,
-                                    refund_amount));
+                   TALER_amount_add (&god->refund_taken,
+                                     &god->refund_taken,
+                                     refund_amount));
   }
   GNUNET_assert (0 <=
-                  TALER_amount_add (&god->refund_amount,
-                                    &god->refund_amount,
-                                    refund_amount));
+                 TALER_amount_add (&god->refund_amount,
+                                   &god->refund_amount,
+                                   refund_amount));
   god->refunded = true;
 }
 
@@ -1030,6 +1030,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
   if (NULL == god->contract_terms)
   {
     uint64_t order_serial;
+    bool paid = false;
     struct TALER_ClaimTokenP db_claim_token;
 
     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
@@ -1037,6 +1038,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
                                         order_id,
                                         &god->contract_terms,
                                         &order_serial,
+                                        &paid,
                                         &db_claim_token);
     if (0 > qs)
     {
@@ -1063,7 +1065,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
   {
     contract_available = true;
 
-    if (GNUNET_YES == GNUNET_is_zero (&god->h_contract_terms)) 
+    if (GNUNET_YES == GNUNET_is_zero (&god->h_contract_terms))
     {
 
       if (GNUNET_OK !=
@@ -1072,9 +1074,9 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
       {
         GNUNET_break (0);
         return TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
-                                          "contract terms");
+                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                           
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
+                                           "contract terms");
       }
 
     }
@@ -1089,14 +1091,14 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
       {
         GNUNET_break (0);
         return TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
-                                          "contract terms");
+                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                           
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
+                                           "contract terms");
       }
       contract_match = (0 ==
                         GNUNET_memcmp (&h,
-                                      &god->h_contract_terms));
-      if ( !contract_match ) 
+                                       &god->h_contract_terms));
+      if (! contract_match)
       {
         GNUNET_break_op (0);
         return TALER_MHD_reply_with_error (
@@ -1105,7 +1107,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
           TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
           NULL);
       }
-      
+
     }
 
   }
@@ -1509,7 +1511,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh,
     GNUNET_JSON_pack_bool ("refund_pending",
                            god->refund_pending),
     TALER_JSON_pack_amount ("refund_taken",
-                           &god->refund_taken),
+                            &god->refund_taken),
     TALER_JSON_pack_amount ("refund_amount",
                             &god->refund_amount));
 }
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c 
b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c
index 28231695..c806390a 100644
--- a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c
+++ b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c
@@ -62,6 +62,7 @@ claim_order (const char *instance_id,
   struct TALER_ClaimTokenP order_ct;
   enum GNUNET_DB_QueryStatus qs;
   uint64_t order_serial;
+  bool paid = false;
 
   if (GNUNET_OK !=
       TMH_db->start (TMH_db->cls,
@@ -75,6 +76,7 @@ claim_order (const char *instance_id,
                                       order_id,
                                       contract_terms,
                                       &order_serial,
+                                      &paid,
                                       NULL);
   if (0 > qs)
   {
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-paid.c 
b/src/backend/taler-merchant-httpd_post-orders-ID-paid.c
index bc8ccfd9..ee00b973 100644
--- a/src/backend/taler-merchant-httpd_post-orders-ID-paid.c
+++ b/src/backend/taler-merchant-httpd_post-orders-ID-paid.c
@@ -115,12 +115,14 @@ TMH_post_orders_ID_paid (const struct TMH_RequestHandler 
*rh,
   TMH_db->preflight (TMH_db->cls);
   {
     uint64_t order_serial;
+    bool paid;
 
     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                         hc->instance->settings.id,
                                         order_id,
                                         &contract_terms,
                                         &order_serial,
+                                        &paid,
                                         NULL);
   }
   if (0 > qs)
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c 
b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c
index c64eaebf..5104e850 100644
--- a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c
+++ b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c
@@ -1,6 +1,6 @@
 /*
    This file is part of TALER
-   (C) 2014-2021 Taler Systems SA
+   (C) 2014-2022 Taler Systems SA
 
    TALER is free software; you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
@@ -137,8 +137,10 @@ struct DepositConfirmation
    */
   struct TALER_AgeCommitment *age_commitment;
 
-  /* Age mask in the denomination that defines the age groups.  Only
-   * applicable, if minimum age was required. */
+  /**
+   * Age mask in the denomination that defines the age groups.  Only
+   * applicable, if minimum age was required.
+   */
   struct TALER_AgeMask age_mask;
 
   /**
@@ -152,6 +154,11 @@ struct DepositConfirmation
    */
   bool found_in_db;
 
+  /**
+   * true if we #deposit_paid_check() matched this coin in the database.
+   */
+  bool matched_in_db;
+
 };
 
 
@@ -281,7 +288,6 @@ struct PayContext
    */
   struct TALER_Amount max_wire_fee;
 
-
   /**
    * Minimum age required for this purchase.
    */
@@ -767,12 +773,13 @@ deposit_get_callback (
  *        trusted by config
  */
 static void
-process_kyc_with_exchange (void *cls,
-                           const struct TALER_EXCHANGE_HttpResponse *hr,
-                           struct TALER_EXCHANGE_Handle *exchange_handle,
-                           const char *payto_uri,
-                           const struct TALER_Amount *wire_fee,
-                           bool exchange_trusted)
+process_kyc_with_exchange (
+  void *cls,
+  const struct TALER_EXCHANGE_HttpResponse *hr,
+  struct TALER_EXCHANGE_Handle *exchange_handle,
+  const char *payto_uri,
+  const struct TALER_Amount *wire_fee,
+  bool exchange_trusted)
 {
   struct KycContext *kc = cls;
 
@@ -862,10 +869,11 @@ check_kyc (struct PayContext *pc,
       GNUNET_free (kc);
       return; /* we are done */
     }
-    if (GNUNET_TIME_relative_cmp (GNUNET_TIME_absolute_get_duration (
-                                    kc->kyc_timestamp.abs_time),
-                                  <,
-                                  KYC_RETRY_FREQUENCY))
+    if (GNUNET_TIME_relative_cmp (
+          GNUNET_TIME_absolute_get_duration (
+            kc->kyc_timestamp.abs_time),
+          <,
+          KYC_RETRY_FREQUENCY))
     {
       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                   "Not re-checking KYC status at `%s', as we already recently 
asked\n",
@@ -933,19 +941,25 @@ deposit_cb (void *cls,
       {
         enum GNUNET_DB_QueryStatus qs;
 
-        qs = TMH_db->insert_deposit (TMH_db->cls,
-                                     pc->hc->instance->settings.id,
-                                     dr->details.success.deposit_timestamp,
-                                     &pc->h_contract_terms,
-                                     &dc->coin_pub,
-                                     dc->exchange_url,
-                                     &dc->amount_with_fee,
-                                     &dc->deposit_fee,
-                                     &dc->refund_fee,
-                                     &dc->wire_fee,
-                                     &pc->wm->h_wire,
-                                     dr->details.success.exchange_sig,
-                                     dr->details.success.exchange_pub);
+        /* NOTE: We might want to check if the order was fully paid 
concurrently
+           by some other wallet here, and if so, issue an auto-refund. Right 
now,
+           it is possible to over-pay if two wallets literally make a 
concurrent
+           payment, as the earlier check for 'paid' is not in the same 
transaction
+           scope as this 'insert' operation. */
+        qs = TMH_db->insert_deposit (
+          TMH_db->cls,
+          pc->hc->instance->settings.id,
+          dr->details.success.deposit_timestamp,
+          &pc->h_contract_terms,
+          &dc->coin_pub,
+          dc->exchange_url,
+          &dc->amount_with_fee,
+          &dc->deposit_fee,
+          &dc->refund_fee,
+          &dc->wire_fee,
+          &pc->wm->h_wire,
+          dr->details.success.exchange_sig,
+          dr->details.success.exchange_pub);
         if (0 > qs)
         {
           /* Special report if retries insufficient */
@@ -1047,12 +1061,13 @@ deposit_cb (void *cls,
  *        trusted by config
  */
 static void
-process_pay_with_exchange (void *cls,
-                           const struct TALER_EXCHANGE_HttpResponse *hr,
-                           struct TALER_EXCHANGE_Handle *exchange_handle,
-                           const char *payto_uri,
-                           const struct TALER_Amount *wire_fee,
-                           bool exchange_trusted)
+process_pay_with_exchange (
+  void *cls,
+  const struct TALER_EXCHANGE_HttpResponse *hr,
+  struct TALER_EXCHANGE_Handle *exchange_handle,
+  const char *payto_uri,
+  const struct TALER_Amount *wire_fee,
+  bool exchange_trusted)
 {
   struct PayContext *pc = cls;
   struct TMH_HandlerContext *hc = pc->hc;
@@ -1403,6 +1418,7 @@ check_coin_refunded (void *cls,
   for (unsigned int i = 0; i<pc->coins_cnt; i++)
   {
     struct DepositConfirmation *dc = &pc->dc[i];
+
     /* Get matching coins from results.  */
     if (0 != GNUNET_memcmp (coin_pub,
                             &dc->coin_pub))
@@ -1735,6 +1751,31 @@ trigger_payment_notification (struct PayContext *pc)
 }
 
 
+/**
+ * Generate response (payment successful)
+ *
+ * @param[in,out] pc payment context where the payment was successful
+ */
+static void
+generate_success_response (struct PayContext *pc)
+{
+  struct GNUNET_CRYPTO_EddsaSignature sig;
+
+  /* Sign on our end (as the payment did go through, even if it may
+     have been refunded already) */
+  TALER_merchant_pay_sign (&pc->h_contract_terms,
+                           &pc->hc->instance->merchant_priv,
+                           &sig);
+  /* Build the response */
+  resume_pay_with_response (
+    pc,
+    MHD_HTTP_OK,
+    TALER_MHD_MAKE_JSON_PACK (
+      GNUNET_JSON_pack_data_auto ("sig",
+                                  &sig)));
+}
+
+
 /**
  * Actually perform the payment transaction.
  *
@@ -1921,25 +1962,7 @@ execute_pay_transaction (struct PayContext *pc)
     }
     trigger_payment_notification (pc);
   }
-
-  /* Generate response (payment successful) */
-  {
-    struct GNUNET_CRYPTO_EddsaSignature sig;
-
-    /* Sign on our end (as the payment did go through, even if it may
-       have been refunded already) */
-    TALER_merchant_pay_sign (&pc->h_contract_terms,
-                             &pc->hc->instance->merchant_priv,
-                             &sig);
-
-    /* Build the response */
-    resume_pay_with_response (
-      pc,
-      MHD_HTTP_OK,
-      TALER_MHD_MAKE_JSON_PACK (
-        GNUNET_JSON_pack_data_auto ("sig",
-                                    &sig)));
-  }
+  generate_success_response (pc);
 }
 
 
@@ -1947,412 +1970,577 @@ execute_pay_transaction (struct PayContext *pc)
  * Try to parse the pay request into the given pay context.
  * Schedules an error response in the connection on failure.
  *
- * @param connection HTTP connection we are receiving payment on
- * @param[in,out] hc context with further information about the request
- * @param pc context we use to handle the payment
+ * @param[in,out] pc context we use to handle the payment
  * @return #GNUNET_OK on success,
  *         #GNUNET_NO on failure (response was queued with MHD)
  *         #GNUNET_SYSERR on hard error (MHD connection must be dropped)
  */
 static enum GNUNET_GenericReturnValue
-parse_pay (struct MHD_Connection *connection,
-           struct TMH_HandlerContext *hc,
-           struct PayContext *pc)
+parse_pay (struct PayContext *pc)
 {
-  /* First, parse request */
+  const char *session_id = NULL;
+  json_t *coins;
+  struct GNUNET_JSON_Specification spec[] = {
+    GNUNET_JSON_spec_json ("coins",
+                           &coins),
+    GNUNET_JSON_spec_mark_optional (
+      GNUNET_JSON_spec_string ("session_id",
+                               &session_id),
+      NULL),
+    GNUNET_JSON_spec_end ()
+  };
+
   {
-    const char *session_id = NULL;
-    json_t *coins;
-    struct GNUNET_JSON_Specification spec[] = {
-      GNUNET_JSON_spec_json ("coins",
-                             &coins),
-      GNUNET_JSON_spec_mark_optional (
-        GNUNET_JSON_spec_string ("session_id",
-                                 &session_id),
-        NULL),
-      GNUNET_JSON_spec_end ()
-    };
+    enum GNUNET_GenericReturnValue res;
 
+    res = TALER_MHD_parse_json_data (pc->connection,
+                                     pc->hc->request_body,
+                                     spec);
+    if (GNUNET_YES != res)
     {
-      enum GNUNET_GenericReturnValue res;
-
-      res = TALER_MHD_parse_json_data (connection,
-                                       hc->request_body,
-                                       spec);
-      if (GNUNET_YES != res)
-      {
-        GNUNET_break_op (0);
-        return res;
-      }
+      GNUNET_break_op (0);
+      return res;
     }
+  }
 
-    /* copy session ID (if set) */
-    if (NULL != session_id)
-    {
-      pc->session_id = GNUNET_strdup (session_id);
-    }
-    else
-    {
-      /* use empty string as default if client didn't specify it */
-      pc->session_id = GNUNET_strdup ("");
-    }
+  /* copy session ID (if set) */
+  if (NULL != session_id)
+  {
+    pc->session_id = GNUNET_strdup (session_id);
+  }
+  else
+  {
+    /* use empty string as default if client didn't specify it */
+    pc->session_id = GNUNET_strdup ("");
+  }
 
-    if (! json_is_array (coins))
-    {
-      GNUNET_break_op (0);
-      GNUNET_JSON_parse_free (spec);
-      return (MHD_YES ==
-              TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_BAD_REQUEST,
-                                          TALER_EC_GENERIC_PARAMETER_MISSING,
-                                          "'coins' must be an array"))
+  if (! json_is_array (coins))
+  {
+    GNUNET_break_op (0);
+    GNUNET_JSON_parse_free (spec);
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_BAD_REQUEST,
+                                        TALER_EC_GENERIC_PARAMETER_MISSING,
+                                        "'coins' must be an array"))
         ? GNUNET_NO
         : GNUNET_SYSERR;
-    }
+  }
 
-    pc->coins_cnt = json_array_size (coins);
-    if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS)
-    {
-      GNUNET_break_op (0);
-      GNUNET_JSON_parse_free (spec);
-      return (MHD_YES == TALER_MHD_reply_with_error (connection,
-                                                     MHD_HTTP_BAD_REQUEST,
-                                                     
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                                     "'coins' array too long"))
+  pc->coins_cnt = json_array_size (coins);
+  if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS)
+  {
+    GNUNET_break_op (0);
+    GNUNET_JSON_parse_free (spec);
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (
+              pc->connection,
+              MHD_HTTP_BAD_REQUEST,
+              TALER_EC_GENERIC_PARAMETER_MALFORMED,
+              "'coins' array too long"))
         ? GNUNET_NO
         : GNUNET_SYSERR;
-    }
+  }
 
-    /* note: 1 coin = 1 deposit confirmation expected */
-    pc->dc = GNUNET_new_array (pc->coins_cnt,
-                               struct DepositConfirmation);
+  /* note: 1 coin = 1 deposit confirmation expected */
+  pc->dc = GNUNET_new_array (pc->coins_cnt,
+                             struct DepositConfirmation);
 
-    /* This loop populates the array 'dc' in 'pc' */
+  /* This loop populates the array 'dc' in 'pc' */
+  {
+    unsigned int coins_index;
+    json_t *coin;
+    json_array_foreach (coins, coins_index, coin)
     {
-      unsigned int coins_index;
-      json_t *coin;
-      json_array_foreach (coins, coins_index, coin)
+      struct DepositConfirmation *dc = &pc->dc[coins_index];
+      const char *exchange_url;
+      json_t *age_commitment = NULL;
+      struct GNUNET_JSON_Specification ispec[] = {
+        GNUNET_JSON_spec_fixed_auto ("coin_sig",
+                                     &dc->coin_sig),
+        GNUNET_JSON_spec_fixed_auto ("coin_pub",
+                                     &dc->coin_pub),
+        TALER_JSON_spec_denom_sig ("ub_sig",
+                                   &dc->ub_sig),
+        GNUNET_JSON_spec_fixed_auto ("h_denom",
+                                     &dc->h_denom),
+        TALER_JSON_spec_amount ("contribution",
+                                TMH_currency,
+                                &dc->amount_with_fee),
+        GNUNET_JSON_spec_string ("exchange_url",
+                                 &exchange_url),
+        GNUNET_JSON_spec_mark_optional (
+          GNUNET_JSON_spec_fixed_auto ("minimum_age_sig",
+                                       &dc->minimum_age_sig),
+          NULL),
+        GNUNET_JSON_spec_mark_optional (
+          GNUNET_JSON_spec_json ("age_commitment",
+                                 &age_commitment),
+          NULL),
+        GNUNET_JSON_spec_end ()
+      };
+      enum GNUNET_GenericReturnValue res;
+
+      res = TALER_MHD_parse_json_data (pc->connection,
+                                       coin,
+                                       ispec);
+      if (GNUNET_YES != res)
       {
-        struct DepositConfirmation *dc = &pc->dc[coins_index];
-        const char *exchange_url;
-        json_t *age_commitment = NULL;
-        struct GNUNET_JSON_Specification ispec[] = {
-          GNUNET_JSON_spec_fixed_auto ("coin_sig",
-                                       &dc->coin_sig),
-          GNUNET_JSON_spec_fixed_auto ("coin_pub",
-                                       &dc->coin_pub),
-          TALER_JSON_spec_denom_sig ("ub_sig",
-                                     &dc->ub_sig),
-          GNUNET_JSON_spec_fixed_auto ("h_denom",
-                                       &dc->h_denom),
-          TALER_JSON_spec_amount ("contribution",
-                                  TMH_currency,
-                                  &dc->amount_with_fee),
-          GNUNET_JSON_spec_string ("exchange_url",
-                                   &exchange_url),
-          GNUNET_JSON_spec_mark_optional (
-            GNUNET_JSON_spec_fixed_auto ("minimum_age_sig",
-                                         &dc->minimum_age_sig),
-            NULL),
-          GNUNET_JSON_spec_mark_optional (
-            GNUNET_JSON_spec_json ("age_commitment",
-                                   &age_commitment),
-            NULL),
-          GNUNET_JSON_spec_end ()
-        };
-        enum GNUNET_GenericReturnValue res;
-
-        res = TALER_MHD_parse_json_data (connection,
-                                         coin,
-                                         ispec);
-        if (GNUNET_YES != res)
-        {
-          GNUNET_break_op (0);
-          GNUNET_JSON_parse_free (spec);
-          return res;
-        }
+        GNUNET_break_op (0);
+        GNUNET_JSON_parse_free (spec);
+        return res;
+      }
 
-        for (unsigned int j = 0; j<coins_index; j++)
+      for (unsigned int j = 0; j<coins_index; j++)
+      {
+        if (0 ==
+            GNUNET_memcmp (&dc->coin_pub,
+                           &pc->dc[j].coin_pub))
         {
-          if (0 ==
-              GNUNET_memcmp (&dc->coin_pub,
-                             &pc->dc[j].coin_pub))
-          {
-            GNUNET_break_op (0);
-            return (MHD_YES ==
-                    TALER_MHD_reply_with_error (connection,
-                                                MHD_HTTP_BAD_REQUEST,
-                                                
TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                                                "duplicate coin in list"))
+          GNUNET_break_op (0);
+          return (MHD_YES ==
+                  TALER_MHD_reply_with_error (pc->connection,
+                                              MHD_HTTP_BAD_REQUEST,
+                                              
TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                              "duplicate coin in list"))
                 ? GNUNET_NO
                 : GNUNET_SYSERR;
-          }
         }
+      }
 
-        dc->exchange_url = GNUNET_strdup (exchange_url);
-        dc->index = coins_index;
-        dc->pc = pc;
+      dc->exchange_url = GNUNET_strdup (exchange_url);
+      dc->index = coins_index;
+      dc->pc = pc;
+
+      if (0 !=
+          strcasecmp (dc->amount_with_fee.currency,
+                      TMH_currency))
+      {
+        GNUNET_break_op (0);
+        GNUNET_JSON_parse_free (spec);
+        return (MHD_YES ==
+                TALER_MHD_reply_with_error (pc->connection,
+                                            MHD_HTTP_CONFLICT,
+                                            TALER_EC_GENERIC_CURRENCY_MISMATCH,
+                                            TMH_currency))
+              ? GNUNET_NO
+              : GNUNET_SYSERR;
+      }
 
-        if (0 !=
-            strcasecmp (dc->amount_with_fee.currency,
-                        TMH_currency))
+      {
+        bool has_commitment = (NULL != age_commitment) &&
+                              json_is_array (age_commitment);
+        bool has_sig = ! GNUNET_is_zero_ (&dc->minimum_age_sig,
+                                          sizeof(dc->minimum_age_sig));
+        if (has_sig != has_commitment)
         {
           GNUNET_break_op (0);
           GNUNET_JSON_parse_free (spec);
-          return (MHD_YES == TALER_MHD_reply_with_error (connection,
-                                             MHD_HTTP_CONFLICT,
-                                             
TALER_EC_GENERIC_CURRENCY_MISMATCH,
-                                             TMH_currency))
+          return (MHD_YES ==
+                  TALER_MHD_reply_with_error (
+                    pc->connection,
+                    MHD_HTTP_BAD_REQUEST,
+                    TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                    "inconsistency: 'mininum_age_sig' vs. 'age_commitment'")
+                  )
               ? GNUNET_NO
               : GNUNET_SYSERR;
         }
 
+        /* Parse the AgeCommitment, i. e. the public keys */
+        if (has_commitment)
         {
-          bool has_commitment = (NULL != age_commitment) &&
-                                json_is_array (age_commitment);
-          bool has_sig = ! GNUNET_is_zero_ (&dc->minimum_age_sig,
-                                            sizeof(dc->minimum_age_sig));
-          if (has_sig != has_commitment)
+          json_t *pk;
+          unsigned int idx;
+          size_t num = json_array_size (age_commitment);
+
+          /* Sanity check the amount of AgeCommitment's public keys.  The
+           * actual check will be performed once we now the denominations. */
+          if (32 <= num)
           {
             GNUNET_break_op (0);
             GNUNET_JSON_parse_free (spec);
             return (MHD_YES ==
-              TALER_MHD_reply_with_error (
-                connection,
-                MHD_HTTP_BAD_REQUEST,
-                TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                "inconsistency: 'mininum_age_sig' vs. 'age_commitment'")
-              )
-              ? GNUNET_NO
-              : GNUNET_SYSERR;
+                    TALER_MHD_reply_with_error (pc->connection,
+                                                MHD_HTTP_BAD_REQUEST,
+                                                
TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                                                "'age_commitment' too large"
+                                                ))
+                ? GNUNET_NO
+                : GNUNET_SYSERR;
           }
 
-          /* Parse the AgeCommitment, i. e. the public keys */
-          if (has_commitment)
-          {
-            json_t *pk;
-            unsigned int idx;
-            size_t num = json_array_size (age_commitment);
-
-            /* Sanity check the amount of AgeCommitment's public keys.  The
-             * actual check will be performed once we now the denominations. */
-            if (32 <= num)
+          dc->age_commitment = GNUNET_new (struct TALER_AgeCommitment);
+          dc->age_commitment->num = num;
+          dc->age_commitment->keys =
+            GNUNET_new_array (num,
+                              struct TALER_AgeCommitmentPublicKeyP);
+          /* Note that dc->age_commitment.mask will be set later, based on
+           * the actual denomination. */
+
+          json_array_foreach (age_commitment, idx, pk) {
+            struct GNUNET_JSON_Specification pkspec[] = {
+              GNUNET_JSON_spec_fixed_auto (
+                NULL,
+                &dc->age_commitment->keys[idx].pub),
+              GNUNET_JSON_spec_end ()
+            };
+
+            if (GNUNET_OK !=
+                GNUNET_JSON_parse (pk,
+                                   pkspec,
+                                   NULL, NULL))
             {
               GNUNET_break_op (0);
               GNUNET_JSON_parse_free (spec);
               return (MHD_YES ==
-                TALER_MHD_reply_with_error (connection,
-                  MHD_HTTP_BAD_REQUEST,
-                  TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                  "'age_commitment' too large"
-                ))
-                ? GNUNET_NO
-                : GNUNET_SYSERR;
-            }
-
-            dc->age_commitment = GNUNET_new (struct TALER_AgeCommitment);
-            dc->age_commitment->num = num;
-            dc->age_commitment->keys =
-              GNUNET_new_array (num,
-                                struct TALER_AgeCommitmentPublicKeyP);
-            /* Note that dc->age_commitment.mask will be set later, based on
-             * the actual denomination. */
-
-            json_array_foreach (age_commitment, idx, pk) {
-              struct GNUNET_JSON_Specification pkspec[] = {
-                GNUNET_JSON_spec_fixed_auto (NULL,
-                                             &dc
-                                             ->age_commitment
-                                             ->keys[idx].pub),
-                GNUNET_JSON_spec_end ()
-              };
-
-              if (GNUNET_OK !=
-                  GNUNET_JSON_parse (pk,
-                                     pkspec,
-                                     NULL, NULL))
-              {
-                GNUNET_break_op (0);
-                GNUNET_JSON_parse_free (spec);
-                return (MHD_YES ==
-                  TALER_MHD_reply_with_error (
-                    connection,
-                    MHD_HTTP_BAD_REQUEST,
-                    TALER_EC_GENERIC_PARAMETER_MALFORMED,
-                    "age_commitment"))
+                      TALER_MHD_reply_with_error (
+                        pc->connection,
+                        MHD_HTTP_BAD_REQUEST,
+                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
+                        "age_commitment"))
                   ? GNUNET_NO
                   : GNUNET_SYSERR;
-              }
             }
           }
         }
       }
     }
-    GNUNET_JSON_parse_free (spec);
   }
+  GNUNET_JSON_parse_free (spec);
+  return GNUNET_OK;
+}
 
-  /* obtain contract terms */
+
+/**
+ * Function called with information about a coin that was deposited.
+ * Checks if this coin is in our list of deposits as well.
+ *
+ * @param cls closure with our `struct PayContext *`
+ * @param deposit_serial which deposit operation is this about
+ * @param exchange_url URL of the exchange that issued the coin
+ * @param amount_with_fee amount the exchange will deposit for this coin
+ * @param deposit_fee fee the exchange will charge for this coin
+ * @param h_wire hash of merchant's wire details
+ * @param coin_pub public key of the coin
+ */
+static void
+deposit_paid_check (
+  void *cls,
+  uint64_t deposit_serial,
+  const char *exchange_url,
+  const struct TALER_MerchantWireHashP *h_wire,
+  const struct TALER_Amount *amount_with_fee,
+  const struct TALER_Amount *deposit_fee,
+  const struct TALER_CoinSpendPublicKeyP *coin_pub)
+{
+  struct PayContext *pc = cls;
+
+  for (unsigned int i = 0; i<pc->coins_cnt; i++)
   {
-    enum GNUNET_DB_QueryStatus qs;
-    json_t *contract_terms = NULL;
-
-    qs = TMH_db->lookup_contract_terms (TMH_db->cls,
-                                        hc->instance->settings.id,
-                                        pc->order_id,
-                                        &contract_terms,
-                                        &pc->order_serial,
-                                        NULL);
-    if (0 > qs)
+    struct DepositConfirmation *dci = &pc->dc[i];
+
+    if ( (0 ==
+          GNUNET_memcmp (&dci->coin_pub,
+                         coin_pub)) &&
+         (0 ==
+          strcmp (dci->exchange_url,
+                  exchange_url)) &&
+         (0 ==
+          TALER_amount_cmp (&dci->amount_with_fee,
+                            amount_with_fee)) )
     {
-      /* single, read-only SQL statements should never cause
-         serialization problems */
-      GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
-      /* Always report on hard error to enable diagnostics */
-      GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
-      return (MHD_YES ==
-              TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
-                                          "contract terms"))
-       ? GNUNET_NO
-       : GNUNET_SYSERR;
+      dci->matched_in_db = true;
+      break;
     }
-    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
-    {
-      return (MHD_YES ==
-              TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_NOT_FOUND,
-                                          
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
-                                          pc->order_id))
+  }
+}
+
+
+/**
+ * Handle case where contract was already paid. Either decides
+ * the payment is idempotent, or refunds the excess payment.
+ *
+ * @param[in,out] pc context we use to handle the payment
+ * @return #GNUNET_NO if response was queued with MHD
+ *         #GNUNET_SYSERR on hard error (MHD connection must be dropped)
+ */
+static enum GNUNET_GenericReturnValue
+handle_contract_paid (struct PayContext *pc)
+{
+  enum GNUNET_DB_QueryStatus qs;
+  bool unmatched = false;
+  json_t *refunds;
+
+  qs = TMH_db->lookup_deposits_by_order (TMH_db->cls,
+                                         pc->order_serial,
+                                         &deposit_paid_check,
+                                         pc);
+  if (qs <= 0)
+  {
+    GNUNET_break (0);
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                        TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                        "lookup_deposits_by_order"))
        ? GNUNET_NO
        : GNUNET_SYSERR;
-    }
+  }
+  for (unsigned int i = 0; i<pc->coins_cnt; i++)
+  {
+    struct DepositConfirmation *dci = &pc->dc[i];
 
-    /* hash contract (needed later) */
-    if (GNUNET_OK !=
-        TALER_JSON_contract_hash (contract_terms,
-                                  &pc->h_contract_terms))
-    {
-      GNUNET_break (0);
-      json_decref (contract_terms);
-      return (MHD_YES ==
-              TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
-                                          NULL))
+    if (! dci->matched_in_db)
+      unmatched = true;
+  }
+  if (! unmatched)
+  {
+    /* Everything fine, idempotent request */
+    struct GNUNET_CRYPTO_EddsaSignature sig;
+
+    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+                "Idempotent pay request for order `%s', signing again\n",
+                pc->order_id);
+    TALER_merchant_pay_sign (&pc->h_contract_terms,
+                             &pc->hc->instance->merchant_priv,
+                             &sig);
+    return (MHD_YES ==
+            TALER_MHD_REPLY_JSON_PACK (
+              pc->connection,
+              MHD_HTTP_OK,
+              GNUNET_JSON_pack_data_auto ("sig",
+                                          &sig)))
        ? GNUNET_NO
        : GNUNET_SYSERR;
-    }
-    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
-                "Handling payment for order `%s' with contract hash `%s'\n",
-                pc->order_id,
-                GNUNET_h2s (&pc->h_contract_terms.hash));
+  }
+  /* Conflict, double-payment detected! */
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Client attempted to pay extra for already paid order `%s'\n",
+              pc->order_id);
+  refunds = json_array ();
+  GNUNET_assert (NULL != refunds);
+  for (unsigned int i = 0; i<pc->coins_cnt; i++)
+  {
+    struct DepositConfirmation *dci = &pc->dc[i];
+    struct TALER_MerchantSignatureP merchant_sig;
 
-    /* basic sanity check on the contract */
-    if (NULL == json_object_get (contract_terms,
-                                 "merchant"))
-    {
-      /* invalid contract */
-      GNUNET_break (0);
-      json_decref (contract_terms);
-      return (MHD_YES ==
-              TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                          
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING,
-                                          NULL))
+    if (dci->matched_in_db)
+      continue;
+    TALER_merchant_refund_sign (&dci->coin_pub,
+                                &pc->h_contract_terms,
+                                0, /* rtransaction id */
+                                &dci->amount_with_fee,
+                                &pc->hc->instance->merchant_priv,
+                                &merchant_sig);
+    GNUNET_assert (
+      0 ==
+      json_array_append_new (
+        refunds,
+        GNUNET_JSON_PACK (
+          GNUNET_JSON_pack_data_auto (
+            "coin_pub",
+            &dci->coin_pub),
+          GNUNET_JSON_pack_data_auto (
+            "merchant_sig",
+            &merchant_sig),
+          TALER_JSON_pack_amount ("amount",
+                                  &dci->amount_with_fee),
+          GNUNET_JSON_pack_uint64 ("rtransaction_id",
+                                   0))));
+  }
+  return (MHD_YES ==
+          TALER_MHD_REPLY_JSON_PACK (
+            pc->connection,
+            MHD_HTTP_CONFLICT,
+            TALER_MHD_PACK_EC (
+              TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID),
+            GNUNET_JSON_pack_array_steal ("refunds",
+                                          refunds)))
        ? GNUNET_NO
        : GNUNET_SYSERR;
-    }
+}
 
-    /* Get details from contract and check fundamentals */
-    {
-      const char *fulfillment_url = NULL;
-      struct GNUNET_JSON_Specification espec[] = {
-        TALER_JSON_spec_amount ("amount",
-                                TMH_currency,
-                                &pc->amount),
-        GNUNET_JSON_spec_mark_optional (
-          GNUNET_JSON_spec_string ("fulfillment_url",
-                                   &fulfillment_url),
-          NULL),
-        TALER_JSON_spec_amount ("max_fee",
-                                TMH_currency,
-                                &pc->max_fee),
-        TALER_JSON_spec_amount ("max_wire_fee",
-                                TMH_currency,
-                                &pc->max_wire_fee),
-        GNUNET_JSON_spec_uint32 ("wire_fee_amortization",
-                                 &pc->wire_fee_amortization),
-        GNUNET_JSON_spec_timestamp ("timestamp",
-                                    &pc->timestamp),
-        GNUNET_JSON_spec_timestamp ("refund_deadline",
-                                    &pc->refund_deadline),
-        GNUNET_JSON_spec_timestamp ("pay_deadline",
-                                    &pc->pay_deadline),
-        GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
-                                    &pc->wire_transfer_deadline),
-        GNUNET_JSON_spec_fixed_auto ("h_wire",
-                                     &pc->h_wire),
-        GNUNET_JSON_spec_mark_optional (
-          GNUNET_JSON_spec_uint32 ("minimum_age",
-                                   &pc->minimum_age),
-          NULL),
-        GNUNET_JSON_spec_end ()
-      };
-      enum GNUNET_GenericReturnValue res;
 
-      pc->minimum_age = 0;
+/**
+ * Check the database state for the given order. * Schedules an error response 
in the connection on failure.
+ *
+ * @param connection HTTP connection we are receiving payment on
+ * @param[in,out] hc context with further information about the request
+ * @param pc context we use to handle the payment
+ * @return #GNUNET_OK on success,
+ *         #GNUNET_NO on failure (response was queued with MHD)
+ *         #GNUNET_SYSERR on hard error (MHD connection must be dropped)
+ */
+static enum GNUNET_GenericReturnValue
+check_contract (struct PayContext *pc)
+{
+  /* obtain contract terms */
+  enum GNUNET_DB_QueryStatus qs;
+  json_t *contract_terms = NULL;
+  bool paid = false;
+
+  qs = TMH_db->lookup_contract_terms (TMH_db->cls,
+                                      pc->hc->instance->settings.id,
+                                      pc->order_id,
+                                      &contract_terms,
+                                      &pc->order_serial,
+                                      &paid,
+                                      NULL);
+  if (0 > qs)
+  {
+    /* single, read-only SQL statements should never cause
+       serialization problems */
+    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
+    /* Always report on hard error to enable diagnostics */
+    GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                        TALER_EC_GENERIC_DB_FETCH_FAILED,
+                                        "contract terms"))
+       ? GNUNET_NO
+       : GNUNET_SYSERR;
+  }
+  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+  {
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_NOT_FOUND,
+                                        
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
+                                        pc->order_id))
+       ? GNUNET_NO
+       : GNUNET_SYSERR;
+  }
+  /* hash contract (needed later) */
+  if (GNUNET_OK !=
+      TALER_JSON_contract_hash (contract_terms,
+                                &pc->h_contract_terms))
+  {
+    GNUNET_break (0);
+    json_decref (contract_terms);
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                        
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
+                                        NULL))
+       ? GNUNET_NO
+       : GNUNET_SYSERR;
+  }
+  if (paid)
+  {
+    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+                "Order `%s' paid, checking for double-payment\n",
+                pc->order_id);
+    return handle_contract_paid (pc);
+  }
+  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+              "Handling payment for order `%s' with contract hash `%s'\n",
+              pc->order_id,
+              GNUNET_h2s (&pc->h_contract_terms.hash));
 
-      res = TALER_MHD_parse_internal_json_data (connection,
-                                                contract_terms,
-                                                espec);
-      if (NULL != fulfillment_url)
-        pc->fulfillment_url = GNUNET_strdup (fulfillment_url);
-      json_decref (contract_terms);
-      if (GNUNET_YES != res)
-      {
-        GNUNET_break (0);
-        return res;
-      }
-    }
+  /* basic sanity check on the contract */
+  if (NULL == json_object_get (contract_terms,
+                               "merchant"))
+  {
+    /* invalid contract */
+    GNUNET_break (0);
+    json_decref (contract_terms);
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                        
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING,
+                                        NULL))
+       ? GNUNET_NO
+       : GNUNET_SYSERR;
+  }
 
-    if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline,
-                                   <,
-                                   pc->refund_deadline))
+  /* Get details from contract and check fundamentals */
+  {
+    const char *fulfillment_url = NULL;
+    struct GNUNET_JSON_Specification espec[] = {
+      TALER_JSON_spec_amount ("amount",
+                              TMH_currency,
+                              &pc->amount),
+      GNUNET_JSON_spec_mark_optional (
+        GNUNET_JSON_spec_string ("fulfillment_url",
+                                 &fulfillment_url),
+        NULL),
+      TALER_JSON_spec_amount ("max_fee",
+                              TMH_currency,
+                              &pc->max_fee),
+      TALER_JSON_spec_amount ("max_wire_fee",
+                              TMH_currency,
+                              &pc->max_wire_fee),
+      GNUNET_JSON_spec_uint32 ("wire_fee_amortization",
+                               &pc->wire_fee_amortization),
+      GNUNET_JSON_spec_timestamp ("timestamp",
+                                  &pc->timestamp),
+      GNUNET_JSON_spec_timestamp ("refund_deadline",
+                                  &pc->refund_deadline),
+      GNUNET_JSON_spec_timestamp ("pay_deadline",
+                                  &pc->pay_deadline),
+      GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
+                                  &pc->wire_transfer_deadline),
+      GNUNET_JSON_spec_fixed_auto ("h_wire",
+                                   &pc->h_wire),
+      GNUNET_JSON_spec_mark_optional (
+        GNUNET_JSON_spec_uint32 ("minimum_age",
+                                 &pc->minimum_age),
+        NULL),
+      GNUNET_JSON_spec_end ()
+    };
+    enum GNUNET_GenericReturnValue res;
+
+    pc->minimum_age = 0;
+    res = TALER_MHD_parse_internal_json_data (pc->connection,
+                                              contract_terms,
+                                              espec);
+    if (NULL != fulfillment_url)
+      pc->fulfillment_url = GNUNET_strdup (fulfillment_url);
+    json_decref (contract_terms);
+    if (GNUNET_YES != res)
     {
-      /* This should already have been checked when creating the order! */
       GNUNET_break (0);
-      return TALER_MHD_reply_with_error (connection,
-                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
-                                         
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE,
-                                         NULL);
+      return res;
     }
+  }
 
-    if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time))
-    {
-      /* too late */
-      return (MHD_YES ==
-              TALER_MHD_reply_with_error (connection,
-                                          MHD_HTTP_GONE,
-                                          
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED,
-                                          NULL))
+  if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline,
+                                 <,
+                                 pc->refund_deadline))
+  {
+    /* This should already have been checked when creating the order! */
+    GNUNET_break (0);
+    return TALER_MHD_reply_with_error (pc->connection,
+                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
+                                       
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE,
+                                       NULL);
+  }
+  if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time))
+  {
+    /* too late */
+    return (MHD_YES ==
+            TALER_MHD_reply_with_error (pc->connection,
+                                        MHD_HTTP_GONE,
+                                        
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED,
+                                        NULL))
        ? GNUNET_NO
        : GNUNET_SYSERR;
-    }
   }
 
   /* Make sure wire method (still) exists for this instance */
   {
     struct TMH_WireMethod *wm;
 
-    wm = hc->instance->wm_head;
+    wm = pc->hc->instance->wm_head;
     while (0 != GNUNET_memcmp (&pc->h_wire,
                                &wm->h_wire))
       wm = wm->next;
     if (NULL == wm)
     {
       GNUNET_break (0);
-      return TALER_MHD_reply_with_error (connection,
+      return TALER_MHD_reply_with_error (pc->connection,
                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
                                          
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN,
                                          NULL);
@@ -2405,9 +2593,9 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler 
*rh,
                         struct TMH_HandlerContext *hc)
 {
   struct PayContext *pc = hc->ctx;
+  enum GNUNET_GenericReturnValue ret;
 
   GNUNET_assert (NULL != hc->infix);
-
   if (NULL == pc)
   {
     pc = GNUNET_new (struct PayContext);
@@ -2419,6 +2607,11 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler 
*rh,
     pc->order_id = hc->infix;
     hc->ctx = pc;
     hc->cc = &pay_context_cleanup;
+    ret = parse_pay (pc);
+    if (GNUNET_OK != ret)
+      return (GNUNET_NO == ret)
+       ? MHD_YES
+       : MHD_NO;
   }
   if (GNUNET_SYSERR == pc->suspended)
     return MHD_NO; /* during shutdown, we don't generate any more replies */
@@ -2435,17 +2628,11 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler 
*rh,
                                pc->response_code,
                                pc->response);
   }
-  {
-    enum GNUNET_GenericReturnValue ret;
-
-    ret = parse_pay (connection,
-                     hc,
-                     pc);
-    if (GNUNET_OK != ret)
-      return (GNUNET_NO == ret)
-       ? MHD_YES
-       : MHD_NO;
-  }
+  ret = check_contract (pc);
+  if (GNUNET_OK != ret)
+    return (GNUNET_NO == ret)
+      ? MHD_YES
+      : MHD_NO;
 
   /* Payment not finished, suspend while we interact with the exchange */
   MHD_suspend_connection (connection);
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c 
b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c
index b6aebf71..40c89712 100644
--- a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c
+++ b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c
@@ -596,12 +596,14 @@ TMH_post_orders_ID_refund (const struct 
TMH_RequestHandler *rh,
     {
       json_t *contract_terms;
       uint64_t order_serial;
+      bool paid;
 
       qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                           hc->instance->settings.id,
                                           hc->infix,
                                           &contract_terms,
                                           &order_serial,
+                                          &paid,
                                           NULL);
       if (0 > qs)
       {
diff --git a/src/backend/taler-merchant-httpd_private-delete-orders-ID.c 
b/src/backend/taler-merchant-httpd_private-delete-orders-ID.c
index 9eaa8b0e..9d0b4fb4 100644
--- a/src/backend/taler-merchant-httpd_private-delete-orders-ID.c
+++ b/src/backend/taler-merchant-httpd_private-delete-orders-ID.c
@@ -38,11 +38,22 @@ TMH_private_delete_orders_ID (const struct 
TMH_RequestHandler *rh,
 {
   struct TMH_MerchantInstance *mi = hc->instance;
   enum GNUNET_DB_QueryStatus qs;
+  const char *force_s;
+  bool force;
+
+  force_s = MHD_lookup_connection_value (connection,
+                                         MHD_GET_ARGUMENT_KIND,
+                                         "force");
+  if (NULL == force_s)
+    force_s = "no";
+  force = (0 == strcasecmp (force_s,
+                            "yes"));
 
   GNUNET_assert (NULL != mi);
   qs = TMH_db->delete_order (TMH_db->cls,
                              mi->settings.id,
-                             hc->infix);
+                             hc->infix,
+                             force);
   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
     qs = TMH_db->delete_contract_terms (TMH_db->cls,
                                         mi->settings.id,
@@ -64,6 +75,8 @@ TMH_private_delete_orders_ID (const struct TMH_RequestHandler 
*rh,
   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     {
       struct TALER_MerchantPostDataHashP unused;
+      uint64_t order_serial;
+      bool paid = false;
 
       qs = TMH_db->lookup_order (TMH_db->cls,
                                  mi->settings.id,
@@ -71,27 +84,31 @@ TMH_private_delete_orders_ID (const struct 
TMH_RequestHandler *rh,
                                  NULL,
                                  &unused,
                                  NULL);
-    }
-    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
-    {
-      uint64_t order_serial;
-
-      qs = TMH_db->lookup_contract_terms (TMH_db->cls,
-                                          mi->settings.id,
-                                          hc->infix,
-                                          NULL,
-                                          &order_serial,
-                                          NULL);
-    }
-    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+      if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+      {
+        qs = TMH_db->lookup_contract_terms (TMH_db->cls,
+                                            mi->settings.id,
+                                            hc->infix,
+                                            NULL,
+                                            &order_serial,
+                                            &paid,
+                                            NULL);
+      }
+      if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+        return TALER_MHD_reply_with_error (connection,
+                                           MHD_HTTP_NOT_FOUND,
+                                           
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
+                                           hc->infix);
+      if (paid)
+        return TALER_MHD_reply_with_error (connection,
+                                           MHD_HTTP_CONFLICT,
+                                           
TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID,
+                                           hc->infix);
       return TALER_MHD_reply_with_error (connection,
-                                         MHD_HTTP_NOT_FOUND,
-                                         
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
+                                         MHD_HTTP_CONFLICT,
+                                         
TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT,
                                          hc->infix);
-    return TALER_MHD_reply_with_error (connection,
-                                       MHD_HTTP_CONFLICT,
-                                       
TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT,
-                                       hc->infix);
+    }
   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     return TALER_MHD_reply_static (connection,
                                    MHD_HTTP_NO_CONTENT,
diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c 
b/src/backend/taler-merchant-httpd_private-get-orders-ID.c
index 828d1d65..bc5bcfd9 100644
--- a/src/backend/taler-merchant-httpd_private-get-orders-ID.c
+++ b/src/backend/taler-merchant-httpd_private-get-orders-ID.c
@@ -1012,12 +1012,17 @@ TMH_private_get_orders_ID (const struct 
TMH_RequestHandler *rh,
     gorc->fulfillment_url = NULL;
   }
   TMH_db->preflight (TMH_db->cls);
-  qs = TMH_db->lookup_contract_terms (TMH_db->cls,
-                                      hc->instance->settings.id,
-                                      hc->infix,
-                                      &gorc->contract_terms,
-                                      &gorc->order_serial,
-                                      NULL);
+  {
+    bool paid = false;
+
+    qs = TMH_db->lookup_contract_terms (TMH_db->cls,
+                                        hc->instance->settings.id,
+                                        hc->infix,
+                                        &gorc->contract_terms,
+                                        &gorc->order_serial,
+                                        &paid,
+                                        NULL);
+  }
   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
   {
     order_only = true;
diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c 
b/src/backend/taler-merchant-httpd_private-get-orders.c
index e6f615ea..d9059702 100644
--- a/src/backend/taler-merchant-httpd_private-get-orders.c
+++ b/src/backend/taler-merchant-httpd_private-get-orders.c
@@ -316,12 +316,14 @@ add_order (void *cls,
   {
     /* First try to find the order in the contracts */
     uint64_t os;
+    bool paid = false;
 
     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                         po->instance_id,
                                         order_id,
                                         &contract_terms,
                                         &os,
+                                        &paid,
                                         NULL);
     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
       GNUNET_break (os == order_serial);
diff --git a/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c 
b/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c
index a158db7c..798d610a 100644
--- a/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c
+++ b/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c
@@ -96,6 +96,7 @@ TMH_private_patch_orders_ID_forget (const struct 
TMH_RequestHandler *rh,
     json_t *fields;
     json_t *contract_terms;
     bool changed = false;
+    bool paid = false;
 
     if (GNUNET_OK !=
         TMH_db->start (TMH_db->cls,
@@ -111,6 +112,7 @@ TMH_private_patch_orders_ID_forget (const struct 
TMH_RequestHandler *rh,
                                         order_id,
                                         &contract_terms,
                                         &order_serial,
+                                        &paid,
                                         NULL);
     switch (qs)
     {
diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c 
b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c
index 96352a93..3953fa06 100644
--- a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c
+++ b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c
@@ -158,12 +158,14 @@ TMH_private_post_orders_ID_refund (const struct 
TMH_RequestHandler *rh,
     uint64_t order_serial;
     struct GNUNET_TIME_Timestamp refund_deadline;
     struct GNUNET_TIME_Timestamp timestamp;
+    bool paid = false;
 
     qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                         hc->instance->settings.id,
                                         hc->infix,
                                         &contract_terms,
                                         &order_serial,
+                                        &paid,
                                         NULL);
     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
     {
@@ -306,12 +308,14 @@ TMH_private_post_orders_ID_refund (const struct 
TMH_RequestHandler *rh,
       enum GNUNET_DB_QueryStatus qs;
       json_t *contract_terms;
       uint64_t order_serial;
+      bool paid;
 
       qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                           hc->instance->settings.id,
                                           hc->infix,
                                           &contract_terms,
                                           &order_serial,
+                                          &paid,
                                           NULL);
       if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
       {
diff --git a/src/backenddb/plugin_merchantdb_postgres.c 
b/src/backenddb/plugin_merchantdb_postgres.c
index 87049d9e..9b39365b 100644
--- a/src/backenddb/plugin_merchantdb_postgres.c
+++ b/src/backenddb/plugin_merchantdb_postgres.c
@@ -1631,13 +1631,15 @@ postgres_expire_locks (void *cls)
  * @param cls closure
  * @param instance_id instance to delete order of
  * @param order_id order to delete
+ * @param force delete claimed but unpaid orders as well
  * @return DB status code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS
  *           if pending payment prevents deletion OR order unknown
  */
 static enum GNUNET_DB_QueryStatus
 postgres_delete_order (void *cls,
                        const char *instance_id,
-                       const char *order_id)
+                       const char *order_id,
+                       bool force)
 {
   struct PostgresClosure *pg = cls;
   struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
@@ -1645,13 +1647,25 @@ postgres_delete_order (void *cls,
     GNUNET_PQ_query_param_string (instance_id),
     GNUNET_PQ_query_param_string (order_id),
     GNUNET_PQ_query_param_absolute_time (&now),
+    GNUNET_PQ_query_param_bool (force),
+    GNUNET_PQ_query_param_end
+  };
+  struct GNUNET_PQ_QueryParam params2[] = {
+    GNUNET_PQ_query_param_string (instance_id),
+    GNUNET_PQ_query_param_string (order_id),
     GNUNET_PQ_query_param_end
   };
+  enum GNUNET_DB_QueryStatus qs;
 
   check_connection (pg);
+  qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
+                                           "delete_order",
+                                           params);
+  if ( (qs <= 0) || (! force))
+    return qs;
   return GNUNET_PQ_eval_prepared_non_select (pg->conn,
-                                             "delete_order",
-                                             params);
+                                             "delete_contract",
+                                             params2);
 }
 
 
@@ -2015,6 +2029,7 @@ postgres_insert_order_lock (void *cls,
  * @param order_id order_id used to lookup.
  * @param[out] contract_terms where to store the result, NULL to only check 
for existence
  * @param[out] order_serial set to the order's serial number
+ * @param[out] paid set to true if the order is fully paid
  * @param[out] claim_token set to the claim token, NULL to only check for 
existence
  * @return transaction status
  */
@@ -2024,6 +2039,7 @@ postgres_lookup_contract_terms (void *cls,
                                 const char *order_id,
                                 json_t **contract_terms,
                                 uint64_t *order_serial,
+                                bool *paid,
                                 struct TALER_ClaimTokenP *claim_token)
 {
   struct PostgresClosure *pg = cls;
@@ -2034,12 +2050,14 @@ postgres_lookup_contract_terms (void *cls,
     GNUNET_PQ_query_param_string (order_id),
     GNUNET_PQ_query_param_end
   };
-  struct GNUNET_PQ_ResultSpec rs[4] = {
+  struct GNUNET_PQ_ResultSpec rs[] = {
     /* contract_terms must be first! */
     TALER_PQ_result_spec_json ("contract_terms",
                                contract_terms),
     GNUNET_PQ_result_spec_uint64 ("order_serial",
                                   order_serial),
+    GNUNET_PQ_result_spec_bool ("paid",
+                                paid),
     GNUNET_PQ_result_spec_auto_from_type ("claim_token",
                                           &ct),
     GNUNET_PQ_result_spec_end
@@ -7156,20 +7174,33 @@ postgres_connect (void *cls)
 
     /* for postgres_delete_order() */
     GNUNET_PQ_make_prepare ("delete_order",
+                            "WITH ms AS"
+                            "(SELECT merchant_serial "
+                            "   FROM merchant_instances"
+                            "  WHERE merchant_id=$1)"
+                            ", mc AS"
+                            "(SELECT paid"
+                            "   FROM merchant_contract_terms"
+                            "   JOIN ms USING (merchant_serial)"
+                            "  WHERE order_id=$2) "
                             "DELETE"
-                            " FROM merchant_orders"
-                            " WHERE merchant_orders.merchant_serial="
+                            " FROM merchant_orders mo"
+                            " WHERE order_id=$2"
+                            "   AND merchant_serial=(SELECT merchant_serial 
FROM ms)"
+                            "   AND (  (pay_deadline < $3)"
+                            "       OR (NOT EXISTS (SELECT paid FROM mc))"
+                            "       OR ($4 AND (FALSE=(SELECT paid FROM mc))) 
);",
+                            4),
+    GNUNET_PQ_make_prepare ("delete_contract",
+                            "DELETE"
+                            " FROM merchant_contract_terms"
+                            " WHERE order_id=$2 AND"
+                            "   merchant_serial="
                             "     (SELECT merchant_serial "
                             "        FROM merchant_instances"
                             "        WHERE merchant_id=$1)"
-                            "   AND merchant_orders.order_id=$2"
-                            "   AND"
-                            "       ((NOT EXISTS"
-                            "        (SELECT order_id"
-                            "           FROM merchant_contract_terms"
-                            "           WHERE 
merchant_contract_terms.order_id=$2))"
-                            "   OR pay_deadline < $3)",
-                            3),
+                            "   AND NOT paid;",
+                            2),
     /* for postgres_lookup_order() */
     GNUNET_PQ_make_prepare ("lookup_order",
                             "SELECT"
@@ -8040,6 +8071,7 @@ postgres_connect (void *cls)
                             " contract_terms"
                             ",order_serial"
                             ",claim_token"
+                            ",paid"
                             " FROM merchant_contract_terms"
                             " WHERE order_id=$2"
                             "   AND merchant_serial="
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
index 2d764e7d..de27af31 100644
--- a/src/backenddb/test_merchantdb.c
+++ b/src/backenddb/test_merchantdb.c
@@ -1623,7 +1623,8 @@ test_delete_order (const struct InstanceData *instance,
   TEST_COND_RET_ON_FAIL (expected_result ==
                          plugin->delete_order (plugin->cls,
                                                instance->instance.id,
-                                               order->id),
+                                               order->id,
+                                               false),
                          "Delete order failed\n");
   return 0;
 }
@@ -1688,6 +1689,7 @@ test_lookup_contract_terms (const struct InstanceData 
*instance,
 {
   json_t *contract = NULL;
   uint64_t order_serial;
+  bool paid;
 
   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
       plugin->lookup_contract_terms (plugin->cls,
@@ -1695,6 +1697,7 @@ test_lookup_contract_terms (const struct InstanceData 
*instance,
                                      order->id,
                                      &contract,
                                      &order_serial,
+                                     &paid,
                                      NULL))
   {
     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
@@ -2072,12 +2075,15 @@ run_test_orders (struct TestOrders_Closure *cls)
   {
     json_t *lookup_contract = NULL;
     uint64_t lookup_order_serial;
+    bool paid;
+
     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
         plugin->lookup_contract_terms (plugin->cls,
                                        cls->instance.instance.id,
                                        cls->orders[1].id,
                                        &lookup_contract,
                                        &lookup_order_serial,
+                                       &paid,
                                        NULL))
     {
       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
diff --git a/src/include/taler_merchant_service.h 
b/src/include/taler_merchant_service.h
index d632df67..32209bcd 100644
--- a/src/include/taler_merchant_service.h
+++ b/src/include/taler_merchant_service.h
@@ -2027,6 +2027,7 @@ typedef void
  * @param ctx the context
  * @param backend_url HTTP base URL for the backend
  * @param order_id identifier of the order
+ * @param force force deletion of claimed (but unpaid) orders
  * @param cb function to call with the backend's deletion status
  * @param cb_cls closure for @a cb
  * @return the request handle; NULL upon error
@@ -2036,6 +2037,7 @@ TALER_MERCHANT_order_delete (
   struct GNUNET_CURL_Context *ctx,
   const char *backend_url,
   const char *order_id,
+  bool force,
   TALER_MERCHANT_OrderDeleteCallback cb,
   void *cb_cls);
 
@@ -2119,6 +2121,44 @@ TALER_MERCHANT_order_claim_cancel (struct 
TALER_MERCHANT_OrderClaimHandle *och);
 struct TALER_MERCHANT_OrderPayHandle;
 
 
+/**
+ * Information returned in response to a payment.
+ */
+struct TALER_MERCHANT_PayResponse
+{
+
+  /**
+   * General HTTP response details.
+   */
+  struct TALER_MERCHANT_HttpResponse hr;
+
+  /**
+   * Details returned depending on @e hr.
+   */
+  union
+  {
+
+    /**
+     * Details returned on success.
+     */
+    struct
+    {
+
+      /**
+       * Signature affirming that the order was paid.
+       */
+      struct TALER_MerchantSignatureP merchant_sig;
+
+    } success;
+
+    // TODO: might want to return further details on errors,
+    // especially refund signatures on double-pay conflict.
+
+  } details;
+
+};
+
+
 /**
  * Callbacks of this type are used to serve the result of submitting a
  * POST /orders/$ID/pay request to a merchant.
@@ -2131,8 +2171,7 @@ struct TALER_MERCHANT_OrderPayHandle;
 typedef void
 (*TALER_MERCHANT_OrderPayCallback) (
   void *cls,
-  const struct TALER_MERCHANT_HttpResponse *hr,
-  const struct TALER_MerchantSignatureP *merchant_sig);
+  const struct TALER_MERCHANT_PayResponse *pr);
 
 
 /**
diff --git a/src/include/taler_merchantdb_plugin.h 
b/src/include/taler_merchantdb_plugin.h
index 41fb0b59..79d679e9 100644
--- a/src/include/taler_merchantdb_plugin.h
+++ b/src/include/taler_merchantdb_plugin.h
@@ -1222,13 +1222,15 @@ struct TALER_MERCHANTDB_Plugin
    * @param cls closure
    * @param instance_id instance to delete order of
    * @param order_id order to delete
+   * @param force force deletion of claimed but unpaid orders
    * @return DB status code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS
    *           if locks prevent deletion OR order unknown
    */
   enum GNUNET_DB_QueryStatus
   (*delete_order)(void *cls,
                   const char *instance_id,
-                  const char *order_id);
+                  const char *order_id,
+                  bool force);
 
 
   /**
@@ -1354,6 +1356,7 @@ struct TALER_MERCHANTDB_Plugin
    * @param order_id order_id used to lookup.
    * @param[out] contract_terms where to store the result, NULL to only check 
for existence
    * @param[out] order_serial set to the order's serial number
+   * @param[out] paid set to true if the order is fully paid
    * @param[out] claim_token set to the claim token, NULL to only check for 
existence
    * @return transaction status
    */
@@ -1363,6 +1366,7 @@ struct TALER_MERCHANTDB_Plugin
                            const char *order_id,
                            json_t **contract_terms,
                            uint64_t *order_serial,
+                           bool *paid,
                            struct TALER_ClaimTokenP *claim_token);
 
 
diff --git a/src/lib/merchant_api_delete_order.c 
b/src/lib/merchant_api_delete_order.c
index 33a12294..a0cf941f 100644
--- a/src/lib/merchant_api_delete_order.c
+++ b/src/lib/merchant_api_delete_order.c
@@ -1,6 +1,6 @@
 /*
   This file is part of TALER
-  Copyright (C) 2020 Taler Systems SA
+  Copyright (C) 2020-2022 Taler Systems SA
 
   TALER is free software; you can redistribute it and/or modify it under the
   terms of the GNU Lesser General Public License as published by the Free 
Software
@@ -116,6 +116,7 @@ TALER_MERCHANT_order_delete (
   struct GNUNET_CURL_Context *ctx,
   const char *backend_url,
   const char *order_id,
+  bool force,
   TALER_MERCHANT_OrderDeleteCallback cb,
   void *cb_cls)
 {
@@ -129,8 +130,11 @@ TALER_MERCHANT_order_delete (
     char *path;
 
     GNUNET_asprintf (&path,
-                     "private/orders/%s",
-                     order_id);
+                     "private/orders/%s%s",
+                     order_id,
+                     force
+                     ? "?force=yes"
+                     : "");
 
     odh->url = TALER_url_join (backend_url,
                                path,
diff --git a/src/lib/merchant_api_post_order_pay.c 
b/src/lib/merchant_api_post_order_pay.c
index 4310838f..b5e2c706 100644
--- a/src/lib/merchant_api_post_order_pay.c
+++ b/src/lib/merchant_api_post_order_pay.c
@@ -203,36 +203,34 @@ cert_cb (void *cls,
 
   if (TALER_EXCHANGE_VC_INCOMPATIBLE & compat)
   {
-    struct TALER_MERCHANT_HttpResponse hr = {
-      .http_status = MHD_HTTP_CONFLICT,
-      .exchange_http_status = 0,
-      .ec = TALER_EC_WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
-      .reply = oph->full_reply,
-      .exchange_reply = ehr->reply,
-      .hint = "could not check error: incompatible exchange version"
+    struct TALER_MERCHANT_PayResponse pr = {
+      .hr.http_status = MHD_HTTP_CONFLICT,
+      .hr.exchange_http_status = 0,
+      .hr.ec = TALER_EC_WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+      .hr.reply = oph->full_reply,
+      .hr.exchange_reply = ehr->reply,
+      .hr.hint = "could not check error: incompatible exchange version"
     };
 
     oph->pay_cb (oph->pay_cb_cls,
-                 &hr,
-                 NULL);
+                 &pr);
     TALER_MERCHANT_order_pay_cancel (oph);
     return;
   }
   if ( (MHD_HTTP_OK != ehr->http_status) ||
        (NULL == keys) )
   {
-    struct TALER_MERCHANT_HttpResponse hr = {
-      .http_status = MHD_HTTP_CONFLICT,
-      .exchange_http_status = ehr->http_status,
-      .ec = TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
-      .reply = oph->full_reply,
-      .exchange_reply = ehr->reply,
-      .hint = "failed to download /keys from the exchange"
+    struct TALER_MERCHANT_PayResponse pr = {
+      .hr.http_status = MHD_HTTP_CONFLICT,
+      .hr.exchange_http_status = ehr->http_status,
+      .hr.ec = TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
+      .hr.reply = oph->full_reply,
+      .hr.exchange_reply = ehr->reply,
+      .hr.hint = "failed to download /keys from the exchange"
     };
 
     oph->pay_cb (oph->pay_cb_cls,
-                 &hr,
-                 NULL);
+                 &pr);
     TALER_MERCHANT_order_pay_cancel (oph);
     return;
   }
@@ -241,29 +239,27 @@ cert_cb (void *cls,
       check_conflict (oph,
                       keys))
   {
-    struct TALER_MERCHANT_HttpResponse hr = {
-      .http_status = 0,
-      .ec = TALER_EC_GENERIC_INVALID_RESPONSE,
-      .reply = oph->full_reply
+    struct TALER_MERCHANT_PayResponse pr = {
+      .hr.http_status = 0,
+      .hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE,
+      .hr.reply = oph->full_reply
     };
 
     oph->pay_cb (oph->pay_cb_cls,
-                 &hr,
-                 NULL);
+                 &pr);
     TALER_MERCHANT_order_pay_cancel (oph);
     return;
   }
 
   {
-    struct TALER_MERCHANT_HttpResponse hr = {
-      .http_status = MHD_HTTP_CONFLICT,
-      .ec = TALER_JSON_get_error_code (oph->full_reply),
-      .reply = oph->full_reply
+    struct TALER_MERCHANT_PayResponse pr = {
+      .hr.http_status = MHD_HTTP_CONFLICT,
+      .hr.ec = TALER_JSON_get_error_code (oph->full_reply),
+      .hr.reply = oph->full_reply
     };
 
     oph->pay_cb (oph->pay_cb_cls,
-                 &hr,
-                 NULL);
+                 &pr);
     TALER_MERCHANT_order_pay_cancel (oph);
   }
 }
@@ -274,7 +270,8 @@ cert_cb (void *cls,
  * Now we need to check the provided cryptograophic proof that the
  * coin was actually already spent!
  *
- * @param oph handle of the original pay operation
+ * @param[in,out] oph handle of the original pay operation
+ * @param[in,out] pr response to modify if #GNUNET_OK is returned
  * @param json cryptograophic proof returned by the
  *        exchange/merchant
  * @return #GNUNET_OK if proof checks out,
@@ -283,6 +280,7 @@ cert_cb (void *cls,
  */
 static enum GNUNET_GenericReturnValue
 parse_conflict (struct TALER_MERCHANT_OrderPayHandle *oph,
+                struct TALER_MERCHANT_PayResponse *pr,
                 const json_t *json)
 {
   json_t *ereply;
@@ -299,6 +297,32 @@ parse_conflict (struct TALER_MERCHANT_OrderPayHandle *oph,
                            &oph->error_history),
     GNUNET_JSON_spec_end ()
   };
+  enum TALER_ErrorCode ec = TALER_JSON_get_error_code (json);
+
+  switch (ec)
+  {
+  case TALER_EC_GENERIC_CURRENCY_MISMATCH:
+    /* no proof to check, still very strange, as we
+       should have checked that the currency matches */
+    GNUNET_break_op (0);
+    TALER_MERCHANT_parse_error_details_ (json,
+                                         MHD_HTTP_CONFLICT,
+                                         &pr->hr);
+    return GNUNET_OK;
+  case TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID:
+    /* We can only be happy and accept the result;
+       TODO: parse the refunds... */
+    TALER_MERCHANT_parse_error_details_ (json,
+                                         MHD_HTTP_CONFLICT,
+                                         &pr->hr);
+    return GNUNET_OK;
+  case TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS:
+    /* main case, handled below */
+    break;
+  default:
+    GNUNET_break_op (0);
+    return GNUNET_SYSERR;
+  }
 
   if (GNUNET_OK !=
       GNUNET_JSON_parse (json,
@@ -357,12 +381,10 @@ handle_pay_finished (void *cls,
 {
   struct TALER_MERCHANT_OrderPayHandle *oph = cls;
   const json_t *json = response;
-  struct TALER_MERCHANT_HttpResponse hr = {
-    .http_status = (unsigned int) response_code,
-    .reply = json
+  struct TALER_MERCHANT_PayResponse pr = {
+    .hr.http_status = (unsigned int) response_code,
+    .hr.reply = json
   };
-  struct TALER_MerchantSignatureP *merchant_sigp = NULL;
-  struct TALER_MerchantSignatureP merchant_sig;
 
   oph->job = NULL;
   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
@@ -371,15 +393,16 @@ handle_pay_finished (void *cls,
   switch (response_code)
   {
   case 0:
-    hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+    pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
     break;
   case MHD_HTTP_OK:
     if (oph->am_wallet)
     {
       /* Here we can (and should) verify the merchant's signature */
       struct GNUNET_JSON_Specification spec[] = {
-        GNUNET_JSON_spec_fixed_auto ("sig",
-                                     &merchant_sig),
+        GNUNET_JSON_spec_fixed_auto (
+          "sig",
+          &pr.details.success.merchant_sig),
         GNUNET_JSON_spec_end ()
       };
 
@@ -389,61 +412,60 @@ handle_pay_finished (void *cls,
                              NULL, NULL))
       {
         GNUNET_break_op (0);
-        hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
-        hr.http_status = 0;
-        hr.hint = "sig field missing in response";
+        pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        pr.hr.http_status = 0;
+        pr.hr.hint = "sig field missing in response";
         break;
       }
 
       if (GNUNET_OK !=
           TALER_merchant_pay_verify (&oph->h_contract_terms,
                                      &oph->merchant_pub,
-                                     &merchant_sig))
+                                     &pr.details.success.merchant_sig))
       {
         GNUNET_break_op (0);
-        hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
-        hr.http_status = 0;
-        hr.hint = "signature invalid";
+        pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
+        pr.hr.http_status = 0;
+        pr.hr.hint = "signature invalid";
       }
-      merchant_sigp = &merchant_sig;
     }
     break;
   /* Tolerating Not Acceptable because sometimes
    * - especially in tests - we might want to POST
    * coins one at a time.  */
   case MHD_HTTP_NOT_ACCEPTABLE:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    pr.hr.ec = TALER_JSON_get_error_code (json);
+    pr.hr.hint = TALER_JSON_get_error_hint (json);
     break;
   case MHD_HTTP_BAD_REQUEST:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    pr.hr.ec = TALER_JSON_get_error_code (json);
+    pr.hr.hint = TALER_JSON_get_error_hint (json);
     /* This should never happen, either us
      * or the merchant is buggy (or API version conflict);
      * just pass JSON reply to the application */
     break;
   case MHD_HTTP_PAYMENT_REQUIRED:
     /* was originally paid, but then refunded */
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    pr.hr.ec = TALER_JSON_get_error_code (json);
+    pr.hr.hint = TALER_JSON_get_error_hint (json);
     break;
   case MHD_HTTP_FORBIDDEN:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    pr.hr.ec = TALER_JSON_get_error_code (json);
+    pr.hr.hint = TALER_JSON_get_error_hint (json);
     /* Nothing really to verify, merchant says we tried to abort the payment
      * after it was successful. We should pass the JSON reply to the
      * application */
     break;
   case MHD_HTTP_NOT_FOUND:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    pr.hr.ec = TALER_JSON_get_error_code (json);
+    pr.hr.hint = TALER_JSON_get_error_hint (json);
     /* Nothing really to verify, this should never
        happen, we should pass the JSON reply to the
        application */
     break;
   case MHD_HTTP_REQUEST_TIMEOUT:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    pr.hr.ec = TALER_JSON_get_error_code (json);
+    pr.hr.hint = TALER_JSON_get_error_hint (json);
     /* The merchant couldn't generate a timely response, likely because
        it itself waited too long on the exchange.
        Pass on to application. */
@@ -452,13 +474,13 @@ handle_pay_finished (void *cls,
     {
       enum GNUNET_GenericReturnValue ret;
 
-      hr.ec = TALER_JSON_get_error_code (json);
-      hr.hint = TALER_JSON_get_error_hint (json);
       ret = parse_conflict (oph,
+                            &pr,
                             json);
       switch (ret)
       {
       case GNUNET_OK:
+        /* continued below, 'pr' was modified */
         break;
       case GNUNET_NO:
         /* handled asynchronously! */
@@ -471,8 +493,9 @@ handle_pay_finished (void *cls,
       break;
     }
   case MHD_HTTP_GONE:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    TALER_MERCHANT_parse_error_details_ (json,
+                                         response_code,
+                                         &pr.hr);
     /* The merchant says we are too late, the offer has expired or some
        denomination key of a coin involved has expired.
        Might be a disagreement in timestamps? Still, pass on to application. */
@@ -480,15 +503,16 @@ handle_pay_finished (void *cls,
   case MHD_HTTP_PRECONDITION_FAILED:
     TALER_MERCHANT_parse_error_details_ (json,
                                          response_code,
-                                         &hr);
+                                         &pr.hr);
     /* Nothing really to verify, the merchant is blaming us for failing to
        satisfy some constraint (likely it does not like our exchange because
        of some disagreement on the PKI).  We should pass the JSON reply to the
        application */
     break;
   case MHD_HTTP_INTERNAL_SERVER_ERROR:
-    hr.ec = TALER_JSON_get_error_code (json);
-    hr.hint = TALER_JSON_get_error_hint (json);
+    TALER_MERCHANT_parse_error_details_ (json,
+                                         response_code,
+                                         &pr.hr);
     /* Server had an internal issue; we should retry,
        but this API leaves this to the application */
     break;
@@ -497,37 +521,36 @@ handle_pay_finished (void *cls,
        We should pass the JSON reply to the application */
     TALER_MERCHANT_parse_error_details_ (json,
                                          response_code,
-                                         &hr);
+                                         &pr.hr);
     break;
   case MHD_HTTP_SERVICE_UNAVAILABLE:
     TALER_MERCHANT_parse_error_details_ (json,
                                          response_code,
-                                         &hr);
+                                         &pr.hr);
     /* Exchange couldn't respond properly; the retry is
        left to the application */
     break;
   case MHD_HTTP_GATEWAY_TIMEOUT:
     TALER_MERCHANT_parse_error_details_ (json,
                                          response_code,
-                                         &hr);
+                                         &pr.hr);
     /* Exchange couldn't respond in a timely fashion;
        the retry is left to the application */
     break;
   default:
     TALER_MERCHANT_parse_error_details_ (json,
                                          response_code,
-                                         &hr);
+                                         &pr.hr);
     /* unexpected response code */
     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                 "Unexpected response code %u/%d\n",
                 (unsigned int) response_code,
-                (int) hr.ec);
+                (int) pr.hr.ec);
     GNUNET_break_op (0);
     break;
   }
   oph->pay_cb (oph->pay_cb_cls,
-               &hr,
-               merchant_sigp);
+               &pr);
   TALER_MERCHANT_order_pay_cancel (oph);
 }
 
diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c
index 27129066..c73e025e 100644
--- a/src/testing/test_merchant_api.c
+++ b/src/testing/test_merchant_api.c
@@ -1162,26 +1162,36 @@ run (void *cls,
   };
 
   struct TALER_TESTING_Command pay_again[] = {
-    cmd_transfer_to_exchange ("create-reserve-10",
-                              "EUR:10.02"),
-    cmd_exec_wirewatch ("wirewatch-10"),
+    cmd_transfer_to_exchange ("create-reserve-20",
+                              "EUR:20.04"),
+    cmd_exec_wirewatch ("wirewatch-20"),
     TALER_TESTING_cmd_check_bank_admin_transfer ("check_bank_transfer-10",
-                                                 "EUR:10.02",
+                                                 "EUR:20.04",
                                                  payer_payto,
                                                  exchange_payto,
-                                                 "create-reserve-10"),
+                                                 "create-reserve-20"),
     TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-10a",
-                                       "create-reserve-10",
+                                       "create-reserve-20",
                                        "EUR:5",
                                        0,
                                        MHD_HTTP_OK),
     TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-10b",
-                                       "create-reserve-10",
+                                       "create-reserve-20",
+                                       "EUR:5",
+                                       0,
+                                       MHD_HTTP_OK),
+    TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-10c",
+                                       "create-reserve-20",
+                                       "EUR:5",
+                                       0,
+                                       MHD_HTTP_OK),
+    TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-10d",
+                                       "create-reserve-20",
                                        "EUR:5",
                                        0,
                                        MHD_HTTP_OK),
-    TALER_TESTING_cmd_status ("withdraw-status-10",
-                              "create-reserve-10",
+    TALER_TESTING_cmd_status ("withdraw-status-20",
+                              "create-reserve-20",
                               "EUR:0",
                               MHD_HTTP_OK),
     TALER_TESTING_cmd_merchant_post_orders ("create-proposal-10",
@@ -1207,6 +1217,14 @@ run (void *cls,
                                           "EUR:5",
                                           "EUR:4.99",
                                           NULL),
+    TALER_TESTING_cmd_merchant_pay_order ("pay-too-much-10",
+                                          merchant_url,
+                                          MHD_HTTP_CONFLICT,
+                                          "create-proposal-10",
+                                          
"withdraw-coin-10c;withdraw-coin-10d",
+                                          "EUR:5",
+                                          "EUR:4.99",
+                                          NULL),
     CMD_EXEC_AGGREGATOR ("run-aggregator-10"),
     TALER_TESTING_cmd_check_bank_transfer ("check_bank_transfer-9.97-10",
                                            EXCHANGE_URL,
diff --git a/src/testing/testing_api_cmd_delete_order.c 
b/src/testing/testing_api_cmd_delete_order.c
index d5d8b283..586b348c 100644
--- a/src/testing/testing_api_cmd_delete_order.c
+++ b/src/testing/testing_api_cmd_delete_order.c
@@ -124,6 +124,7 @@ delete_order_run (void *cls,
   dos->odh = TALER_MERCHANT_order_delete (is->ctx,
                                           dos->merchant_url,
                                           dos->order_id,
+                                          false, /* TODO: support testing 
force... */
                                           &delete_order_cb,
                                           dos);
   GNUNET_assert (NULL != dos->odh);
diff --git a/src/testing/testing_api_cmd_pay_order.c 
b/src/testing/testing_api_cmd_pay_order.c
index be2649c0..61a43b9e 100644
--- a/src/testing/testing_api_cmd_pay_order.c
+++ b/src/testing/testing_api_cmd_pay_order.c
@@ -184,8 +184,8 @@ build_coins (struct TALER_MERCHANT_PayCoin **pc,
                                                      &denom_value));
       GNUNET_assert (GNUNET_OK ==
                      TALER_TESTING_get_trait_h_age_commitment (coin_cmd,
-                                                              0,
-                                                              
&h_age_commitment));
+                                                               0,
+                                                               
&h_age_commitment));
       icoin->coin_priv = *coin_priv;
       icoin->denom_pub = denom_pub->key;
       icoin->denom_sig = *denom_sig;
@@ -218,30 +218,27 @@ build_coins (struct TALER_MERCHANT_PayCoin **pc,
  * HTTP response code matches our expectation.
  *
  * @param cls closure with the interpreter state
- * @param hr HTTP response
- * @param merchant_sig signature affirming payment,
- *        NULL on errors
+ * @param pr HTTP response
  */
 static void
 pay_cb (void *cls,
-        const struct TALER_MERCHANT_HttpResponse *hr,
-        const struct TALER_MerchantSignatureP *merchant_sig)
+        const struct TALER_MERCHANT_PayResponse *pr)
 {
   struct PayState *ps = cls;
 
   ps->oph = NULL;
-  if (ps->http_status != hr->http_status)
+  if (ps->http_status != pr->hr.http_status)
   {
     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                 "Unexpected response code %u (%d) to command %s\n",
-                hr->http_status,
-                (int) hr->ec,
+                pr->hr.http_status,
+                (int) pr->hr.ec,
                 TALER_TESTING_interpreter_get_current_label (ps->is));
     TALER_TESTING_FAIL (ps->is);
   }
-  if (MHD_HTTP_OK == hr->http_status)
+  if (MHD_HTTP_OK == pr->hr.http_status)
   {
-    ps->merchant_sig = *merchant_sig;
+    ps->merchant_sig = pr->details.success.merchant_sig;
   }
   TALER_TESTING_interpreter_next (ps->is);
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]