LCOV - code coverage report
Current view: top level - backend - taler-merchant-webhook.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 108 186 58.1 %
Date: 2025-06-23 16:22:09 Functions: 7 8 87.5 %

          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          15 : shutdown_task (void *cls)
      92             : {
      93             :   struct WorkResponse *w;
      94             : 
      95             :   (void) cls;
      96          15 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
      97             :               "Running shutdown\n");
      98          15 :   if (NULL != event_handler)
      99             :   {
     100          15 :     db_plugin->event_listen_cancel (event_handler);
     101          15 :     event_handler = NULL;
     102             :   }
     103          15 :   if (NULL != task)
     104             :   {
     105          13 :     GNUNET_SCHEDULER_cancel (task);
     106          13 :     task = NULL;
     107             :   }
     108          15 :   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          15 :   db_plugin->rollback (db_plugin->cls); /* just in case */
     119          15 :   TALER_MERCHANTDB_plugin_unload (db_plugin);
     120          15 :   db_plugin = NULL;
     121          15 :   cfg = NULL;
     122          15 :   if (NULL != ctx)
     123             :   {
     124          15 :     GNUNET_CURL_fini (ctx);
     125          15 :     ctx = NULL;
     126             :   }
     127          15 :   if (NULL != rc)
     128             :   {
     129          15 :     GNUNET_CURL_gnunet_rc_destroy (rc);
     130          15 :     rc = NULL;
     131             :   }
     132          15 : }
     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          41 : select_work (void *cls)
     428             : {
     429             :   enum GNUNET_DB_QueryStatus qs;
     430             :   struct GNUNET_TIME_Relative rel;
     431             : 
     432             :   (void) cls;
     433          41 :   task = NULL;
     434          41 :   db_plugin->preflight (db_plugin->cls);
     435          41 :   qs = db_plugin->lookup_pending_webhooks (db_plugin->cls,
     436             :                                            &pending_webhooks_cb,
     437             :                                            NULL);
     438          41 :   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          39 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     448          39 :     if (test_mode)
     449             :     {
     450           2 :       GNUNET_SCHEDULER_shutdown ();
     451           2 :       return;
     452             :     }
     453          37 :     qs = db_plugin->lookup_future_webhook (db_plugin->cls,
     454             :                                            &future_webhook_cb,
     455             :                                            NULL);
     456             :     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          37 :     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          37 :       rel = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, 5);
     472          37 :       task = GNUNET_SCHEDULER_add_delayed (rel,
     473             :                                            &select_work,
     474             :                                            NULL);
     475          37 :       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          15 : 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          15 :   cfg = c;
     502          15 :   GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
     503             :                                  NULL);
     504          15 :   ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
     505             :                           &rc);
     506          15 :   rc = GNUNET_CURL_gnunet_rc_create (ctx);
     507          15 :   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          15 :   if (NULL ==
     515          15 :       (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          15 :   if (GNUNET_OK !=
     524          15 :       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          15 :     struct GNUNET_DB_EventHeaderP es = {
     534          15 :       .size = htons (sizeof (es)),
     535          15 :       .type = htons (TALER_DBEVENT_MERCHANT_WEBHOOK_PENDING)
     536             :     };
     537             : 
     538          30 :     event_handler = db_plugin->event_listen (db_plugin->cls,
     539             :                                              &es,
     540          15 :                                              GNUNET_TIME_UNIT_FOREVER_REL,
     541             :                                              &db_notify,
     542             :                                              NULL);
     543             :   }
     544          15 :   GNUNET_assert (NULL == task);
     545          15 :   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          15 : main (int argc,
     558             :       char *const *argv)
     559             : {
     560          15 :   struct GNUNET_GETOPT_CommandLineOption options[] = {
     561          15 :     GNUNET_GETOPT_option_flag ('t',
     562             :                                "test",
     563             :                                "run in test mode and exit when idle",
     564             :                                &test_mode),
     565          15 :     GNUNET_GETOPT_option_timetravel ('T',
     566             :                                      "timetravel"),
     567          15 :     GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
     568             :     GNUNET_GETOPT_OPTION_END
     569             :   };
     570             :   enum GNUNET_GenericReturnValue ret;
     571             : 
     572          15 :   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          15 :   if (GNUNET_SYSERR == ret)
     581           0 :     return EXIT_INVALIDARGUMENT;
     582          15 :   if (GNUNET_NO == ret)
     583           0 :     return EXIT_SUCCESS;
     584          15 :   return global_ret;
     585             : }
     586             : 
     587             : 
     588             : /* end of taler-merchant-webhook.c */

Generated by: LCOV version 1.16