merchant

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

pg_increase_refund.c (19970B)


      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 backenddb/pg_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 "pg_increase_refund.h"
     26 #include "pg_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 PostgresClosure *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 PostgresClosure *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 (
    328   void *cls,
    329   PGresult *result,
    330   unsigned int num_results)
    331 {
    332   struct InsertRefundContext *ctx = cls;
    333   struct PostgresClosure *pg = ctx->pg;
    334   struct TALER_Amount current_refund;
    335   struct RefundCoinData rcd[GNUNET_NZL (num_results)];
    336   struct GNUNET_TIME_Timestamp now;
    337 
    338   now = GNUNET_TIME_timestamp_get ();
    339   GNUNET_assert (GNUNET_OK ==
    340                  TALER_amount_set_zero (ctx->refund->currency,
    341                                         &current_refund));
    342   memset (rcd,
    343           0,
    344           sizeof (rcd));
    345   /* Pass 1:  Collect amount of existing refunds into current_refund.
    346    * Also store existing refunded amount for each deposit in deposit_refund. */
    347   for (unsigned int i = 0; i<num_results; i++)
    348   {
    349     struct RefundCoinData *rcdi = &rcd[i];
    350     struct GNUNET_PQ_ResultSpec rs[] = {
    351       GNUNET_PQ_result_spec_auto_from_type ("coin_pub",
    352                                             &rcdi->coin_pub),
    353       GNUNET_PQ_result_spec_uint64 ("order_serial",
    354                                     &rcdi->order_serial),
    355       GNUNET_PQ_result_spec_string ("exchange_url",
    356                                     &rcdi->exchange_url),
    357       TALER_PQ_result_spec_amount_with_currency ("amount_with_fee",
    358                                                  &rcdi->deposited_with_fee),
    359       GNUNET_PQ_result_spec_end
    360     };
    361     struct FindRefundContext ictx = {
    362       .pg = pg
    363     };
    364     struct ExchangeLimit *el;
    365 
    366     if (GNUNET_OK !=
    367         GNUNET_PQ_extract_result (result,
    368                                   rs,
    369                                   i))
    370     {
    371       GNUNET_break (0);
    372       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    373       goto cleanup;
    374     }
    375     el = setup_exchange (ctx,
    376                          rcdi->exchange_url);
    377     if (0 != strcmp (rcdi->deposited_with_fee.currency,
    378                      ctx->refund->currency))
    379     {
    380       GNUNET_break_op (0);
    381       ctx->rs = TALER_MERCHANTDB_RS_BAD_CURRENCY;
    382       goto cleanup;
    383     }
    384 
    385     {
    386       enum GNUNET_DB_QueryStatus ires;
    387       struct GNUNET_PQ_QueryParam params[] = {
    388         GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub),
    389         GNUNET_PQ_query_param_uint64 (&rcdi->order_serial),
    390         GNUNET_PQ_query_param_end
    391       };
    392 
    393       GNUNET_assert (GNUNET_OK ==
    394                      TALER_amount_set_zero (
    395                        ctx->refund->currency,
    396                        &ictx.refunded_amount));
    397       ires = GNUNET_PQ_eval_prepared_multi_select (
    398         ctx->pg->conn,
    399         "find_refunds_by_coin",
    400         params,
    401         &process_refund_cb,
    402         &ictx);
    403       if ( (ictx.err) ||
    404            (GNUNET_DB_STATUS_HARD_ERROR == ires) )
    405       {
    406         GNUNET_break (0);
    407         ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    408         goto cleanup;
    409       }
    410       if (GNUNET_DB_STATUS_SOFT_ERROR == ires)
    411       {
    412         ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR;
    413         goto cleanup;
    414       }
    415     }
    416     if (0 >
    417         TALER_amount_add (&current_refund,
    418                           &current_refund,
    419                           &ictx.refunded_amount))
    420     {
    421       GNUNET_break (0);
    422       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    423       goto cleanup;
    424     }
    425     rcdi->refund_amount = ictx.refunded_amount;
    426     rcdi->max_rtransaction_id = ictx.max_rtransaction_id;
    427     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    428                 "Existing refund for coin %s is %s\n",
    429                 TALER_B2S (&rcdi->coin_pub),
    430                 TALER_amount2s (&ictx.refunded_amount));
    431     GNUNET_break (lower_balance (el,
    432                                  &ictx.refunded_amount));
    433   } /* end for all deposited coins */
    434 
    435   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    436               "Total existing refund is %s\n",
    437               TALER_amount2s (&current_refund));
    438 
    439   /* stop immediately if we are 'done' === amount already
    440    * refunded.  */
    441   if (0 >= TALER_amount_cmp (ctx->refund,
    442                              &current_refund))
    443   {
    444     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    445                 "Existing refund of %s at or above requested refund. Finished early.\n",
    446                 TALER_amount2s (&current_refund));
    447     ctx->rs = TALER_MERCHANTDB_RS_SUCCESS;
    448     goto cleanup;
    449   }
    450 
    451   /* Phase 2:  Try to increase current refund until it matches desired refund */
    452   for (unsigned int i = 0; i<num_results; i++)
    453   {
    454     struct RefundCoinData *rcdi = &rcd[i];
    455     const struct TALER_Amount *increment;
    456     struct TALER_Amount left;
    457     struct TALER_Amount remaining_refund;
    458     struct ExchangeLimit *el;
    459 
    460     /* How much of the coin is left after the existing refunds? */
    461     if (0 >
    462         TALER_amount_subtract (&left,
    463                                &rcdi->deposited_with_fee,
    464                                &rcdi->refund_amount))
    465     {
    466       GNUNET_break (0);
    467       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    468       goto cleanup;
    469     }
    470 
    471     if (TALER_amount_is_zero (&left))
    472     {
    473       /* coin was fully refunded, move to next coin */
    474       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    475                   "Coin %s fully refunded, moving to next coin\n",
    476                   TALER_B2S (&rcdi->coin_pub));
    477       continue;
    478     }
    479     el = find_exchange (ctx,
    480                         rcdi->exchange_url);
    481     if ( (NULL != el) &&
    482          (TALER_amount_is_zero (&el->remaining_refund_limit)) )
    483     {
    484       /* legal limit reached, move to next coin */
    485       GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    486                   "Exchange %s legal limit reached, moving to next coin\n",
    487                   rcdi->exchange_url);
    488       continue;
    489     }
    490 
    491     rcdi->max_rtransaction_id++;
    492     /* How much of the refund is still to be paid back? */
    493     if (0 >
    494         TALER_amount_subtract (&remaining_refund,
    495                                ctx->refund,
    496                                &current_refund))
    497     {
    498       GNUNET_break (0);
    499       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    500       goto cleanup;
    501     }
    502     /* cap by legal limit */
    503     if (NULL != el)
    504     {
    505       struct TALER_Amount new_limit;
    506 
    507       TALER_amount_min (&new_limit,
    508                         &remaining_refund,
    509                         &el->remaining_refund_limit);
    510       if (0 != TALER_amount_cmp (&new_limit,
    511                                  &remaining_refund))
    512       {
    513         remaining_refund = new_limit;
    514         ctx->legal_capped = true;
    515       }
    516     }
    517     /* By how much will we increase the refund for this coin? */
    518     if (0 >= TALER_amount_cmp (&remaining_refund,
    519                                &left))
    520     {
    521       /* remaining_refund <= left */
    522       increment = &remaining_refund;
    523     }
    524     else
    525     {
    526       increment = &left;
    527     }
    528 
    529     if (0 >
    530         TALER_amount_add (&current_refund,
    531                           &current_refund,
    532                           increment))
    533     {
    534       GNUNET_break (0);
    535       ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    536       goto cleanup;
    537     }
    538     GNUNET_break (lower_balance (el,
    539                                  increment));
    540     /* actually run the refund */
    541     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    542                 "Coin %s deposit amount is %s\n",
    543                 TALER_B2S (&rcdi->coin_pub),
    544                 TALER_amount2s (&rcdi->deposited_with_fee));
    545     GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    546                 "Coin %s refund will be incremented by %s\n",
    547                 TALER_B2S (&rcdi->coin_pub),
    548                 TALER_amount2s (increment));
    549     {
    550       enum GNUNET_DB_QueryStatus qs;
    551       struct GNUNET_PQ_QueryParam params[] = {
    552         GNUNET_PQ_query_param_uint64 (&rcdi->order_serial),
    553         GNUNET_PQ_query_param_uint64 (&rcdi->max_rtransaction_id), /* already inc'ed */
    554         GNUNET_PQ_query_param_timestamp (&now),
    555         GNUNET_PQ_query_param_auto_from_type (&rcdi->coin_pub),
    556         GNUNET_PQ_query_param_string (ctx->reason),
    557         TALER_PQ_query_param_amount_with_currency (pg->conn,
    558                                                    increment),
    559         GNUNET_PQ_query_param_end
    560       };
    561 
    562       check_connection (pg);
    563       qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
    564                                                "insert_refund",
    565                                                params);
    566       switch (qs)
    567       {
    568       case GNUNET_DB_STATUS_HARD_ERROR:
    569         GNUNET_break (0);
    570         ctx->rs = TALER_MERCHANTDB_RS_HARD_ERROR;
    571         goto cleanup;
    572       case GNUNET_DB_STATUS_SOFT_ERROR:
    573         ctx->rs = TALER_MERCHANTDB_RS_SOFT_ERROR;
    574         goto cleanup;
    575       default:
    576         ctx->rs = (enum TALER_MERCHANTDB_RefundStatus) qs;
    577         break;
    578       }
    579     }
    580 
    581     /* stop immediately if we are done */
    582     if (0 == TALER_amount_cmp (ctx->refund,
    583                                &current_refund))
    584     {
    585       ctx->rs = TALER_MERCHANTDB_RS_SUCCESS;
    586       goto cleanup;
    587     }
    588   }
    589 
    590   if (ctx->legal_capped)
    591   {
    592     ctx->rs = TALER_MERCHANTDB_RS_LEGAL_FAILURE;
    593     goto cleanup;
    594   }
    595   /**
    596    * We end up here if not all of the refund has been covered.
    597    * Although this should be checked as the business should never
    598    * issue a refund bigger than the contract's actual price, we cannot
    599    * rely upon the frontend being correct.
    600    */
    601   GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    602               "The refund of %s is bigger than the order's value\n",
    603               TALER_amount2s (ctx->refund));
    604   ctx->rs = TALER_MERCHANTDB_RS_TOO_HIGH;
    605 cleanup:
    606   for (unsigned int i = 0; i<num_results; i++)
    607     GNUNET_free (rcd[i].exchange_url);
    608 }
    609 
    610 
    611 enum TALER_MERCHANTDB_RefundStatus
    612 TMH_PG_increase_refund (
    613   void *cls,
    614   const char *instance_id,
    615   const char *order_id,
    616   const struct TALER_Amount *refund,
    617   TALER_MERCHANTDB_OperationLimitCallback olc,
    618   void *olc_cls,
    619   const char *reason)
    620 {
    621   struct PostgresClosure *pg = cls;
    622   enum GNUNET_DB_QueryStatus qs;
    623   struct GNUNET_PQ_QueryParam params[] = {
    624     GNUNET_PQ_query_param_string (instance_id),
    625     GNUNET_PQ_query_param_string (order_id),
    626     GNUNET_PQ_query_param_end
    627   };
    628   struct InsertRefundContext ctx = {
    629     .pg = pg,
    630     .refund = refund,
    631     .olc = olc,
    632     .olc_cls = olc_cls,
    633     .reason = reason
    634   };
    635 
    636   // FIXME: return 'refund_serial' from this INSERT statement for #10577
    637   PREPARE (pg,
    638            "insert_refund",
    639            "INSERT INTO merchant_refunds"
    640            "(order_serial"
    641            ",rtransaction_id"
    642            ",refund_timestamp"
    643            ",coin_pub"
    644            ",reason"
    645            ",refund_amount"
    646            ") VALUES"
    647            "($1, $2, $3, $4, $5, $6)");
    648   PREPARE (pg,
    649            "find_refunds_by_coin",
    650            "SELECT"
    651            " refund_amount"
    652            ",rtransaction_id"
    653            " FROM merchant_refunds"
    654            " WHERE coin_pub=$1"
    655            "   AND order_serial=$2");
    656   PREPARE (pg,
    657            "find_deposits_for_refund",
    658            "SELECT"
    659            " dep.coin_pub"
    660            ",dco.order_serial"
    661            ",dep.amount_with_fee"
    662            ",dco.exchange_url"
    663            " FROM merchant_deposits dep"
    664            " JOIN merchant_deposit_confirmations dco"
    665            "   USING (deposit_confirmation_serial)"
    666            " WHERE order_serial="
    667            "  (SELECT order_serial"
    668            "     FROM merchant_contract_terms"
    669            "    WHERE order_id=$2"
    670            "      AND paid"
    671            "      AND merchant_serial="
    672            "        (SELECT merchant_serial"
    673            "           FROM merchant_instances"
    674            "          WHERE merchant_id=$1))");
    675   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    676               "Asked to refund %s on order %s\n",
    677               TALER_amount2s (refund),
    678               order_id);
    679   qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
    680                                              "find_deposits_for_refund",
    681                                              params,
    682                                              &process_deposits_for_refund_cb,
    683                                              &ctx);
    684   {
    685     struct ExchangeLimit *el;
    686 
    687     while (NULL != (el = ctx.el_head))
    688     {
    689       GNUNET_CONTAINER_DLL_remove (ctx.el_head,
    690                                    ctx.el_tail,
    691                                    el);
    692       GNUNET_free (el->exchange_url);
    693       GNUNET_free (el);
    694     }
    695   }
    696   switch (qs)
    697   {
    698   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    699     /* never paid, means we clearly cannot refund anything */
    700     return TALER_MERCHANTDB_RS_NO_SUCH_ORDER;
    701   case GNUNET_DB_STATUS_SOFT_ERROR:
    702     return TALER_MERCHANTDB_RS_SOFT_ERROR;
    703   case GNUNET_DB_STATUS_HARD_ERROR:
    704     return TALER_MERCHANTDB_RS_HARD_ERROR;
    705   default:
    706     /* Got one or more deposits */
    707     return ctx.rs;
    708   }
    709 }