merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

increase_refund.c (20094B)


      1 /*
      2    This file is part of TALER
      3    Copyright (C) 2022-2024 Taler Systems SA
      4 
      5    TALER is free software; you can redistribute it and/or modify it under the
      6    terms of the GNU General Public License as published by the Free Software
      7    Foundation; either version 3, or (at your option) any later version.
      8 
      9    TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11    A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13    You should have received a copy of the GNU General Public License along with
     14    TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 /**
     17  * @file src/backenddb/increase_refund.c
     18  * @brief Implementation of the increase_refund function for Postgres
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include <taler/taler_error_codes.h>
     23 #include <taler/taler_dbevents.h>
     24 #include <taler/taler_pq_lib.h>
     25 #include "merchant-database/increase_refund.h"
     26 #include "helper.h"
     27 
     28 
     29 /**
     30  * Information about refund limits per exchange.
     31  */
     32 struct ExchangeLimit
     33 {
     34   /**
     35    * Kept in a DLL.
     36    */
     37   struct ExchangeLimit *next;
     38 
     39   /**
     40    * Kept in a DLL.
     41    */
     42   struct ExchangeLimit *prev;
     43 
     44   /**
     45    * Exchange the limit is about.
     46    */
     47   char *exchange_url;
     48 
     49   /**
     50    * Refund amount remaining at this exchange.
     51    */
     52   struct TALER_Amount remaining_refund_limit;
     53 
     54 };
     55 
     56 
     57 /**
     58  * Closure for #process_refund_cb().
     59  */
     60 struct FindRefundContext
     61 {
     62 
     63   /**
     64    * Plugin context.
     65    */
     66   struct TALER_MERCHANTDB_PostgresContext *pg;
     67 
     68   /**
     69    * Updated to reflect total amount refunded so far.
     70    */
     71   struct TALER_Amount refunded_amount;
     72 
     73   /**
     74    * Set to the largest refund transaction ID encountered.
     75    */
     76   uint64_t max_rtransaction_id;
     77 
     78   /**
     79    * Set to true on hard errors.
     80    */
     81   bool err;
     82 };
     83 
     84 
     85 /**
     86  * Closure for #process_deposits_for_refund_cb().
     87  */
     88 struct InsertRefundContext
     89 {
     90   /**
     91    * Used to provide a connection to the db
     92    */
     93   struct TALER_MERCHANTDB_PostgresContext *pg;
     94 
     95   /**
     96    * Head of DLL of per-exchange refund limits.
     97    */
     98   struct ExchangeLimit *el_head;
     99 
    100   /**
    101    * Tail of DLL of per-exchange refund limits.
    102    */
    103   struct ExchangeLimit *el_tail;
    104 
    105   /**
    106    * Amount to which increase the refund for this contract
    107    */
    108   const struct TALER_Amount *refund;
    109 
    110   /**
    111    * Human-readable reason behind this refund
    112    */
    113   const char *reason;
    114 
    115   /**
    116    * Function to call to determine per-exchange limits.
    117    * NULL for no limits.
    118    */
    119   TALER_MERCHANTDB_OperationLimitCallback olc;
    120 
    121   /**
    122    * Closure for @e olc.
    123    */
    124   void *olc_cls;
    125 
    126   /**
    127    * Transaction status code.
    128    */
    129   enum TALER_MERCHANTDB_RefundStatus rs;
    130 
    131   /**
    132    * Did we have to cap refunds of any coin
    133    * due to legal limits?
    134    */
    135   bool legal_capped;
    136 };
    137 
    138 
    139 /**
    140  * Data extracted per coin.
    141  */
    142 struct RefundCoinData
    143 {
    144 
    145   /**
    146    * Public key of a coin.
    147    */
    148   struct TALER_CoinSpendPublicKeyP coin_pub;
    149 
    150   /**
    151    * Amount deposited for this coin.
    152    */
    153   struct TALER_Amount deposited_with_fee;
    154 
    155   /**
    156    * Amount refunded already for this coin.
    157    */
    158   struct TALER_Amount refund_amount;
    159 
    160   /**
    161    * Order serial (actually not really per-coin).
    162    */
    163   uint64_t order_serial;
    164 
    165   /**
    166    * Maximum rtransaction_id for this coin so far.
    167    */
    168   uint64_t max_rtransaction_id;
    169 
    170   /**
    171    * Exchange this coin was issued by.
    172    */
    173   char *exchange_url;
    174 
    175 };
    176 
    177 
    178 /**
    179  * Find an exchange record for the refund limit enforcement.
    180  *
    181  * @param irc refund context
    182  * @param exchange_url base URL of the exchange
    183  */
    184 static struct ExchangeLimit *
    185 find_exchange (struct InsertRefundContext *irc,
    186                const char *exchange_url)
    187 {
    188   if (NULL == irc->olc)
    189     return NULL; /* no limits */
    190   /* Check if entry exists, if so, do nothing */
    191   for (struct ExchangeLimit *el = irc->el_head;
    192        NULL != el;
    193        el = el->next)
    194     if (0 == strcmp (exchange_url,
    195                      el->exchange_url))
    196       return el;
    197   return NULL;
    198 }
    199 
    200 
    201 /**
    202  * Setup an exchange for the refund limit enforcement and initialize the
    203  * original refund limit for the exchange.
    204  *
    205  * @param irc refund context
    206  * @param exchange_url base URL of the exchange
    207  * @return limiting data structure
    208  */
    209 static struct ExchangeLimit *
    210 setup_exchange (struct InsertRefundContext *irc,
    211                 const char *exchange_url)
    212 {
    213   struct ExchangeLimit *el;
    214 
    215   if (NULL == irc->olc)
    216     return NULL; /* no limits */
    217   /* Check if entry exists, if so, do nothing */
    218   if (NULL !=
    219       (el = find_exchange (irc,
    220                            exchange_url)))
    221     return el;
    222   el = GNUNET_new (struct ExchangeLimit);
    223   el->exchange_url = GNUNET_strdup (exchange_url);
    224   /* olc only lowers, so set to the maximum amount we care about */
    225   el->remaining_refund_limit = *irc->refund;
    226   irc->olc (irc->olc_cls,
    227             exchange_url,
    228             &el->remaining_refund_limit);
    229   GNUNET_CONTAINER_DLL_insert (irc->el_head,
    230                                irc->el_tail,
    231                                el);
    232   return el;
    233 }
    234 
    235 
    236 /**
    237  * Lower the remaining refund limit in @a el by @a val.
    238  *
    239  * @param[in,out] el exchange limit to lower
    240  * @param val amount to lower limit by
    241  * @return true on success, false on failure
    242  */
    243 static bool
    244 lower_balance (struct ExchangeLimit *el,
    245                const struct TALER_Amount *val)
    246 {
    247   if (NULL == el)
    248     return true;
    249   return 0 <= TALER_amount_subtract (&el->remaining_refund_limit,
    250                                      &el->remaining_refund_limit,
    251                                      val);
    252 }
    253 
    254 
    255 /**
    256  * Function to be called with the results of a SELECT statement
    257  * that has returned @a num_results results.
    258  *
    259  * @param cls closure, our `struct FindRefundContext`
    260  * @param result the postgres result
    261  * @param num_results the number of results in @a result
    262  */
    263 static void
    264 process_refund_cb (void *cls,
    265                    PGresult *result,
    266                    unsigned int num_results)
    267 {
    268   struct FindRefundContext *ictx = cls;
    269 
    270   for (unsigned int i = 0; i<num_results; i++)
    271   {
    272     /* Sum up existing refunds */
    273     struct TALER_Amount acc;
    274     uint64_t rtransaction_id;
    275     struct GNUNET_PQ_ResultSpec rs[] = {
    276       TALER_PQ_result_spec_amount_with_currency ("refund_amount",
    277                                                  &acc),
    278       GNUNET_PQ_result_spec_uint64 ("rtransaction_id",
    279                                     &rtransaction_id),
    280       GNUNET_PQ_result_spec_end
    281     };
    282 
    283     if (GNUNET_OK !=
    284         GNUNET_PQ_extract_result (result,
    285                                   rs,
    286                                   i))
    287     {
    288       GNUNET_break (0);
    289       ictx->err = true;
    290       return;
    291     }
    292     if (GNUNET_OK !=
    293         TALER_amount_cmp_currency (&ictx->refunded_amount,
    294                                    &acc))
    295     {
    296       GNUNET_break (0);
    297       ictx->err = true;
    298       return;
    299     }
    300     if (0 >
    301         TALER_amount_add (&ictx->refunded_amount,
    302                           &ictx->refunded_amount,
    303                           &acc))
    304     {
    305       GNUNET_break (0);
    306       ictx->err = true;
    307       return;
    308     }
    309     ictx->max_rtransaction_id = GNUNET_MAX (ictx->max_rtransaction_id,
    310                                             rtransaction_id);
    311     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    312                 "Found refund of %s\n",
    313                 TALER_amount2s (&acc));
    314   }
    315 }
    316 
    317 
    318 /**
    319  * Function to be called with the results of a SELECT statement
    320  * that has returned @a num_results results.
    321  *
    322  * @param cls closure, our `struct InsertRefundContext`
    323  * @param result the postgres result
    324  * @param num_results the number of results in @a result
    325  */
    326 static void
    327 process_deposits_for_refund_cb (void *cls,
    328                                 PGresult *result,
    329                                 unsigned int num_results)
    330 {
    331   struct InsertRefundContext *ctx = cls;
    332   struct TALER_MERCHANTDB_PostgresContext *pg = ctx->pg;
    333   struct TALER_Amount current_refund;
    334   struct RefundCoinData rcd[GNUNET_NZL (num_results)];
    335   struct GNUNET_TIME_Timestamp now;
    336 
    337   now = GNUNET_TIME_timestamp_get ();
    338   GNUNET_assert (GNUNET_OK ==
    339                  TALER_amount_set_zero (ctx->refund->currency,
    340                                         &current_refund));
    341   memset (rcd,
    342           0,
    343           sizeof (rcd));
    344   /* Pass 1:  Collect amount of existing refunds into current_refund.
    345    * Also store existing refunded amount for each deposit in deposit_refund. */
    346   for (unsigned int i = 0; i<num_results; i++)
    347   {
    348     struct RefundCoinData *rcdi = &rcd[i];
    349     struct GNUNET_PQ_ResultSpec rs[] = {
    350       GNUNET_PQ_result_spec_auto_from_type ("coin_pub",
    351                                             &rcdi->coin_pub),
    352       GNUNET_PQ_result_spec_uint64 ("order_serial",
    353                                     &rcdi->order_serial),
    354       GNUNET_PQ_result_spec_string ("exchange_url",
    355                                     &rcdi->exchange_url),
    356       TALER_PQ_result_spec_amount_with_currency ("amount_with_fee",
    357                                                  &rcdi->deposited_with_fee),
    358       GNUNET_PQ_result_spec_end
    359     };
    360     struct FindRefundContext ictx = {
    361       .pg = pg
    362     };
    363     struct ExchangeLimit *el;
    364 
    365     if (GNUNET_OK !=
    366         GNUNET_PQ_extract_result (result,
    367                                   rs,
    368                                   i))
    369     {
    370       GNUNET_break (0);
    371       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    372       goto cleanup;
    373     }
    374     el = setup_exchange (ctx,
    375                          rcdi->exchange_url);
    376     if (0 != strcmp (rcdi->deposited_with_fee.currency,
    377                      ctx->refund->currency))
    378     {
    379       GNUNET_break_op (0);
    380       ctx->rs = TALER_MERCHANTDB_RS_BAD_CURRENCY;
    381       goto cleanup;
    382     }
    383 
    384     {
    385       enum GNUNET_DB_QueryStatus ires;
    386       struct GNUNET_PQ_QueryParam params[] = {
    387         GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub),
    388         GNUNET_PQ_query_param_uint64 (&rcdi->order_serial),
    389         GNUNET_PQ_query_param_end
    390       };
    391 
    392       GNUNET_assert (GNUNET_OK ==
    393                      TALER_amount_set_zero (
    394                        ctx->refund->currency,
    395                        &ictx.refunded_amount));
    396       ires = GNUNET_PQ_eval_prepared_multi_select (
    397         pg->conn,
    398         "find_refunds_by_coin",
    399         params,
    400         &process_refund_cb,
    401         &ictx);
    402       if ( (ictx.err) ||
    403            (GNUNET_DB_STATUS_HARD_ERROR == ires) )
    404       {
    405         GNUNET_break (0);
    406         ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    407         goto cleanup;
    408       }
    409       if (GNUNET_DB_STATUS_SOFT_ERROR == ires)
    410       {
    411         ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR;
    412         goto cleanup;
    413       }
    414     }
    415     if (0 >
    416         TALER_amount_add (&current_refund,
    417                           &current_refund,
    418                           &ictx.refunded_amount))
    419     {
    420       GNUNET_break (0);
    421       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    422       goto cleanup;
    423     }
    424     rcdi->refund_amount = ictx.refunded_amount;
    425     rcdi->max_rtransaction_id = ictx.max_rtransaction_id;
    426     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    427                 "Existing refund for coin %s is %s\n",
    428                 TALER_B2S (&rcdi->coin_pub),
    429                 TALER_amount2s (&ictx.refunded_amount));
    430     GNUNET_break (lower_balance (el,
    431                                  &ictx.refunded_amount));
    432   } /* end for all deposited coins */
    433 
    434   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    435               "Total existing refund is %s\n",
    436               TALER_amount2s (&current_refund));
    437 
    438   /* stop immediately if we are 'done' === amount already
    439    * refunded.  */
    440   if (0 >= TALER_amount_cmp (ctx->refund,
    441                              &current_refund))
    442   {
    443     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    444                 "Existing refund of %s at or above requested refund. Finished early.\n",
    445                 TALER_amount2s (&current_refund));
    446     ctx->rs = TALER_MERCHANTDB_RS_SUCCESS;
    447     goto cleanup;
    448   }
    449 
    450   /* Phase 2:  Try to increase current refund until it matches desired refund */
    451   for (unsigned int i = 0; i<num_results; i++)
    452   {
    453     struct RefundCoinData *rcdi = &rcd[i];
    454     const struct TALER_Amount *increment;
    455     struct TALER_Amount left;
    456     struct TALER_Amount remaining_refund;
    457     struct ExchangeLimit *el;
    458 
    459     /* How much of the coin is left after the existing refunds? */
    460     if (0 >
    461         TALER_amount_subtract (&left,
    462                                &rcdi->deposited_with_fee,
    463                                &rcdi->refund_amount))
    464     {
    465       GNUNET_break (0);
    466       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    467       goto cleanup;
    468     }
    469 
    470     if (TALER_amount_is_zero (&left))
    471     {
    472       /* coin was fully refunded, move to next coin */
    473       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    474                   "Coin %s fully refunded, moving to next coin\n",
    475                   TALER_B2S (&rcdi->coin_pub));
    476       continue;
    477     }
    478     el = find_exchange (ctx,
    479                         rcdi->exchange_url);
    480     if ( (NULL != el) &&
    481          (TALER_amount_is_zero (&el->remaining_refund_limit)) )
    482     {
    483       /* legal limit reached, move to next coin */
    484       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    485                   "Exchange %s legal limit reached, moving to next coin\n",
    486                   rcdi->exchange_url);
    487       continue;
    488     }
    489 
    490     rcdi->max_rtransaction_id++;
    491     /* How much of the refund is still to be paid back? */
    492     if (0 >
    493         TALER_amount_subtract (&remaining_refund,
    494                                ctx->refund,
    495                                &current_refund))
    496     {
    497       GNUNET_break (0);
    498       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    499       goto cleanup;
    500     }
    501     /* cap by legal limit */
    502     if (NULL != el)
    503     {
    504       struct TALER_Amount new_limit;
    505 
    506       TALER_amount_min (&new_limit,
    507                         &remaining_refund,
    508                         &el->remaining_refund_limit);
    509       if (0 != TALER_amount_cmp (&new_limit,
    510                                  &remaining_refund))
    511       {
    512         remaining_refund = new_limit;
    513         ctx->legal_capped = true;
    514       }
    515     }
    516     /* By how much will we increase the refund for this coin? */
    517     if (0 >= TALER_amount_cmp (&remaining_refund,
    518                                &left))
    519     {
    520       /* remaining_refund <= left */
    521       increment = &remaining_refund;
    522     }
    523     else
    524     {
    525       increment = &left;
    526     }
    527 
    528     if (0 >
    529         TALER_amount_add (&current_refund,
    530                           &current_refund,
    531                           increment))
    532     {
    533       GNUNET_break (0);
    534       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    535       goto cleanup;
    536     }
    537     GNUNET_break (lower_balance (el,
    538                                  increment));
    539     /* actually run the refund */
    540     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    541                 "Coin %s deposit amount is %s\n",
    542                 TALER_B2S (&rcdi->coin_pub),
    543                 TALER_amount2s (&rcdi->deposited_with_fee));
    544     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    545                 "Coin %s refund will be incremented by %s\n",
    546                 TALER_B2S (&rcdi->coin_pub),
    547                 TALER_amount2s (increment));
    548     {
    549       enum GNUNET_DB_QueryStatus qs;
    550       struct GNUNET_PQ_QueryParam params[] = {
    551         GNUNET_PQ_query_param_uint64 (&rcdi->order_serial),
    552         GNUNET_PQ_query_param_uint64 (&rcdi->max_rtransaction_id), /* already inc'ed */
    553         GNUNET_PQ_query_param_timestamp (&now),
    554         GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub),
    555         GNUNET_PQ_query_param_string (ctx->reason),
    556         TALER_PQ_query_param_amount_with_currency (pg->conn,
    557                                                    increment),
    558         GNUNET_PQ_query_param_end
    559       };
    560 
    561       check_connection (pg);
    562       qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
    563                                                "insert_refund",
    564                                                params);
    565       switch (qs)
    566       {
    567       case GNUNET_DB_STATUS_HARD_ERROR:
    568         GNUNET_break (0);
    569         ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    570         goto cleanup;
    571       case GNUNET_DB_STATUS_SOFT_ERROR:
    572         ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR;
    573         goto cleanup;
    574       default:
    575         ctx->rs = (enum TALER_MERCHANTDB_RefundStatus) qs;
    576         break;
    577       }
    578     }
    579 
    580     /* stop immediately if we are done */
    581     if (0 == TALER_amount_cmp (ctx->refund,
    582                                &current_refund))
    583     {
    584       ctx->rs = TALER_MERCHANTDB_RS_SUCCESS;
    585       goto cleanup;
    586     }
    587   }
    588 
    589   if (ctx->legal_capped)
    590   {
    591     ctx->rs = TALER_MERCHANTDB_RS_LEGAL_FAILURE;
    592     goto cleanup;
    593   }
    594   /**
    595    * We end up here if not all of the refund has been covered.
    596    * Although this should be checked as the business should never
    597    * issue a refund bigger than the contract's actual price, we cannot
    598    * rely upon the frontend being correct.
    599    */
    600   GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    601               "The refund of %s is bigger than the order's value\n",
    602               TALER_amount2s (ctx->refund));
    603   ctx->rs = TALER_MERCHANTDB_RS_TOO_HIGH;
    604 cleanup:
    605   for (unsigned int i = 0; i<num_results; i++)
    606     GNUNET_free (rcd[i].exchange_url);
    607 }
    608 
    609 
    610 enum TALER_MERCHANTDB_RefundStatus
    611 TALER_MERCHANTDB_increase_refund (
    612   struct TALER_MERCHANTDB_PostgresContext *pg,
    613   const char *instance_id,
    614   const char *order_id,
    615   const struct TALER_Amount *refund,
    616   TALER_MERCHANTDB_OperationLimitCallback olc,
    617   void *olc_cls,
    618   const char *reason)
    619 {
    620   enum GNUNET_DB_QueryStatus qs;
    621   struct GNUNET_PQ_QueryParam params[] = {
    622     GNUNET_PQ_query_param_string (instance_id),
    623     GNUNET_PQ_query_param_string (order_id),
    624     GNUNET_PQ_query_param_end
    625   };
    626   struct InsertRefundContext ctx = {
    627     .pg = pg,
    628     .refund = refund,
    629     .olc = olc,
    630     .olc_cls = olc_cls,
    631     .reason = reason
    632   };
    633 
    634   // FIXME: return 'refund_serial' from this INSERT statement for #10577
    635   PREPARE (pg,
    636            "insert_refund",
    637            "INSERT INTO merchant_refunds"
    638            "(order_serial"
    639            ",rtransaction_id"
    640            ",refund_timestamp"
    641            ",coin_pub"
    642            ",reason"
    643            ",refund_amount"
    644            ") VALUES"
    645            "($1, $2, $3, $4, $5, $6)");
    646   PREPARE (pg,
    647            "find_refunds_by_coin",
    648            "SELECT"
    649            " refund_amount"
    650            ",rtransaction_id"
    651            " FROM merchant_refunds"
    652            " WHERE coin_pub=$1"
    653            "   AND order_serial=$2");
    654   PREPARE (pg,
    655            "find_deposits_for_refund",
    656            "SELECT"
    657            " dep.coin_pub"
    658            ",dco.order_serial"
    659            ",dep.amount_with_fee"
    660            ",dco.exchange_url"
    661            " FROM merchant_deposits dep"
    662            " JOIN merchant_deposit_confirmations dco"
    663            "   USING (deposit_confirmation_serial)"
    664            " WHERE order_serial="
    665            "  (SELECT order_serial"
    666            "     FROM merchant_contract_terms"
    667            "    WHERE order_id=$2"
    668            "      AND paid"
    669            "      AND merchant_serial="
    670            "        (SELECT merchant_serial"
    671            "           FROM merchant_instances"
    672            "          WHERE merchant_id=$1))");
    673   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    674               "Asked to refund %s on order %s\n",
    675               TALER_amount2s (refund),
    676               order_id);
    677   qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
    678                                              "find_deposits_for_refund",
    679                                              params,
    680                                              &process_deposits_for_refund_cb,
    681                                              &ctx);
    682   {
    683     struct ExchangeLimit *el;
    684 
    685     while (NULL != (el = ctx.el_head))
    686     {
    687       GNUNET_CONTAINER_DLL_remove (ctx.el_head,
    688                                    ctx.el_tail,
    689                                    el);
    690       GNUNET_free (el->exchange_url);
    691       GNUNET_free (el);
    692     }
    693   }
    694   switch (qs)
    695   {
    696   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    697     /* never paid, means we clearly cannot refund anything */
    698     return TALER_MERCHANTDB_RS_NO_SUCH_ORDER;
    699   case GNUNET_DB_STATUS_SOFT_ERROR:
    700     return TALER_MERCHANTDB_RS_SOFT_ERROR;
    701   case GNUNET_DB_STATUS_HARD_ERROR:
    702     return TALER_MERCHANTDB_RS_HARD_ERROR;
    703   default:
    704     /* Got one or more deposits */
    705     return ctx.rs;
    706   }
    707 }