LCOV - code coverage report
Current view: top level - backend - taler-merchant-webhook.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 58.3 % 187 109
Test Date: 2025-11-06 19:31:41 Functions: 87.5 % 8 7

            Line data    Source code
       1              : /*
       2              :   This file is part of TALER
       3              :   Copyright (C) 2023 Taler Systems SA
       4              : 
       5              :   TALER is free software; you can redistribute it and/or modify it under the
       6              :   terms of the GNU Affero 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 Affero General Public License for more details.
      12              : 
      13              :   You should have received a copy of the GNU Affero General Public License along with
      14              :   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
      15              : */
      16              : /**
      17              :  * @file taler-merchant-webhook.c
      18              :  * @brief Process that runs webhooks triggered by the merchant backend
      19              :  * @author Priscilla HUANG
      20              :  */
      21              : #include "platform.h"
      22              : #include "microhttpd.h"
      23              : #include <gnunet/gnunet_util_lib.h>
      24              : #include <jansson.h>
      25              : #include <pthread.h>
      26              : #include "taler_merchant_util.h"
      27              : #include "taler_merchantdb_lib.h"
      28              : #include "taler_merchantdb_plugin.h"
      29              : #include <taler/taler_dbevents.h>
      30              : 
      31              : 
      32              : struct WorkResponse
      33              : {
      34              :   struct WorkResponse *next;
      35              :   struct WorkResponse *prev;
      36              :   struct GNUNET_CURL_Job *job;
      37              :   uint64_t webhook_pending_serial;
      38              :   char *body;
      39              :   struct curl_slist *job_headers;
      40              : };
      41              : 
      42              : 
      43              : static struct WorkResponse *w_head;
      44              : 
      45              : static struct WorkResponse *w_tail;
      46              : 
      47              : static struct GNUNET_DB_EventHandler *event_handler;
      48              : 
      49              : /**
      50              :  * The merchant's configuration.
      51              :  */
      52              : static const struct GNUNET_CONFIGURATION_Handle *cfg;
      53              : 
      54              : /**
      55              :  * Our database plugin.
      56              :  */
      57              : static struct TALER_MERCHANTDB_Plugin *db_plugin;
      58              : 
      59              : /**
      60              :  * Next task to run, if any.
      61              :  */
      62              : static struct GNUNET_SCHEDULER_Task *task;
      63              : 
      64              : /**
      65              :  * Handle to the context for interacting with the bank / wire gateway.
      66              :  */
      67              : static struct GNUNET_CURL_Context *ctx;
      68              : 
      69              : /**
      70              :  * Scheduler context for running the @e ctx.
      71              :  */
      72              : static struct GNUNET_CURL_RescheduleContext *rc;
      73              : 
      74              : /**
      75              :  * Value to return from main(). 0 on success, non-zero on errors.
      76              :  */
      77              : static int global_ret;
      78              : 
      79              : /**
      80              :  * #GNUNET_YES if we are in test mode and should exit when idle.
      81              :  */
      82              : static int test_mode;
      83              : 
      84              : 
      85              : /**
      86              :  * We're being aborted with CTRL-C (or SIGTERM). Shut down.
      87              :  *
      88              :  * @param cls closure
      89              :  */
      90              : static void
      91           16 : shutdown_task (void *cls)
      92              : {
      93              :   struct WorkResponse *w;
      94              : 
      95              :   (void) cls;
      96           16 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
      97              :               "Running shutdown\n");
      98           16 :   if (NULL != event_handler)
      99              :   {
     100           16 :     db_plugin->event_listen_cancel (event_handler);
     101           16 :     event_handler = NULL;
     102              :   }
     103           16 :   if (NULL != task)
     104              :   {
     105           14 :     GNUNET_SCHEDULER_cancel (task);
     106           14 :     task = NULL;
     107              :   }
     108           16 :   while (NULL != (w = w_head))
     109              :   {
     110            0 :     GNUNET_CONTAINER_DLL_remove (w_head,
     111              :                                  w_tail,
     112              :                                  w);
     113            0 :     GNUNET_CURL_job_cancel (w->job);
     114            0 :     curl_slist_free_all (w->job_headers);
     115            0 :     GNUNET_free (w->body);
     116            0 :     GNUNET_free (w);
     117              :   }
     118           16 :   db_plugin->rollback (db_plugin->cls); /* just in case */
     119           16 :   TALER_MERCHANTDB_plugin_unload (db_plugin);
     120           16 :   db_plugin = NULL;
     121           16 :   cfg = NULL;
     122           16 :   if (NULL != ctx)
     123              :   {
     124           16 :     GNUNET_CURL_fini (ctx);
     125           16 :     ctx = NULL;
     126              :   }
     127           16 :   if (NULL != rc)
     128              :   {
     129           16 :     GNUNET_CURL_gnunet_rc_destroy (rc);
     130           16 :     rc = NULL;
     131              :   }
     132           16 : }
     133              : 
     134              : 
     135              : /**
     136              :  * Select webhook to process.
     137              :  *
     138              :  * @param cls NULL
     139              :  */
     140              : static void
     141              : select_work (void *cls);
     142              : 
     143              : 
     144              : /**
     145              :  * This function is used by the function `pending_webhooks_cb`. According to the response code,
     146              :  * we delete or update the webhook.
     147              :  *
     148              :  * @param cls closure
     149              :  * @param response_code HTTP response code from server, 0 on hard error
     150              :  * @param body http body of the response
     151              :  * @param body_size number of bytes in @a body
     152              :  */
     153              : static void
     154            2 : handle_webhook_response (void *cls,
     155              :                          long response_code,
     156              :                          const void *body,
     157              :                          size_t body_size)
     158              : {
     159            2 :   struct WorkResponse *w = cls;
     160              : 
     161              :   (void) body;
     162              :   (void) body_size;
     163            2 :   w->job = NULL;
     164            2 :   GNUNET_CONTAINER_DLL_remove (w_head,
     165              :                                w_tail,
     166              :                                w);
     167            2 :   GNUNET_free (w->body);
     168            2 :   curl_slist_free_all (w->job_headers);
     169            2 :   if (NULL == w_head)
     170            2 :     task = GNUNET_SCHEDULER_add_now (&select_work,
     171              :                                      NULL);
     172            2 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     173              :               "Webhook %llu returned with status %ld\n",
     174              :               (unsigned long long) w->webhook_pending_serial,
     175              :               response_code);
     176            2 :   if (2 == response_code / 100) /* any 2xx http status code is OK! */
     177              :   {
     178              :     enum GNUNET_DB_QueryStatus qs;
     179              : 
     180            2 :     qs = db_plugin->delete_pending_webhook (db_plugin->cls,
     181              :                                             w->webhook_pending_serial);
     182            2 :     GNUNET_free (w);
     183            2 :     switch (qs)
     184              :     {
     185            0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     186              :     case GNUNET_DB_STATUS_SOFT_ERROR:
     187            0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     188              :                   "Failed to delete webhook, delete returned: %d\n",
     189              :                   qs);
     190            0 :       global_ret = EXIT_FAILURE;
     191            0 :       GNUNET_SCHEDULER_shutdown ();
     192            0 :       return;
     193            2 :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     194            2 :       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     195              :                   "Delete returned: %d\n",
     196              :                   qs);
     197            2 :       return;
     198            0 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     199            0 :       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     200              :                   "Delete returned: %d\n",
     201              :                   qs);
     202            0 :       return;
     203              :     }
     204            0 :     GNUNET_assert (0);
     205              :   }
     206              : 
     207              :   {
     208              :     struct GNUNET_TIME_Relative next_attempt;
     209              :     enum GNUNET_DB_QueryStatus qs;
     210            0 :     switch (response_code)
     211              :     {
     212            0 :     case MHD_HTTP_BAD_REQUEST:
     213            0 :       next_attempt = GNUNET_TIME_UNIT_FOREVER_REL;   // never try again
     214            0 :       break;
     215            0 :     case MHD_HTTP_INTERNAL_SERVER_ERROR:
     216            0 :       next_attempt = GNUNET_TIME_UNIT_MINUTES;
     217            0 :       break;
     218            0 :     case MHD_HTTP_FORBIDDEN:
     219            0 :       next_attempt = GNUNET_TIME_UNIT_MINUTES;
     220            0 :       break;
     221            0 :     default:
     222            0 :       next_attempt = GNUNET_TIME_UNIT_HOURS;
     223            0 :       break;
     224              :     }
     225            0 :     qs = db_plugin->update_pending_webhook (db_plugin->cls,
     226              :                                             w->webhook_pending_serial,
     227              :                                             GNUNET_TIME_relative_to_absolute (
     228              :                                               next_attempt));
     229            0 :     GNUNET_free (w);
     230            0 :     switch (qs)
     231              :     {
     232            0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     233              :     case GNUNET_DB_STATUS_SOFT_ERROR:
     234            0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     235              :                   "Failed to update pending webhook to next in %s Rval: %d\n",
     236              :                   GNUNET_TIME_relative2s (next_attempt,
     237              :                                           true),
     238              :                   qs);
     239            0 :       global_ret = EXIT_FAILURE;
     240            0 :       GNUNET_SCHEDULER_shutdown ();
     241            0 :       return;
     242            0 :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     243            0 :       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     244              :                   "Next in %s Rval: %d\n",
     245              :                   GNUNET_TIME_relative2s (next_attempt, true),
     246              :                   qs);
     247            0 :       return;
     248            0 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     249            0 :       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     250              :                   "Next in %s Rval: %d\n",
     251              :                   GNUNET_TIME_relative2s (next_attempt, true),
     252              :                   qs);
     253            0 :       return;
     254              :     }
     255            0 :     GNUNET_assert (0);
     256              :   }
     257              : }
     258              : 
     259              : 
     260              : /**
     261              :  * Typically called by `select_work`.
     262              :  *
     263              :  * @param cls a `json_t *` JSON array to build
     264              :  * @param webhook_pending_serial reference to the configured webhook template.
     265              :  * @param next_attempt is the time we should make the next request to the webhook.
     266              :  * @param retries how often have we tried this request to the webhook.
     267              :  * @param url to make request to
     268              :  * @param http_method use for the webhook
     269              :  * @param header of the webhook
     270              :  * @param body of the webhook
     271              :  */
     272              : static void
     273            2 : pending_webhooks_cb (void *cls,
     274              :                      uint64_t webhook_pending_serial,
     275              :                      struct GNUNET_TIME_Absolute next_attempt,
     276              :                      uint32_t retries,
     277              :                      const char *url,
     278              :                      const char *http_method,
     279              :                      const char *header,
     280              :                      const char *body)
     281              : {
     282            2 :   struct WorkResponse *w = GNUNET_new (struct WorkResponse);
     283              :   CURL *eh;
     284            2 :   struct curl_slist *job_headers = NULL;
     285              : 
     286              :   (void) retries;
     287              :   (void) next_attempt;
     288              :   (void) cls;
     289            2 :   GNUNET_CONTAINER_DLL_insert (w_head,
     290              :                                w_tail,
     291              :                                w);
     292            2 :   w->webhook_pending_serial = webhook_pending_serial;
     293            2 :   eh = curl_easy_init ();
     294            2 :   GNUNET_assert (NULL != eh);
     295            2 :   GNUNET_assert (CURLE_OK ==
     296              :                  curl_easy_setopt (eh,
     297              :                                    CURLOPT_CUSTOMREQUEST,
     298              :                                    http_method));
     299            2 :   GNUNET_assert (CURLE_OK ==
     300              :                  curl_easy_setopt (eh,
     301              :                                    CURLOPT_URL,
     302              :                                    url));
     303            2 :   GNUNET_assert (CURLE_OK ==
     304              :                  curl_easy_setopt (eh,
     305              :                                    CURLOPT_VERBOSE,
     306              :                                    0L));
     307              : 
     308              :   /* conversion body data */
     309            2 :   if (NULL != body)
     310              :   {
     311            2 :     w->body = GNUNET_strdup (body);
     312            2 :     GNUNET_assert (CURLE_OK ==
     313              :                    curl_easy_setopt (eh,
     314              :                                      CURLOPT_POSTFIELDS,
     315              :                                      w->body));
     316              :   }
     317              :   /* conversion header to job_headers data */
     318            2 :   if (NULL != header)
     319              :   {
     320            2 :     char *header_copy = GNUNET_strdup (header);
     321              : 
     322            2 :     for (const char *tok = strtok (header_copy, "\n");
     323            4 :          NULL != tok;
     324            2 :          tok = strtok (NULL, "\n"))
     325              :     {
     326              :       // extract all Key: value from 'header_copy'!
     327            2 :       job_headers = curl_slist_append (job_headers,
     328              :                                        tok);
     329              :     }
     330            2 :     GNUNET_free (header_copy);
     331            2 :     GNUNET_assert (CURLE_OK ==
     332              :                    curl_easy_setopt (eh,
     333              :                                      CURLOPT_HTTPHEADER,
     334              :                                      job_headers));
     335            2 :     w->job_headers = job_headers;
     336              :   }
     337            2 :   GNUNET_assert (CURLE_OK ==
     338              :                  curl_easy_setopt (eh,
     339              :                                    CURLOPT_MAXREDIRS,
     340              :                                    5));
     341            2 :   GNUNET_assert (CURLE_OK ==
     342              :                  curl_easy_setopt (eh,
     343              :                                    CURLOPT_FOLLOWLOCATION,
     344              :                                    1));
     345              : 
     346            2 :   w->job = GNUNET_CURL_job_add_raw (ctx,
     347              :                                     eh,
     348              :                                     job_headers,
     349              :                                     &handle_webhook_response,
     350              :                                     w);
     351            2 :   if (NULL == w->job)
     352              :   {
     353            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     354              :                 "Failed to start the curl job for pending webhook #%llu\n",
     355              :                 (unsigned long long) webhook_pending_serial);
     356            0 :     curl_slist_free_all (w->job_headers);
     357            0 :     GNUNET_free (w->body);
     358            0 :     GNUNET_CONTAINER_DLL_remove (w_head,
     359              :                                  w_tail,
     360              :                                  w);
     361            0 :     GNUNET_free (w);
     362            0 :     GNUNET_SCHEDULER_shutdown ();
     363            0 :     return;
     364              :   }
     365              : }
     366              : 
     367              : 
     368              : /**
     369              :  * Function called on events received from Postgres.
     370              :  *
     371              :  * @param cls closure, NULL
     372              :  * @param extra additional event data provided
     373              :  * @param extra_size number of bytes in @a extra
     374              :  */
     375              : static void
     376           24 : db_notify (void *cls,
     377              :            const void *extra,
     378              :            size_t extra_size)
     379              : {
     380              :   (void) cls;
     381              :   (void) extra;
     382              :   (void) extra_size;
     383              : 
     384           24 :   GNUNET_assert (NULL != task);
     385           24 :   GNUNET_SCHEDULER_cancel (task);
     386           24 :   task = GNUNET_SCHEDULER_add_now (&select_work,
     387              :                                    NULL);
     388           24 : }
     389              : 
     390              : 
     391              : /**
     392              :  * Typically called by `select_work`.
     393              :  *
     394              :  * @param cls a `json_t *` JSON array to build
     395              :  * @param webhook_pending_serial reference to the configured webhook template.
     396              :  * @param next_attempt is the time we should make the next request to the webhook.
     397              :  * @param retries how often have we tried this request to the webhook.
     398              :  * @param url to make request to
     399              :  * @param http_method use for the webhook
     400              :  * @param header of the webhook
     401              :  * @param body of the webhook
     402              :  */
     403              : static void
     404            0 : future_webhook_cb (void *cls,
     405              :                    uint64_t webhook_pending_serial,
     406              :                    struct GNUNET_TIME_Absolute next_attempt,
     407              :                    uint32_t retries,
     408              :                    const char *url,
     409              :                    const char *http_method,
     410              :                    const char *header,
     411              :                    const char *body)
     412              : {
     413              :   (void) webhook_pending_serial;
     414              :   (void) retries;
     415              :   (void) url;
     416              :   (void) http_method;
     417              :   (void) header;
     418              :   (void) body;
     419              : 
     420            0 :   task = GNUNET_SCHEDULER_add_at (next_attempt,
     421              :                                   &select_work,
     422              :                                   NULL);
     423            0 : }
     424              : 
     425              : 
     426              : static void
     427           42 : select_work (void *cls)
     428              : {
     429              :   enum GNUNET_DB_QueryStatus qs;
     430              :   struct GNUNET_TIME_Relative rel;
     431              : 
     432              :   (void) cls;
     433           42 :   task = NULL;
     434           42 :   db_plugin->preflight (db_plugin->cls);
     435           42 :   qs = db_plugin->lookup_pending_webhooks (db_plugin->cls,
     436              :                                            &pending_webhooks_cb,
     437              :                                            NULL);
     438           42 :   switch (qs)
     439              :   {
     440            0 :   case GNUNET_DB_STATUS_HARD_ERROR:
     441              :   case GNUNET_DB_STATUS_SOFT_ERROR:
     442            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     443              :                 "Failed to lookup pending webhooks!\n");
     444            0 :     global_ret = EXIT_FAILURE;
     445            0 :     GNUNET_SCHEDULER_shutdown ();
     446            0 :     return;
     447           40 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     448           40 :     if (test_mode)
     449              :     {
     450            2 :       GNUNET_SCHEDULER_shutdown ();
     451            2 :       return;
     452              :     }
     453           38 :     qs = db_plugin->lookup_future_webhook (db_plugin->cls,
     454              :                                            &future_webhook_cb,
     455              :                                            NULL);
     456           38 :     switch (qs)
     457              :     {
     458            0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     459              :     case GNUNET_DB_STATUS_SOFT_ERROR:
     460            0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     461              :                   "Failed to lookup future webhook!\n");
     462            0 :       global_ret = EXIT_FAILURE;
     463            0 :       GNUNET_SCHEDULER_shutdown ();
     464            0 :       return;
     465            0 :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     466            0 :       return;
     467           38 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     468              :       /* wait 5 min */
     469              :       /* Note: this should not even be necessary if all webhooks
     470              :          use the events properly... */
     471           38 :       rel = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 5);
     472           38 :       task = GNUNET_SCHEDULER_add_delayed (rel,
     473              :                                            &select_work,
     474              :                                            NULL);
     475           38 :       return;
     476              :     }
     477              :   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     478              :   default:
     479            2 :     return; // wait for completion, then select more work.
     480              :   }
     481              : }
     482              : 
     483              : 
     484              : /**
     485              :  * First task.
     486              :  *
     487              :  * @param cls closure, NULL
     488              :  * @param args remaining command-line arguments
     489              :  * @param cfgfile name of the configuration file used (for saving, can be NULL!)
     490              :  * @param c configuration
     491              :  */
     492              : static void
     493           16 : run (void *cls,
     494              :      char *const *args,
     495              :      const char *cfgfile,
     496              :      const struct GNUNET_CONFIGURATION_Handle *c)
     497              : {
     498              :   (void) args;
     499              :   (void) cfgfile;
     500              : 
     501           16 :   cfg = c;
     502           16 :   GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
     503              :                                  NULL);
     504           16 :   ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
     505              :                           &rc);
     506           16 :   rc = GNUNET_CURL_gnunet_rc_create (ctx);
     507           16 :   if (NULL == ctx)
     508              :   {
     509            0 :     GNUNET_break (0);
     510            0 :     GNUNET_SCHEDULER_shutdown ();
     511            0 :     global_ret = EXIT_FAILURE;
     512            0 :     return;
     513              :   }
     514           16 :   if (NULL ==
     515           16 :       (db_plugin = TALER_MERCHANTDB_plugin_load (cfg)))
     516              :   {
     517            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     518              :                 "Failed to initialize DB subsystem\n");
     519            0 :     GNUNET_SCHEDULER_shutdown ();
     520            0 :     global_ret = EXIT_NOTCONFIGURED;
     521            0 :     return;
     522              :   }
     523           16 :   if (GNUNET_OK !=
     524           16 :       db_plugin->connect (db_plugin->cls))
     525              :   {
     526            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     527              :                 "Failed to connect to database. Consider running taler-merchant-dbinit!\n");
     528            0 :     GNUNET_SCHEDULER_shutdown ();
     529            0 :     global_ret = EXIT_FAILURE;
     530            0 :     return;
     531              :   }
     532              :   {
     533           16 :     struct GNUNET_DB_EventHeaderP es = {
     534           16 :       .size = htons (sizeof (es)),
     535           16 :       .type = htons (TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING)
     536              :     };
     537              : 
     538           32 :     event_handler = db_plugin->event_listen (db_plugin->cls,
     539              :                                              &es,
     540           16 :                                              GNUNET_TIME_UNIT_FOREVER_REL,
     541              :                                              &db_notify,
     542              :                                              NULL);
     543              :   }
     544           16 :   GNUNET_assert (NULL == task);
     545           16 :   task = GNUNET_SCHEDULER_add_now (&select_work,
     546              :                                    NULL);
     547              : }
     548              : 
     549              : 
     550              : /**
     551              :  * The main function of the taler-merchant-webhook
     552              :  * @param argc number of arguments from the command line
     553              :  * @param argv command line arguments
     554              :  * @return 0 ok, 1 on error
     555              :  */
     556              : int
     557           16 : main (int argc,
     558              :       char *const *argv)
     559              : {
     560           16 :   struct GNUNET_GETOPT_CommandLineOption options[] = {
     561           16 :     GNUNET_GETOPT_option_flag ('t',
     562              :                                "test",
     563              :                                "run in test mode and exit when idle",
     564              :                                &test_mode),
     565           16 :     GNUNET_GETOPT_option_timetravel ('T',
     566              :                                      "timetravel"),
     567           16 :     GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
     568              :     GNUNET_GETOPT_OPTION_END
     569              :   };
     570              :   enum GNUNET_GenericReturnValue ret;
     571              : 
     572           16 :   ret = GNUNET_PROGRAM_run (
     573              :     TALER_MERCHANT_project_data (),
     574              :     argc, argv,
     575              :     "taler-merchant-webhook",
     576              :     gettext_noop (
     577              :       "background process that executes webhooks"),
     578              :     options,
     579              :     &run, NULL);
     580           16 :   if (GNUNET_SYSERR == ret)
     581            0 :     return EXIT_INVALIDARGUMENT;
     582           16 :   if (GNUNET_NO == ret)
     583            0 :     return EXIT_SUCCESS;
     584           16 :   return global_ret;
     585              : }
     586              : 
     587              : 
     588              : /* end of taler-merchant-webhook.c */
        

Generated by: LCOV version 2.0-1