merchant

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

taler-merchant-httpd_get-private-statistics-report-transactions.c (20923B)


      1 /*
      2   This file is part of TALER
      3   (C) 2025 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 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/backend/taler-merchant-httpd_get-private-statistics-report-transactions.c
     18  * @brief implement GET /statistics-report/transactions
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include "taler-merchant-httpd_get-private-statistics-report-transactions.h"
     23 #include <gnunet/gnunet_json_lib.h>
     24 #include <taler/taler_json_lib.h>
     25 #include <taler/taler_mhd_lib.h>
     26 #include "taler/taler_merchant_util.h"
     27 #include "merchant-database/lookup_statistics_amount_by_bucket2.h"
     28 #include "merchant-database/lookup_statistics_counter_by_bucket2.h"
     29 
     30 
     31 /**
     32  * Sensible bound on the number of buckets a client may request.
     33  */
     34 #define MAX_DELTA 1024
     35 
     36 
     37 /**
     38  * Closure for the detail_cb().
     39  */
     40 struct ResponseContext
     41 {
     42   /**
     43    * Format of the response we are to generate.
     44    */
     45   enum
     46   {
     47     RCF_JSON,
     48     RCF_PDF
     49   } format;
     50 
     51   /**
     52    * Stored in a DLL while suspended.
     53    */
     54   struct ResponseContext *next;
     55 
     56   /**
     57    * Stored in a DLL while suspended.
     58    */
     59   struct ResponseContext *prev;
     60 
     61   /**
     62    * Context for this request.
     63    */
     64   struct TMH_HandlerContext *hc;
     65 
     66   /**
     67    * Async context used to run Typst.
     68    */
     69   struct TALER_MHD_TypstContext *tc;
     70 
     71   /**
     72    * Response to return.
     73    */
     74   struct MHD_Response *response;
     75 
     76   /**
     77    * Time when we started processing the request.
     78    */
     79   struct GNUNET_TIME_Timestamp now;
     80 
     81   /**
     82    * Period of each bucket.
     83    */
     84   struct GNUNET_TIME_Relative period;
     85 
     86   /**
     87    * Granularity of the buckets. Matches @e period.
     88    */
     89   const char *granularity;
     90 
     91   /**
     92    * Number of buckets to return.
     93    */
     94   uint64_t count;
     95 
     96   /**
     97    * HTTP status to use with @e response.
     98    */
     99   unsigned int http_status;
    100 
    101   /**
    102    * Length of the @e labels array.
    103    */
    104   unsigned int labels_cnt;
    105 
    106   /**
    107    * Array of labels for the chart.
    108    */
    109   char **labels;
    110 
    111   /**
    112    * Data groups for the chart.
    113    */
    114   json_t *data_groups;
    115 
    116   /**
    117    * #GNUNET_YES if connection was suspended,
    118    * #GNUNET_SYSERR if we were resumed on shutdown.
    119    */
    120   enum GNUNET_GenericReturnValue suspended;
    121 
    122 };
    123 
    124 
    125 /**
    126  * DLL of requests awaiting Typst.
    127  */
    128 static struct ResponseContext *rctx_head;
    129 
    130 /**
    131  * DLL of requests awaiting Typst.
    132  */
    133 static struct ResponseContext *rctx_tail;
    134 
    135 
    136 void
    137 TMH_handler_statistic_report_transactions_cleanup ()
    138 {
    139   struct ResponseContext *rctx;
    140 
    141   while (NULL != (rctx = rctx_head))
    142   {
    143     GNUNET_CONTAINER_DLL_remove (rctx_head,
    144                                  rctx_tail,
    145                                  rctx);
    146     rctx->suspended = GNUNET_SYSERR;
    147     MHD_resume_connection (rctx->hc->connection);
    148   }
    149 }
    150 
    151 
    152 /**
    153  * Free resources from @a ctx
    154  *
    155  * @param[in] ctx the `struct ResponseContext` to clean up
    156  */
    157 static void
    158 free_rc (void *ctx)
    159 {
    160   struct ResponseContext *rctx = ctx;
    161 
    162   if (NULL != rctx->tc)
    163   {
    164     TALER_MHD_typst_cancel (rctx->tc);
    165     rctx->tc = NULL;
    166   }
    167   if (NULL != rctx->response)
    168   {
    169     MHD_destroy_response (rctx->response);
    170     rctx->response = NULL;
    171   }
    172   for (unsigned int i = 0; i<rctx->labels_cnt; i++)
    173     GNUNET_free (rctx->labels[i]);
    174   GNUNET_array_grow (rctx->labels,
    175                      rctx->labels_cnt,
    176                      0);
    177   json_decref (rctx->data_groups);
    178   GNUNET_free (rctx);
    179 }
    180 
    181 
    182 /**
    183  * Function called with the result of a #TALER_MHD_typst() operation.
    184  *
    185  * @param cls closure
    186  * @param tr result of the operation
    187  */
    188 static void
    189 pdf_cb (void *cls,
    190         const struct TALER_MHD_TypstResponse *tr)
    191 {
    192   struct ResponseContext *rctx = cls;
    193 
    194   rctx->tc = NULL;
    195   GNUNET_CONTAINER_DLL_remove (rctx_head,
    196                                rctx_tail,
    197                                rctx);
    198   rctx->suspended = GNUNET_NO;
    199   MHD_resume_connection (rctx->hc->connection);
    200   TALER_MHD_daemon_trigger ();
    201   if (TALER_EC_NONE != tr->ec)
    202   {
    203     rctx->http_status
    204       = TALER_ErrorCode_get_http_status (tr->ec);
    205     rctx->response
    206       = TALER_MHD_make_error (tr->ec,
    207                               tr->details.hint);
    208     return;
    209   }
    210   rctx->http_status
    211     = MHD_HTTP_OK;
    212   rctx->response
    213     = TALER_MHD_response_from_pdf_file (tr->details.filename);
    214 }
    215 
    216 
    217 /**
    218  * Typically called by `lookup_statistics_amount_by_bucket2`.
    219  *
    220  * @param[in,out] cls our `struct ResponseContext` to update
    221  * @param bucket_start start time of the bucket
    222  * @param amounts_len the length of @a amounts array
    223  * @param amounts the cumulative amounts in the bucket
    224  */
    225 static void
    226 amount_by_bucket (void *cls,
    227                   struct GNUNET_TIME_Timestamp bucket_start,
    228                   unsigned int amounts_len,
    229                   const struct TALER_Amount amounts[static amounts_len])
    230 {
    231   struct ResponseContext *rctx = cls;
    232   json_t *values;
    233 
    234   for (unsigned int i = 0; i<amounts_len; i++)
    235   {
    236     bool found = false;
    237 
    238     for (unsigned int j = 0; j<rctx->labels_cnt; j++)
    239     {
    240       if (0 == strcmp (amounts[i].currency,
    241                        rctx->labels[j]))
    242       {
    243         found = true;
    244         break;
    245       }
    246     }
    247     if (! found)
    248     {
    249       GNUNET_array_append (rctx->labels,
    250                            rctx->labels_cnt,
    251                            GNUNET_strdup (amounts[i].currency));
    252     }
    253   }
    254 
    255   values = json_array ();
    256   GNUNET_assert (NULL != values);
    257   for (unsigned int i = 0; i<rctx->labels_cnt; i++)
    258   {
    259     const char *label = rctx->labels[i];
    260     double d = 0.0;
    261 
    262     for (unsigned int j = 0; j<amounts_len; j++)
    263     {
    264       const struct TALER_Amount *a = &amounts[j];
    265 
    266       if (0 != strcmp (amounts[j].currency,
    267                        label))
    268         continue;
    269       d = a->value * 1.0
    270           + (a->fraction * 1.0 / TALER_AMOUNT_FRAC_BASE);
    271       break;
    272     } /* for all amounts */
    273     GNUNET_assert (0 ==
    274                    json_array_append_new (values,
    275                                           json_real (d)));
    276   } /* for all labels */
    277 
    278   {
    279     json_t *dg;
    280 
    281     dg = GNUNET_JSON_PACK (
    282       GNUNET_JSON_pack_timestamp ("start_date",
    283                                   bucket_start),
    284       GNUNET_JSON_pack_array_steal ("values",
    285                                     values));
    286     GNUNET_assert (0 ==
    287                    json_array_append_new (rctx->data_groups,
    288                                           dg));
    289 
    290   }
    291 }
    292 
    293 
    294 /**
    295  * Create the transaction volume report.
    296  *
    297  * @param[in,out] rctx request context to use
    298  * @param[in,out] charts JSON chart array to expand
    299  * @return #GNUNET_OK on success,
    300  *         #GNUNET_NO to end with #MHD_YES,
    301  *         #GNUNET_NO to end with #MHD_NO.
    302  */
    303 static enum GNUNET_GenericReturnValue
    304 make_transaction_volume_report (struct ResponseContext *rctx,
    305                                 json_t *charts)
    306 {
    307   const char *bucket_name = "deposits-received";
    308   enum GNUNET_DB_QueryStatus qs;
    309   json_t *chart;
    310   json_t *labels;
    311 
    312   rctx->data_groups = json_array ();
    313   GNUNET_assert (NULL != rctx->data_groups);
    314   qs = TALER_MERCHANTDB_lookup_statistics_amount_by_bucket2 (
    315     TMH_db,
    316     rctx->hc->instance->settings.id,
    317     bucket_name,
    318     rctx->granularity,
    319     rctx->count,
    320     &amount_by_bucket,
    321     rctx);
    322   if (0 > qs)
    323   {
    324     GNUNET_break (0);
    325     return (MHD_YES ==
    326             TALER_MHD_reply_with_error (
    327               rctx->hc->connection,
    328               MHD_HTTP_INTERNAL_SERVER_ERROR,
    329               TALER_EC_GENERIC_DB_FETCH_FAILED,
    330               "lookup_statistics_amount_by_bucket2"))
    331         ? GNUNET_NO : GNUNET_SYSERR;
    332   }
    333   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    334   {
    335     json_decref (rctx->data_groups);
    336     rctx->data_groups = NULL;
    337     return GNUNET_OK;
    338   }
    339 
    340   labels = json_array ();
    341   GNUNET_assert (NULL != labels);
    342   for (unsigned int i=0; i<rctx->labels_cnt; i++)
    343   {
    344     GNUNET_assert (0 ==
    345                    json_array_append_new (labels,
    346                                           json_string (rctx->labels[i])));
    347     GNUNET_free (rctx->labels[i]);
    348   }
    349   GNUNET_array_grow (rctx->labels,
    350                      rctx->labels_cnt,
    351                      0);
    352   chart = GNUNET_JSON_PACK (
    353     GNUNET_JSON_pack_string ("chart_name",
    354                              "Sales volume"),
    355     GNUNET_JSON_pack_string ("y_label",
    356                              "Sales"),
    357     GNUNET_JSON_pack_array_steal ("data_groups",
    358                                   rctx->data_groups),
    359     GNUNET_JSON_pack_array_steal ("labels",
    360                                   labels),
    361     GNUNET_JSON_pack_bool ("cumulative",
    362                            false));
    363   rctx->data_groups = NULL;
    364   GNUNET_assert (0 ==
    365                  json_array_append_new (charts,
    366                                         chart));
    367   return GNUNET_OK;
    368 }
    369 
    370 
    371 /**
    372  * Typically called by `lookup_statistics_counter_by_bucket2`.
    373  *
    374  * @param[in,out] cls our `struct ResponseContext` to update
    375  * @param bucket_start start time of the bucket
    376  * @param counters_len the length of @a cumulative_amounts
    377  * @param descriptions description for the counter in the bucket
    378  * @param counters the counters in the bucket
    379  */
    380 static void
    381 count_by_bucket (void *cls,
    382                  struct GNUNET_TIME_Timestamp bucket_start,
    383                  unsigned int counters_len,
    384                  const char *descriptions[static counters_len],
    385                  uint64_t counters[static counters_len])
    386 {
    387   struct ResponseContext *rctx = cls;
    388   json_t *values;
    389 
    390   for (unsigned int i = 0; i<counters_len; i++)
    391   {
    392     bool found = false;
    393 
    394     for (unsigned int j = 0; j<rctx->labels_cnt; j++)
    395     {
    396       if (0 == strcmp (descriptions[i],
    397                        rctx->labels[j]))
    398       {
    399         found = true;
    400         break;
    401       }
    402     }
    403     if (! found)
    404     {
    405       GNUNET_array_append (rctx->labels,
    406                            rctx->labels_cnt,
    407                            GNUNET_strdup (descriptions[i]));
    408     }
    409   }
    410 
    411   values = json_array ();
    412   GNUNET_assert (NULL != values);
    413   for (unsigned int i = 0; i<rctx->labels_cnt; i++)
    414   {
    415     const char *label = rctx->labels[i];
    416     uint64_t v = 0;
    417 
    418     for (unsigned int j = 0; j<counters_len; j++)
    419     {
    420       if (0 != strcmp (descriptions[j],
    421                        label))
    422         continue;
    423       v = counters[j];
    424       break;
    425     } /* for all amounts */
    426     GNUNET_assert (0 ==
    427                    json_array_append_new (values,
    428                                           json_integer (v)));
    429   } /* for all labels */
    430 
    431   {
    432     json_t *dg;
    433 
    434     dg = GNUNET_JSON_PACK (
    435       GNUNET_JSON_pack_timestamp ("start_date",
    436                                   bucket_start),
    437       GNUNET_JSON_pack_array_steal ("values",
    438                                     values));
    439     GNUNET_assert (0 ==
    440                    json_array_append_new (rctx->data_groups,
    441                                           dg));
    442 
    443   }
    444 }
    445 
    446 
    447 /**
    448  * Create the transaction count report.
    449  *
    450  * @param[in,out] rctx request context to use
    451  * @param[in,out] charts JSON chart array to expand
    452  * @return #GNUNET_OK on success,
    453  *         #GNUNET_NO to end with #MHD_YES,
    454  *         #GNUNET_NO to end with #MHD_NO.
    455  */
    456 static enum GNUNET_GenericReturnValue
    457 make_transaction_count_report (struct ResponseContext *rctx,
    458                                json_t *charts)
    459 {
    460   const char *prefix = "orders-paid";
    461   enum GNUNET_DB_QueryStatus qs;
    462   json_t *chart;
    463   json_t *labels;
    464 
    465   rctx->data_groups = json_array ();
    466   GNUNET_assert (NULL != rctx->data_groups);
    467   qs = TALER_MERCHANTDB_lookup_statistics_counter_by_bucket2 (
    468     TMH_db,
    469     rctx->hc->instance->settings.id,
    470     prefix,   /* prefix to match against bucket name */
    471     rctx->granularity,
    472     rctx->count,
    473     &count_by_bucket,
    474     rctx);
    475   if (0 > qs)
    476   {
    477     GNUNET_break (0);
    478     return (MHD_YES ==
    479             TALER_MHD_reply_with_error (
    480               rctx->hc->connection,
    481               MHD_HTTP_INTERNAL_SERVER_ERROR,
    482               TALER_EC_GENERIC_DB_FETCH_FAILED,
    483               "lookup_statistics_counter_by_bucket2"))
    484         ? GNUNET_NO : GNUNET_SYSERR;
    485   }
    486   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    487   {
    488     json_decref (rctx->data_groups);
    489     rctx->data_groups = NULL;
    490     return GNUNET_OK;
    491   }
    492   labels = json_array ();
    493   GNUNET_assert (NULL != labels);
    494   for (unsigned int i=0; i<rctx->labels_cnt; i++)
    495   {
    496     const char *label = rctx->labels[i];
    497 
    498     /* This condition should always hold. */
    499     if (0 ==
    500         strncmp (prefix,
    501                  label,
    502                  strlen (prefix)))
    503       label += strlen (prefix);
    504     GNUNET_assert (0 ==
    505                    json_array_append_new (labels,
    506                                           json_string (label)));
    507     GNUNET_free (rctx->labels[i]);
    508   }
    509   GNUNET_array_grow (rctx->labels,
    510                      rctx->labels_cnt,
    511                      0);
    512   chart = GNUNET_JSON_PACK (
    513     GNUNET_JSON_pack_string ("chart_name",
    514                              "Transaction counts"),
    515     GNUNET_JSON_pack_string ("y_label",
    516                              "Number of transactions"),
    517     GNUNET_JSON_pack_array_steal ("data_groups",
    518                                   rctx->data_groups),
    519     GNUNET_JSON_pack_array_steal ("labels",
    520                                   labels),
    521     GNUNET_JSON_pack_bool ("cumulative",
    522                            false));
    523   rctx->data_groups = NULL;
    524   GNUNET_assert (0 ==
    525                  json_array_append_new (charts,
    526                                         chart));
    527   return GNUNET_OK;
    528 }
    529 
    530 
    531 enum MHD_Result
    532 TMH_private_get_statistics_report_transactions (
    533   const struct TMH_RequestHandler *rh,
    534   struct MHD_Connection *connection,
    535   struct TMH_HandlerContext *hc)
    536 {
    537   struct ResponseContext *rctx = hc->ctx;
    538   struct TMH_MerchantInstance *mi = hc->instance;
    539   json_t *charts;
    540 
    541   if (NULL != rctx)
    542   {
    543     GNUNET_assert (GNUNET_YES != rctx->suspended);
    544     if (GNUNET_SYSERR == rctx->suspended)
    545       return MHD_NO;
    546     if (NULL == rctx->response)
    547     {
    548       GNUNET_break (0);
    549       return MHD_NO;
    550     }
    551     return MHD_queue_response (connection,
    552                                rctx->http_status,
    553                                rctx->response);
    554   }
    555   rctx = GNUNET_new (struct ResponseContext);
    556   rctx->hc = hc;
    557   rctx->now = GNUNET_TIME_timestamp_get ();
    558   hc->ctx = rctx;
    559   hc->cc = &free_rc;
    560   GNUNET_assert (NULL != mi);
    561 
    562   rctx->granularity = MHD_lookup_connection_value (connection,
    563                                                    MHD_GET_ARGUMENT_KIND,
    564                                                    "granularity");
    565   if (NULL == rctx->granularity)
    566   {
    567     rctx->granularity = "day";
    568     rctx->period = GNUNET_TIME_UNIT_DAYS;
    569     rctx->count = 95;
    570   }
    571   else
    572   {
    573     const struct
    574     {
    575       const char *name;
    576       struct GNUNET_TIME_Relative period;
    577       uint64_t default_counter;
    578     } map[] = {
    579       {
    580         .name = "second",
    581         .period = GNUNET_TIME_UNIT_SECONDS,
    582         .default_counter = 120,
    583       },
    584       {
    585         .name = "minute",
    586         .period = GNUNET_TIME_UNIT_MINUTES,
    587         .default_counter = 120,
    588       },
    589       {
    590         .name = "hour",
    591         .period = GNUNET_TIME_UNIT_HOURS,
    592         .default_counter = 48,
    593       },
    594       {
    595         .name = "day",
    596         .period = GNUNET_TIME_UNIT_DAYS,
    597         .default_counter = 95,
    598       },
    599       {
    600         .name = "week",
    601         .period = GNUNET_TIME_UNIT_WEEKS,
    602         .default_counter = 12,
    603       },
    604       {
    605         .name = "month",
    606         .period = GNUNET_TIME_UNIT_MONTHS,
    607         .default_counter = 36,
    608       },
    609       {
    610         .name = "quarter",
    611         .period = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MONTHS,
    612                                                  3),
    613         .default_counter = 40,
    614       },
    615       {
    616         .name = "year",
    617         .period = GNUNET_TIME_UNIT_YEARS,
    618         .default_counter = 10
    619       },
    620       {
    621         .name = NULL
    622       }
    623     };
    624 
    625     rctx->count = 0;
    626     for (unsigned int i = 0; map[i].name != NULL; i++)
    627     {
    628       if (0 == strcasecmp (map[i].name,
    629                            rctx->granularity))
    630       {
    631         rctx->count = map[i].default_counter;
    632         rctx->period = map[i].period;
    633         break;
    634       }
    635     }
    636     if (0 == rctx->count)
    637     {
    638       GNUNET_break_op (0);
    639       return TALER_MHD_reply_with_error (
    640         connection,
    641         MHD_HTTP_GONE,
    642         TALER_EC_MERCHANT_PRIVATE_GET_STATISTICS_REPORT_GRANULARITY_UNAVAILABLE,
    643         rctx->granularity);
    644     }
    645   } /* end handling granularity */
    646 
    647   /* Figure out desired output format */
    648   {
    649     const char *mime;
    650 
    651     mime = MHD_lookup_connection_value (connection,
    652                                         MHD_HEADER_KIND,
    653                                         MHD_HTTP_HEADER_ACCEPT);
    654     if (NULL == mime)
    655       mime = "application/json";
    656     if (0 == strcmp (mime,
    657                      "application/json"))
    658     {
    659       rctx->format = RCF_JSON;
    660     }
    661     else if (0 == strcmp (mime,
    662                           "application/pdf"))
    663     {
    664 
    665       rctx->format = RCF_PDF;
    666     }
    667     else
    668     {
    669       GNUNET_break_op (0);
    670       return TALER_MHD_REPLY_JSON_PACK (
    671         connection,
    672         MHD_HTTP_NOT_ACCEPTABLE,
    673         GNUNET_JSON_pack_string ("hint",
    674                                  mime));
    675     }
    676   } /* end of determine output format */
    677 
    678   TALER_MHD_parse_request_number (connection,
    679                                   "count",
    680                                   &rctx->count);
    681   /* Cap the client-supplied count to a sane maximum to prevent
    682      resource amplification (used both as DB bucket count and in
    683      GNUNET_TIME_relative_multiply() for the start_date). */
    684   if (rctx->count > MAX_DELTA)
    685     rctx->count = MAX_DELTA;
    686 
    687   /* create charts */
    688   charts = json_array ();
    689   GNUNET_assert (NULL != charts);
    690   {
    691     enum GNUNET_GenericReturnValue ret;
    692 
    693     ret = make_transaction_volume_report (rctx,
    694                                           charts);
    695     if (GNUNET_OK != ret)
    696       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    697     ret = make_transaction_count_report (rctx,
    698                                          charts);
    699     if (GNUNET_OK != ret)
    700       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    701   }
    702 
    703   /* generate response */
    704   {
    705     struct GNUNET_TIME_Timestamp start_date;
    706     struct GNUNET_TIME_Timestamp end_date;
    707     json_t *root;
    708 
    709     end_date = rctx->now;
    710     start_date
    711       = GNUNET_TIME_absolute_to_timestamp (
    712           GNUNET_TIME_absolute_subtract (
    713             end_date.abs_time,
    714             GNUNET_TIME_relative_multiply (rctx->period,
    715                                            rctx->count)));
    716     root = GNUNET_JSON_PACK (
    717       GNUNET_JSON_pack_string ("business_name",
    718                                mi->settings.name),
    719       GNUNET_JSON_pack_timestamp ("start_date",
    720                                   start_date),
    721       GNUNET_JSON_pack_timestamp ("end_date",
    722                                   end_date),
    723       GNUNET_JSON_pack_time_rel ("bucket_period",
    724                                  rctx->period),
    725       GNUNET_JSON_pack_array_steal ("charts",
    726                                     charts));
    727 
    728     switch (rctx->format)
    729     {
    730     case RCF_JSON:
    731       return TALER_MHD_reply_json (connection,
    732                                    root,
    733                                    MHD_HTTP_OK);
    734     case RCF_PDF:
    735       {
    736         struct TALER_MHD_TypstDocument doc = {
    737           .form_name = "transactions",
    738           .form_version = "0.0.0",
    739           .data = root
    740         };
    741 
    742         rctx->tc = TALER_MHD_typst (TALER_MERCHANT_project_data (),
    743                                     TMH_cfg,
    744                                     false, /* remove on exit */
    745                                     "merchant",
    746                                     1, /* one document, length of "array"! */
    747                                     &doc,
    748                                     &pdf_cb,
    749                                     rctx);
    750         json_decref (root);
    751         if (NULL == rctx->tc)
    752         {
    753           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    754                       "Client requested PDF, but Typst is unavailable\n");
    755           return TALER_MHD_reply_with_error (
    756             connection,
    757             MHD_HTTP_NOT_IMPLEMENTED,
    758             TALER_EC_MERCHANT_GENERIC_NO_TYPST_OR_PDFTK,
    759             NULL);
    760         }
    761         GNUNET_CONTAINER_DLL_insert (rctx_head,
    762                                      rctx_tail,
    763                                      rctx);
    764         rctx->suspended = GNUNET_YES;
    765         MHD_suspend_connection (connection);
    766         return MHD_YES;
    767       }
    768     } /* end switch */
    769   }
    770   GNUNET_assert (0);
    771   return MHD_NO;
    772 }
    773 
    774 
    775 /* end of taler-merchant-httpd_get-private-statistics-report-transactions.c */