merchant

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

taler-merchant-httpd_get-private-orders.c (50806B)


      1 /*
      2   This file is part of TALER
      3   (C) 2019--2026 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-orders.c
     18  * @brief implement GET /orders
     19  * @author Christian Grothoff
     20  *
     21  * FIXME-cleanup: consider introducing phases / state machine
     22  */
     23 #include "platform.h"
     24 #include "taler-merchant-httpd_get-private-orders.h"
     25 #include <taler/taler_merchant_util.h>
     26 #include <taler/taler_json_lib.h>
     27 #include <taler/taler_dbevents.h>
     28 #include "merchant-database/lookup_contract_terms3.h"
     29 #include "merchant-database/lookup_order.h"
     30 #include "merchant-database/set_instance.h"
     31 #include "merchant-database/lookup_order_status_by_serial.h"
     32 #include "merchant-database/lookup_orders.h"
     33 #include "merchant-database/lookup_refunds_detailed.h"
     34 #include "merchant-database/event_listen.h"
     35 #include "merchant-database/preflight.h"
     36 #include "merchant-database/event_notify.h"
     37 
     38 
     39 /**
     40  * Sensible bound on TALER_MERCHANTDB_OrderFilter.delta
     41  */
     42 #define MAX_DELTA 1024
     43 
     44 #define CSV_HEADER \
     45         "Order ID,Row,YYYY-MM-DD,HH:MM,Timestamp,Amount,Refund amount,Pending refund amount,Summary,Refundable,Paid\r\n"
     46 #define CSV_FOOTER "\r\n"
     47 
     48 #define XML_HEADER "<?xml version=\"1.0\"?>" \
     49         "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \
     50         " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \
     51         " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \
     52         " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \
     53         " xmlns:o=\"urn:schemas-microsoft-com:office:office\""        \
     54         " xmlns:x=\"urn:schemas-microsoft-com:office:excel\""         \
     55         " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \
     56         "<Styles>" \
     57         "<Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd hh:mm:ss\"/></Style>" \
     58         "<Style ss:ID=\"Total\"><Font ss:Bold=\"1\"/></Style>" \
     59         "</Styles>\n" \
     60         "<Worksheet ss:Name=\"Orders\">\n"                            \
     61         "<Table>\n" \
     62         "<Row>\n" \
     63         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Order ID</Data></Cell>\n" \
     64         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Timestamp</Data></Cell>\n" \
     65         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Price</Data></Cell>\n" \
     66         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Refunded</Data></Cell>\n" \
     67         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Summary</Data></Cell>\n" \
     68         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Paid</Data></Cell>\n" \
     69         "</Row>\n"
     70 #define XML_FOOTER "</Table></Worksheet></Workbook>"
     71 
     72 
     73 /**
     74  * A pending GET /orders request.
     75  */
     76 struct TMH_PendingOrder
     77 {
     78 
     79   /**
     80    * Kept in a DLL.
     81    */
     82   struct TMH_PendingOrder *prev;
     83 
     84   /**
     85    * Kept in a DLL.
     86    */
     87   struct TMH_PendingOrder *next;
     88 
     89   /**
     90    * Which connection was suspended.
     91    */
     92   struct MHD_Connection *con;
     93 
     94   /**
     95    * Which instance is this client polling? This also defines
     96    * which DLL this struct is part of.
     97    */
     98   struct TMH_MerchantInstance *mi;
     99 
    100   /**
    101    * At what time does this request expire? If set in the future, we
    102    * may wait this long for a payment to arrive before responding.
    103    */
    104   struct GNUNET_TIME_Absolute long_poll_timeout;
    105 
    106   /**
    107    * Filter to apply.
    108    */
    109   struct TALER_MERCHANTDB_OrderFilter of;
    110 
    111   /**
    112    * The array of orders (used for JSON and PDF/Typst).
    113    */
    114   json_t *pa;
    115 
    116   /**
    117    * Running total of order amounts, for totals row in CSV/XML/PDF.
    118    * Initialised to zero on first order seen.
    119    */
    120   struct TALER_AmountSet total_amount;
    121 
    122   /**
    123    * Running total of granted refund amounts.
    124    * Initialised to zero on first paid order seen.
    125    */
    126   struct TALER_AmountSet total_refund_amount;
    127 
    128   /**
    129    * Running total of pending refund amounts.
    130    * Initialised to zero on first paid order seen.
    131    */
    132   struct TALER_AmountSet total_pending_refund_amount;
    133 
    134   /**
    135    * The name of the instance we are querying for.
    136    */
    137   const char *instance_id;
    138 
    139   /**
    140    * Alias of @a of.summary_filter, but with memory to be released (owner).
    141    */
    142   char *summary_filter;
    143 
    144   /**
    145    * The result after adding the orders (#TALER_EC_NONE for okay, anything else for an error).
    146    */
    147   enum TALER_ErrorCode result;
    148 
    149   /**
    150    * Is the structure in the DLL
    151    */
    152   bool in_dll;
    153 
    154   /**
    155    * Output format requested by the client.
    156    */
    157   enum
    158   {
    159     POF_JSON,
    160     POF_CSV,
    161     POF_XML,
    162     POF_PDF
    163   } format;
    164 
    165   /**
    166    * Buffer used when format is #POF_CSV.
    167    */
    168   struct GNUNET_Buffer csv;
    169 
    170   /**
    171    * Buffer used when format is #POF_XML.
    172    */
    173   struct GNUNET_Buffer xml;
    174 
    175   /**
    176    * Async context used to run Typst (for #POF_PDF).
    177    */
    178   struct TALER_MHD_TypstContext *tc;
    179 
    180   /**
    181    * Pre-built MHD response (used when #POF_PDF Typst is done).
    182    */
    183   struct MHD_Response *response;
    184 
    185   /**
    186    * Task to timeout pending order.
    187    */
    188   struct GNUNET_SCHEDULER_Task *order_timeout_task;
    189 
    190   /**
    191    * HTTP status to return with @e response.
    192    */
    193   unsigned int http_status;
    194 };
    195 
    196 
    197 /**
    198  * DLL head for requests suspended waiting for Typst.
    199  */
    200 static struct TMH_PendingOrder *pdf_head;
    201 
    202 /**
    203  * DLL tail for requests suspended waiting for Typst.
    204  */
    205 static struct TMH_PendingOrder *pdf_tail;
    206 
    207 
    208 void
    209 TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi)
    210 {
    211   struct TMH_PendingOrder *po;
    212 
    213   while (NULL != (po = mi->po_head))
    214   {
    215     GNUNET_assert (po->in_dll);
    216     GNUNET_CONTAINER_DLL_remove (mi->po_head,
    217                                  mi->po_tail,
    218                                  po);
    219     MHD_resume_connection (po->con);
    220     po->in_dll = false;
    221   }
    222   if (NULL != mi->po_eh)
    223   {
    224     TALER_MERCHANTDB_event_listen_cancel (mi->po_eh);
    225     mi->po_eh = NULL;
    226   }
    227 }
    228 
    229 
    230 void
    231 TMH_force_get_orders_resume_typst ()
    232 {
    233   struct TMH_PendingOrder *po;
    234 
    235   while (NULL != (po = pdf_head))
    236   {
    237     GNUNET_CONTAINER_DLL_remove (pdf_head,
    238                                  pdf_tail,
    239                                  po);
    240     MHD_resume_connection (po->con);
    241   }
    242 }
    243 
    244 
    245 /**
    246  * Task run to trigger timeouts on GET /orders requests with long polling.
    247  *
    248  * @param cls a `struct TMH_PendingOrder *`
    249  */
    250 static void
    251 order_timeout (void *cls)
    252 {
    253   struct TMH_PendingOrder *po = cls;
    254   struct TMH_MerchantInstance *mi = po->mi;
    255 
    256   po->order_timeout_task = NULL;
    257   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    258               "Resuming long polled job due to timeout\n");
    259   GNUNET_assert (po->in_dll);
    260   GNUNET_CONTAINER_DLL_remove (mi->po_head,
    261                                mi->po_tail,
    262                                po);
    263   po->in_dll = false;
    264   MHD_resume_connection (po->con);
    265   TALER_MHD_daemon_trigger ();   /* we resumed, kick MHD */
    266 }
    267 
    268 
    269 /**
    270  * Cleanup our "context", where we stored the data
    271  * we are building for the response.
    272  *
    273  * @param ctx context to clean up, must be a `struct TMH_PendingOrder *`
    274  */
    275 static void
    276 cleanup (void *ctx)
    277 {
    278   struct TMH_PendingOrder *po = ctx;
    279 
    280   if (po->in_dll)
    281   {
    282     struct TMH_MerchantInstance *mi = po->mi;
    283 
    284     GNUNET_CONTAINER_DLL_remove (mi->po_head,
    285                                  mi->po_tail,
    286                                  po);
    287     MHD_resume_connection (po->con);
    288   }
    289   if (NULL != po->order_timeout_task)
    290   {
    291     GNUNET_SCHEDULER_cancel (po->order_timeout_task);
    292     po->order_timeout_task = NULL;
    293   }
    294   json_decref (po->pa);
    295   TALER_amount_set_free (&po->total_amount);
    296   TALER_amount_set_free (&po->total_refund_amount);
    297   TALER_amount_set_free (&po->total_pending_refund_amount);
    298   GNUNET_free (po->summary_filter);
    299   switch (po->format)
    300   {
    301   case POF_JSON:
    302     break;
    303   case POF_CSV:
    304     GNUNET_buffer_clear (&po->csv);
    305     break;
    306   case POF_XML:
    307     GNUNET_buffer_clear (&po->xml);
    308     break;
    309   case POF_PDF:
    310     if (NULL != po->tc)
    311     {
    312       TALER_MHD_typst_cancel (po->tc);
    313       po->tc = NULL;
    314     }
    315     break;
    316   }
    317   if (NULL != po->response)
    318   {
    319     MHD_destroy_response (po->response);
    320     po->response = NULL;
    321   }
    322   GNUNET_free (po);
    323 }
    324 
    325 
    326 /**
    327  * Closure for #process_refunds_cb().
    328  */
    329 struct ProcessRefundsClosure
    330 {
    331   /**
    332    * Place where we accumulate the granted refunds.
    333    */
    334   struct TALER_Amount total_refund_amount;
    335 
    336   /**
    337    * Place where we accumulate the pending refunds.
    338    */
    339   struct TALER_Amount pending_refund_amount;
    340 
    341   /**
    342    * Set to an error code if something goes wrong.
    343    */
    344   enum TALER_ErrorCode ec;
    345 };
    346 
    347 
    348 /**
    349  * Function called with information about a refund.
    350  * It is responsible for summing up the refund amount.
    351  *
    352  * @param cls closure
    353  * @param refund_serial unique serial number of the refund
    354  * @param timestamp time of the refund (for grouping of refunds in the wallet UI)
    355  * @param coin_pub public coin from which the refund comes from
    356  * @param exchange_url URL of the exchange that issued @a coin_pub
    357  * @param rtransaction_id identificator of the refund
    358  * @param reason human-readable explanation of the refund
    359  * @param refund_amount refund amount which is being taken from @a coin_pub
    360  * @param pending true if the this refund was not yet processed by the wallet/exchange
    361  */
    362 static void
    363 process_refunds_cb (void *cls,
    364                     uint64_t refund_serial,
    365                     struct GNUNET_TIME_Timestamp timestamp,
    366                     const struct TALER_CoinSpendPublicKeyP *coin_pub,
    367                     const char *exchange_url,
    368                     uint64_t rtransaction_id,
    369                     const char *reason,
    370                     const struct TALER_Amount *refund_amount,
    371                     bool pending)
    372 {
    373   struct ProcessRefundsClosure *prc = cls;
    374 
    375   if (GNUNET_OK !=
    376       TALER_amount_cmp_currency (&prc->total_refund_amount,
    377                                  refund_amount))
    378   {
    379     /* Database error, refunds in mixed currency in DB. Not OK! */
    380     prc->ec = TALER_EC_GENERIC_DB_INVARIANT_FAILURE;
    381     GNUNET_break (0);
    382     return;
    383   }
    384   GNUNET_assert (0 <=
    385                  TALER_amount_add (&prc->total_refund_amount,
    386                                    &prc->total_refund_amount,
    387                                    refund_amount));
    388   if (pending)
    389     GNUNET_assert (0 <=
    390                    TALER_amount_add (&prc->pending_refund_amount,
    391                                      &prc->pending_refund_amount,
    392                                      refund_amount));
    393 }
    394 
    395 
    396 /**
    397  * Add one order entry to the running order-amount total in @a po.
    398  * Sets po->result to TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow.
    399  *
    400  * @param[in,out] po pending order accumulator
    401  * @param amount the order amount to add
    402  */
    403 static void
    404 accumulate_total (struct TMH_PendingOrder *po,
    405                   const struct TALER_Amount *amount)
    406 {
    407   if (0 > TALER_amount_set_add (&po->total_amount,
    408                                 amount,
    409                                 NULL))
    410   {
    411     GNUNET_break (0);
    412     po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
    413   }
    414 }
    415 
    416 
    417 /**
    418  * Add refund amounts to the running refund totals in @a po.
    419  * Sets po->result to TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow.
    420  * Only called for paid orders (where refund tracking is meaningful).
    421  *
    422  * @param[in,out] po pending order accumulator
    423  * @param refund granted refund amount for this order
    424  * @param pending pending (not-yet-processed) refund amount for this order
    425  */
    426 static void
    427 accumulate_refund_totals (struct TMH_PendingOrder *po,
    428                           const struct TALER_Amount *refund,
    429                           const struct TALER_Amount *pending)
    430 {
    431   if (TALER_EC_NONE != po->result)
    432     return;
    433   if (0 > TALER_amount_set_add (&po->total_refund_amount,
    434                                 refund,
    435                                 NULL))
    436   {
    437     GNUNET_break (0);
    438     po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
    439     return;
    440   }
    441   if (0 > TALER_amount_set_add (&po->total_pending_refund_amount,
    442                                 pending,
    443                                 NULL))
    444   {
    445     GNUNET_break (0);
    446     po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
    447   }
    448 }
    449 
    450 
    451 /**
    452  * Add order details to our response accumulator.
    453  *
    454  * @param cls some closure
    455  * @param orig_order_id the order this is about
    456  * @param order_serial serial ID of the order
    457  * @param creation_time when was the order created
    458  */
    459 static void
    460 add_order (void *cls,
    461            const char *orig_order_id,
    462            uint64_t order_serial,
    463            struct GNUNET_TIME_Timestamp creation_time)
    464 {
    465   struct TMH_PendingOrder *po = cls;
    466   json_t *terms = NULL;
    467   struct TALER_PrivateContractHashP h_contract_terms;
    468   enum GNUNET_DB_QueryStatus qs;
    469   char *order_id = NULL;
    470   bool refundable = false;
    471   bool paid;
    472   bool wired;
    473   struct TALER_MERCHANT_Contract *contract = NULL;
    474   struct TALER_MERCHANT_Order *order = NULL;
    475   int16_t choice_index = -1;
    476   struct ProcessRefundsClosure prc = {
    477     .ec = TALER_EC_NONE
    478   };
    479   const struct TALER_Amount *amount;
    480   char amount_buf[128];
    481   char refund_buf[128];
    482   char pending_buf[128];
    483   const struct TALER_MERCHANT_ContractBaseTerms *ct = NULL;
    484 
    485   /* Bail early if we already have an error */
    486   if (TALER_EC_NONE != po->result)
    487     return;
    488 
    489   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    490               "Adding order `%s' (%llu) to result set at instance `%s'\n",
    491               orig_order_id,
    492               (unsigned long long) order_serial,
    493               po->instance_id);
    494   qs = TALER_MERCHANTDB_set_instance (
    495     TMH_db,
    496     po->instance_id);
    497   if (qs <= 0)
    498   {
    499     GNUNET_break (0);
    500     po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
    501     return;
    502   }
    503   qs = TALER_MERCHANTDB_lookup_order_status_by_serial (TMH_db,
    504                                                        po->instance_id,
    505                                                        order_serial,
    506                                                        &order_id,
    507                                                        &h_contract_terms,
    508                                                        &paid);
    509   if (qs < 0)
    510   {
    511     GNUNET_break (0);
    512     po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
    513     goto cleanup;
    514   }
    515   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    516   {
    517     /* Contract terms don't exist, so the order cannot be paid. */
    518     paid = false;
    519     if (NULL == orig_order_id)
    520     {
    521       /* Got a DB trigger about a new proposal, but it
    522          was already deleted again. Just ignore the event. */
    523       GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    524                     TALER_MERCHANTDB_set_instance (
    525                       TMH_db,
    526                       NULL));
    527       return;
    528     }
    529     order_id = GNUNET_strdup (orig_order_id);
    530   }
    531 
    532   {
    533     /* First try to find the order in the contracts */
    534     uint64_t os;
    535     bool session_matches;
    536 
    537     qs = TALER_MERCHANTDB_lookup_contract_terms3 (TMH_db,
    538                                                   po->instance_id,
    539                                                   order_id,
    540                                                   NULL,
    541                                                   &terms,
    542                                                   &os,
    543                                                   &paid,
    544                                                   &wired,
    545                                                   &session_matches,
    546                                                   NULL,
    547                                                   &choice_index);
    548     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
    549     {
    550       GNUNET_break (os == order_serial);
    551       contract = TALER_MERCHANT_contract_parse (terms);
    552       if (NULL == contract)
    553       {
    554         GNUNET_break (0);
    555         po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID;
    556         goto cleanup;
    557       }
    558       ct = contract->pc->base;
    559     }
    560   }
    561   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    562   {
    563     /* Might still be unclaimed, so try order table */
    564     struct TALER_MerchantPostDataHashP unused;
    565 
    566     paid = false;
    567     wired = false;
    568     qs = TALER_MERCHANTDB_lookup_order (TMH_db,
    569                                         po->instance_id,
    570                                         order_id,
    571                                         NULL,
    572                                         &unused,
    573                                         &terms);
    574     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
    575     {
    576       order = TALER_MERCHANT_order_parse (terms);
    577       if (NULL == order)
    578       {
    579         GNUNET_break (0);
    580         po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID;
    581         goto cleanup;
    582       }
    583       ct = order->base;
    584     }
    585   }
    586   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    587   {
    588     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    589                 "Order %llu disappeared during iteration. Skipping.\n",
    590                 (unsigned long long) order_serial);
    591     goto cleanup;
    592   }
    593   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    594   {
    595     GNUNET_break (0);
    596     po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
    597     goto cleanup;
    598   }
    599 
    600 
    601   if (paid)
    602   {
    603     const struct TALER_Amount *brutto;
    604 
    605     GNUNET_assert (NULL != contract);
    606     switch (ct->version)
    607     {
    608     case TALER_MERCHANT_CONTRACT_VERSION_0:
    609       brutto = &contract->pc->details.v0.brutto;
    610       break;
    611     case TALER_MERCHANT_CONTRACT_VERSION_1:
    612       {
    613         struct TALER_MERCHANT_ContractChoice *choice
    614           = &contract->pc->details.v1.choices[choice_index];
    615 
    616         GNUNET_assert (choice_index < contract->pc->details.v1.choices_len);
    617         brutto = &choice->amount;
    618       }
    619       break;
    620     default:
    621       GNUNET_break (0);
    622       goto cleanup;
    623     }
    624     GNUNET_assert (GNUNET_OK ==
    625                    TALER_amount_set_zero (brutto->currency,
    626                                           &prc.total_refund_amount));
    627     GNUNET_assert (GNUNET_OK ==
    628                    TALER_amount_set_zero (brutto->currency,
    629                                           &prc.pending_refund_amount));
    630 
    631     qs = TALER_MERCHANTDB_lookup_refunds_detailed (TMH_db,
    632                                                    po->instance_id,
    633                                                    &h_contract_terms,
    634                                                    &process_refunds_cb,
    635                                                    &prc);
    636     if (0 > qs)
    637     {
    638       GNUNET_break (0);
    639       po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
    640       goto cleanup;
    641     }
    642     if (TALER_EC_NONE != prc.ec)
    643     {
    644       GNUNET_break (0);
    645       po->result = prc.ec;
    646       goto cleanup;
    647     }
    648     if (0 > TALER_amount_cmp (&prc.total_refund_amount,
    649                               brutto) &&
    650         GNUNET_TIME_absolute_is_future (
    651           contract->pc->refund_deadline.abs_time))
    652       refundable = true;
    653   }
    654 
    655   /* compute amount totals */
    656   amount = NULL;
    657   switch (ct->version)
    658   {
    659   case TALER_MERCHANT_CONTRACT_VERSION_0:
    660     {
    661       amount = (NULL != contract)
    662         ? &contract->pc->details.v0.brutto
    663         : &order->details.v0.brutto;
    664 
    665       if (TALER_amount_is_zero (amount) &&
    666           (po->of.wired != TALER_EXCHANGE_YNA_ALL) )
    667       {
    668         /* If we are actually filtering by wire status,
    669            and the order was over an amount of zero,
    670            do not return it as wire status is not
    671            exactly meaningful for orders over zero. */
    672         goto cleanup;
    673       }
    674 
    675       /* Accumulate order total */
    676       if (paid)
    677         accumulate_total (po,
    678                           amount);
    679       if (TALER_EC_NONE != po->result)
    680         goto cleanup;
    681       /* Accumulate refund totals (only meaningful for paid orders) */
    682       if (paid)
    683       {
    684         accumulate_refund_totals (po,
    685                                   &prc.total_refund_amount,
    686                                   &prc.pending_refund_amount);
    687         if (TALER_EC_NONE != po->result)
    688           goto cleanup;
    689       }
    690     }
    691     break;
    692   case TALER_MERCHANT_CONTRACT_VERSION_1:
    693     if (-1 == choice_index)
    694       choice_index = 0; /* default choice */
    695     if (NULL != contract)
    696     {
    697       struct TALER_MERCHANT_ContractChoice *choice
    698         = &contract->pc->details.v1.choices[choice_index];
    699 
    700       GNUNET_assert (choice_index < contract->pc->details.v1.choices_len);
    701       amount = &choice->amount;
    702       /* Accumulate order total */
    703       accumulate_total (po,
    704                         amount);
    705       if (TALER_EC_NONE != po->result)
    706         goto cleanup;
    707       /* Accumulate refund totals (only meaningful for paid orders) */
    708       if (paid)
    709       {
    710         accumulate_refund_totals (po,
    711                                   &prc.total_refund_amount,
    712                                   &prc.pending_refund_amount);
    713         if (TALER_EC_NONE != po->result)
    714           goto cleanup;
    715       }
    716     }
    717     else
    718     {
    719       struct TALER_MERCHANT_OrderChoice *choice
    720         = &order->details.v1.choices[choice_index];
    721 
    722       GNUNET_assert (choice_index < order->details.v1.choices_len);
    723       amount = &choice->amount;
    724       /* Accumulate order total */
    725       accumulate_total (po,
    726                         amount);
    727       if (TALER_EC_NONE != po->result)
    728         goto cleanup;
    729     }
    730     break;
    731   default:
    732     GNUNET_break (0);
    733     po->result = TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION;
    734     goto cleanup;
    735   }
    736 
    737   /* convert amounts to strings (needed for some formats) */
    738   /* FIXME: use currency formatting rules in the future
    739      instead of TALER_amount2s for human readability... */
    740   strcpy (amount_buf,
    741           TALER_amount2s (amount));
    742   if (paid)
    743     strcpy (refund_buf,
    744             TALER_amount2s (&prc.total_refund_amount));
    745   if (paid)
    746     strcpy (pending_buf,
    747             TALER_amount2s (&prc.pending_refund_amount));
    748 
    749   switch (po->format)
    750   {
    751   case POF_JSON:
    752   case POF_PDF:
    753     GNUNET_assert (
    754       0 ==
    755       json_array_append_new (
    756         po->pa,
    757         GNUNET_JSON_PACK (
    758           GNUNET_JSON_pack_string ("order_id",
    759                                    order_id),
    760           GNUNET_JSON_pack_uint64 ("row_id",
    761                                    order_serial),
    762           GNUNET_JSON_pack_timestamp ("timestamp",
    763                                       creation_time),
    764           TALER_JSON_pack_amount ("amount",
    765                                   amount),
    766           GNUNET_JSON_pack_allow_null (
    767             TALER_JSON_pack_amount (
    768               "refund_amount",
    769               paid
    770                 ? &prc.total_refund_amount
    771                 : NULL)),
    772           GNUNET_JSON_pack_allow_null (
    773             TALER_JSON_pack_amount (
    774               "pending_refund_amount",
    775               paid
    776                 ? &prc.pending_refund_amount
    777                 : NULL)),
    778           GNUNET_JSON_pack_string ("summary",
    779                                    ct->summary),
    780           GNUNET_JSON_pack_bool ("refundable",
    781                                  refundable),
    782           GNUNET_JSON_pack_bool ("paid",
    783                                  paid))));
    784     break;
    785   case POF_CSV:
    786     {
    787       size_t len = strlen (ct->summary);
    788       size_t wpos = 0;
    789       char *esummary;
    790       struct tm *tm;
    791       time_t t;
    792 
    793       /* Escape 'summary' to double '"' as per RFC 4180, 2.7. */
    794       esummary = GNUNET_malloc (2 * len + 1);
    795       for (size_t off = 0; off<len; off++)
    796       {
    797         if ('"' == ct->summary[off])
    798           esummary[wpos++] = '"';
    799         esummary[wpos++] = ct->summary[off];
    800       }
    801       t = GNUNET_TIME_timestamp_to_s (creation_time);
    802       tm = localtime (&t);
    803       GNUNET_buffer_write_fstr (
    804         &po->csv,
    805         "%s,%llu,%04u-%02u-%02u,%02u:%02u (%s),%llu,%s,%s,%s,\"%s\",%s,%s\r\n",
    806         order_id,
    807         (unsigned long long) order_serial,
    808         tm->tm_year + 1900,
    809         tm->tm_mon + 1,
    810         tm->tm_mday,
    811         tm->tm_hour,
    812         tm->tm_min,
    813         tm->tm_zone,
    814         (unsigned long long) t,
    815         amount_buf,
    816         paid ? refund_buf : "",
    817         paid ? pending_buf : "",
    818         esummary,
    819         refundable ? "yes" : "no",
    820         paid ? "yes" : "no");
    821       GNUNET_free (esummary);
    822       break;
    823     }
    824   case POF_XML:
    825     {
    826       char *esummary = TALER_escape_xml (ct->summary);
    827       char creation_time_s[128];
    828       const struct tm *tm;
    829       time_t tt;
    830 
    831       tt = (time_t) GNUNET_TIME_timestamp_to_s (creation_time);
    832       tm = gmtime (&tt);
    833       strftime (creation_time_s,
    834                 sizeof (creation_time_s),
    835                 "%Y-%m-%dT%H:%M:%S",
    836                 tm);
    837       GNUNET_buffer_write_fstr (
    838         &po->xml,
    839         "<Row>"
    840         "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
    841         "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"DateTime\">%s</Data></Cell>"
    842         "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
    843         "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
    844         "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
    845         "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>"
    846         "</Row>\n",
    847         order_id,
    848         creation_time_s,
    849         amount_buf,
    850         paid ? refund_buf : "",
    851         NULL != esummary ? esummary : "",
    852         paid ? "TRUE" : "FALSE",
    853         paid ? "1" : "0");
    854       GNUNET_free (esummary);
    855     }
    856     break;
    857   }    /* end switch po->format */
    858 
    859 cleanup:
    860   GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    861                 TALER_MERCHANTDB_set_instance (
    862                   TMH_db,
    863                   NULL));
    864   json_decref (terms);
    865   GNUNET_free (order_id);
    866   if (NULL != order)
    867   {
    868     TALER_MERCHANT_order_free (order);
    869     order = NULL;
    870   }
    871   if (NULL != contract)
    872   {
    873     TALER_MERCHANT_contract_free (contract);
    874     contract = NULL;
    875   }
    876 }
    877 
    878 
    879 /**
    880  * We have received a trigger from the database
    881  * that we should (possibly) resume some requests.
    882  *
    883  * @param cls a `struct TMH_MerchantInstance`
    884  * @param extra a `struct TMH_OrderChangeEventP`
    885  * @param extra_size number of bytes in @a extra
    886  */
    887 static void
    888 resume_by_event (void *cls,
    889                  const void *extra,
    890                  size_t extra_size)
    891 {
    892   struct TMH_MerchantInstance *mi = cls;
    893   const struct TMH_OrderChangeEventDetailsP *oce = extra;
    894   struct TMH_PendingOrder *pn;
    895   enum TMH_OrderStateFlags osf;
    896   uint64_t order_serial_id;
    897   struct GNUNET_TIME_Timestamp date;
    898 
    899   if (sizeof (*oce) != extra_size)
    900   {
    901     GNUNET_break (0);
    902     return;
    903   }
    904   osf = (enum TMH_OrderStateFlags) (int) ntohl (oce->order_state);
    905   order_serial_id = GNUNET_ntohll (oce->order_serial_id);
    906   date = GNUNET_TIME_timestamp_ntoh (oce->execution_date);
    907   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    908               "Received notification about order %llu\n",
    909               (unsigned long long) order_serial_id);
    910   for (struct TMH_PendingOrder *po = mi->po_head;
    911        NULL != po;
    912        po = pn)
    913   {
    914     pn = po->next;
    915     if (! ( ( ((TALER_EXCHANGE_YNA_YES == po->of.paid) ==
    916                (0 != (osf & TMH_OSF_PAID))) ||
    917               (TALER_EXCHANGE_YNA_ALL == po->of.paid) ) &&
    918             ( ((TALER_EXCHANGE_YNA_YES == po->of.refunded) ==
    919                (0 != (osf & TMH_OSF_REFUNDED))) ||
    920               (TALER_EXCHANGE_YNA_ALL == po->of.refunded) ) &&
    921             ( ((TALER_EXCHANGE_YNA_YES == po->of.wired) ==
    922                (0 != (osf & TMH_OSF_WIRED))) ||
    923               (TALER_EXCHANGE_YNA_ALL == po->of.wired) ) ) )
    924     {
    925       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    926                   "Client %p waits on different order type\n",
    927                   po);
    928       continue;
    929     }
    930     if (po->of.delta > 0)
    931     {
    932       if (order_serial_id < po->of.start_row)
    933       {
    934         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    935                     "Client %p waits on different order row\n",
    936                     po);
    937         continue;
    938       }
    939       if (GNUNET_TIME_timestamp_cmp (date,
    940                                      <,
    941                                      po->of.date))
    942       {
    943         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    944                     "Client %p waits on different order date\n",
    945                     po);
    946         continue;
    947       }
    948       po->of.delta--;
    949     }
    950     else
    951     {
    952       if (order_serial_id > po->of.start_row)
    953       {
    954         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    955                     "Client %p waits on different order row\n",
    956                     po);
    957         continue;
    958       }
    959       if (GNUNET_TIME_timestamp_cmp (date,
    960                                      >,
    961                                      po->of.date))
    962       {
    963         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    964                     "Client %p waits on different order date\n",
    965                     po);
    966         continue;
    967       }
    968       po->of.delta++;
    969     }
    970     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    971                 "Waking up client %p!\n",
    972                 po);
    973     add_order (po,
    974                NULL,
    975                order_serial_id,
    976                date);
    977     GNUNET_assert (po->in_dll);
    978     GNUNET_CONTAINER_DLL_remove (mi->po_head,
    979                                  mi->po_tail,
    980                                  po);
    981     po->in_dll = false;
    982     MHD_resume_connection (po->con);
    983     TALER_MHD_daemon_trigger ();   /* we resumed, kick MHD */
    984   }
    985   if (NULL == mi->po_head)
    986   {
    987     TALER_MERCHANTDB_event_listen_cancel (mi->po_eh);
    988     mi->po_eh = NULL;
    989   }
    990 }
    991 
    992 
    993 /**
    994  * There has been a change or addition of a new @a order_id.  Wake up
    995  * long-polling clients that may have been waiting for this event.
    996  *
    997  * @param mi the instance where the order changed
    998  * @param osf order state flags
    999  * @param date execution date of the order
   1000  * @param order_serial_id serial ID of the order in the database
   1001  */
   1002 void
   1003 TMH_notify_order_change (struct TMH_MerchantInstance *mi,
   1004                          enum TMH_OrderStateFlags osf,
   1005                          struct GNUNET_TIME_Timestamp date,
   1006                          uint64_t order_serial_id)
   1007 {
   1008   struct TMH_OrderChangeEventDetailsP oce = {
   1009     .order_serial_id = GNUNET_htonll (order_serial_id),
   1010     .execution_date = GNUNET_TIME_timestamp_hton (date),
   1011     .order_state = htonl (osf)
   1012   };
   1013   struct TMH_OrderChangeEventP eh = {
   1014     .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE),
   1015     .header.size = htons (sizeof (eh)),
   1016     .merchant_pub = mi->merchant_pub
   1017   };
   1018 
   1019   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
   1020               "Notifying clients of new order %llu at %s\n",
   1021               (unsigned long long) order_serial_id,
   1022               TALER_B2S (&mi->merchant_pub));
   1023   TALER_MERCHANTDB_event_notify (TMH_db,
   1024                                  &eh.header,
   1025                                  &oce,
   1026                                  sizeof (oce));
   1027 }
   1028 
   1029 
   1030 /**
   1031  * Transforms an (untrusted) input filter into a Postgresql LIKE filter.
   1032  * Escapes "%" and "_" in the @a input and adds "%" at the beginning
   1033  * and the end to turn the @a input into a suitable Postgresql argument.
   1034  *
   1035  * @param input text to turn into a substring match expression, or NULL
   1036  * @return NULL if @a input was NULL, otherwise transformed @a input
   1037  */
   1038 static char *
   1039 tr (const char *input)
   1040 {
   1041   char *out;
   1042   size_t slen;
   1043   size_t wpos;
   1044 
   1045   if (NULL == input)
   1046     return NULL;
   1047   slen = strlen (input);
   1048   out = GNUNET_malloc (slen * 2 + 3);
   1049   wpos = 0;
   1050   out[wpos++] = '%';
   1051   for (size_t i = 0; i<slen; i++)
   1052   {
   1053     char c = input[i];
   1054 
   1055     if ( (c == '%') ||
   1056          (c == '_') )
   1057       out[wpos++] = '\\';
   1058     out[wpos++] = c;
   1059   }
   1060   out[wpos++] = '%';
   1061   GNUNET_assert (wpos < slen * 2 + 3);
   1062   return out;
   1063 }
   1064 
   1065 
   1066 /**
   1067  * Function called with the result of a #TALER_MHD_typst() operation.
   1068  *
   1069  * @param cls closure, a `struct TMH_PendingOrder *`
   1070  * @param tr result of the operation
   1071  */
   1072 static void
   1073 pdf_cb (void *cls,
   1074         const struct TALER_MHD_TypstResponse *tr)
   1075 {
   1076   struct TMH_PendingOrder *po = cls;
   1077 
   1078   po->tc = NULL;
   1079   GNUNET_CONTAINER_DLL_remove (pdf_head,
   1080                                pdf_tail,
   1081                                po);
   1082   if (TALER_EC_NONE != tr->ec)
   1083   {
   1084     po->http_status
   1085       = TALER_ErrorCode_get_http_status (tr->ec);
   1086     po->response
   1087       = TALER_MHD_make_error (tr->ec,
   1088                               tr->details.hint);
   1089   }
   1090   else
   1091   {
   1092     po->http_status = MHD_HTTP_OK;
   1093     po->response = TALER_MHD_response_from_pdf_file (tr->details.filename);
   1094   }
   1095   MHD_resume_connection (po->con);
   1096   TALER_MHD_daemon_trigger ();
   1097 }
   1098 
   1099 
   1100 /**
   1101  * Build the final response for a completed (non-long-poll) request and
   1102  * queue it on @a connection.
   1103  *
   1104  * Handles all formats (JSON, CSV, XML, PDF).  For PDF this may suspend
   1105  * the connection while Typst runs asynchronously; in that case the caller
   1106  * must return #MHD_YES immediately.
   1107  *
   1108  * @param po the pending order state (already fully populated)
   1109  * @param connection the MHD connection
   1110  * @param mi the merchant instance
   1111  * @return MHD result code
   1112  */
   1113 static enum MHD_Result
   1114 reply_orders (struct TMH_PendingOrder *po,
   1115               struct MHD_Connection *connection,
   1116               struct TMH_MerchantInstance *mi)
   1117 {
   1118   char total_buf[128];
   1119   char refund_buf[128];
   1120   char pending_buf[128];
   1121 
   1122   switch (po->format)
   1123   {
   1124   case POF_JSON:
   1125     return TALER_MHD_REPLY_JSON_PACK (
   1126       connection,
   1127       MHD_HTTP_OK,
   1128       GNUNET_JSON_pack_array_incref ("orders",
   1129                                      po->pa));
   1130   case POF_CSV:
   1131     {
   1132       struct MHD_Response *resp;
   1133       enum MHD_Result mret;
   1134 
   1135       for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
   1136       {
   1137         struct TALER_Amount *tai = &po->total_amount.taa[i];
   1138         const struct TALER_Amount *r;
   1139 
   1140         strcpy (total_buf,
   1141                 TALER_amount2s (tai));
   1142         r = TALER_amount_set_find (tai->currency,
   1143                                    &po->total_refund_amount);
   1144         strcpy (refund_buf,
   1145                 TALER_amount2s (r));
   1146         r = TALER_amount_set_find (tai->currency,
   1147                                    &po->total_pending_refund_amount);
   1148         strcpy (pending_buf,
   1149                 TALER_amount2s (r));
   1150 
   1151         GNUNET_buffer_write_fstr (
   1152           &po->csv,
   1153           "Total (paid %s only),,,,%s,%s,%s,,,\r\n",
   1154           tai->currency,
   1155           total_buf,
   1156           refund_buf,
   1157           pending_buf);
   1158       }
   1159       GNUNET_buffer_write_str (&po->csv,
   1160                                CSV_FOOTER);
   1161       resp = MHD_create_response_from_buffer (po->csv.position,
   1162                                               po->csv.mem,
   1163                                               MHD_RESPMEM_MUST_COPY);
   1164       TALER_MHD_add_global_headers (resp,
   1165                                     false);
   1166       GNUNET_break (MHD_YES ==
   1167                     MHD_add_response_header (resp,
   1168                                              MHD_HTTP_HEADER_CONTENT_TYPE,
   1169                                              "text/csv"));
   1170       mret = MHD_queue_response (connection,
   1171                                  MHD_HTTP_OK,
   1172                                  resp);
   1173       MHD_destroy_response (resp);
   1174       return mret;
   1175     }
   1176   case POF_XML:
   1177     {
   1178       struct MHD_Response *resp;
   1179       enum MHD_Result mret;
   1180 
   1181       for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
   1182       {
   1183         struct TALER_Amount *tai = &po->total_amount.taa[i];
   1184         const struct TALER_Amount *r;
   1185 
   1186         strcpy (total_buf,
   1187                 TALER_amount2s (tai));
   1188         r = TALER_amount_set_find (tai->currency,
   1189                                    &po->total_refund_amount);
   1190         strcpy (refund_buf,
   1191                 TALER_amount2s (r));
   1192         r = TALER_amount_set_find (tai->currency,
   1193                                    &po->total_pending_refund_amount);
   1194         strcpy (pending_buf,
   1195                 TALER_amount2s (r));
   1196 
   1197         /* Append totals row with paid and refunded amount columns */
   1198         GNUNET_buffer_write_fstr (
   1199           &po->xml,
   1200           "<Row>"
   1201           "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">Total (paid %s only)</Data></Cell>"
   1202           "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>"
   1203           "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>"
   1204           "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>"
   1205           "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>"
   1206           "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>"
   1207           "</Row>\n",
   1208           tai->currency,
   1209           total_buf,
   1210           refund_buf);
   1211       }
   1212       GNUNET_buffer_write_str (&po->xml,
   1213                                XML_FOOTER);
   1214       resp = MHD_create_response_from_buffer (po->xml.position,
   1215                                               po->xml.mem,
   1216                                               MHD_RESPMEM_MUST_COPY);
   1217       TALER_MHD_add_global_headers (resp,
   1218                                     false);
   1219       GNUNET_break (MHD_YES ==
   1220                     MHD_add_response_header (resp,
   1221                                              MHD_HTTP_HEADER_CONTENT_TYPE,
   1222                                              "application/vnd.ms-excel"));
   1223       mret = MHD_queue_response (connection,
   1224                                  MHD_HTTP_OK,
   1225                                  resp);
   1226       MHD_destroy_response (resp);
   1227       return mret;
   1228     }
   1229   case POF_PDF:
   1230     {
   1231       /* Build the JSON document for Typst, passing all totals */
   1232       json_t *root;
   1233       struct TALER_MHD_TypstDocument doc;
   1234       json_t *ta = json_array ();
   1235       json_t *ra = json_array ();
   1236       json_t *pa = json_array ();
   1237 
   1238       GNUNET_assert (NULL != ta);
   1239       GNUNET_assert (NULL != ra);
   1240       GNUNET_assert (NULL != pa);
   1241       for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
   1242       {
   1243         struct TALER_Amount *tai = &po->total_amount.taa[i];
   1244         const struct TALER_Amount *r;
   1245 
   1246         GNUNET_assert (0 ==
   1247                        json_array_append_new (ta,
   1248                                               TALER_JSON_from_amount (tai)));
   1249         r = TALER_amount_set_find (tai->currency,
   1250                                    &po->total_refund_amount);
   1251         GNUNET_assert (0 ==
   1252                        json_array_append_new (ra,
   1253                                               TALER_JSON_from_amount (r)));
   1254         r = TALER_amount_set_find (tai->currency,
   1255                                    &po->total_pending_refund_amount);
   1256         GNUNET_assert (0 ==
   1257                        json_array_append_new (pa,
   1258                                               TALER_JSON_from_amount (r)));
   1259       }
   1260       root = GNUNET_JSON_PACK (
   1261         GNUNET_JSON_pack_string ("business_name",
   1262                                  mi->settings.name),
   1263         GNUNET_JSON_pack_array_incref ("orders",
   1264                                        po->pa),
   1265         GNUNET_JSON_pack_array_steal ("total_amounts",
   1266                                       ta),
   1267         GNUNET_JSON_pack_array_steal ("total_refund_amounts",
   1268                                       ra),
   1269         GNUNET_JSON_pack_array_steal ("total_pending_refund_amounts",
   1270                                       pa));
   1271       doc.form_name = "orders";
   1272       doc.form_version = "0.0.0";
   1273       doc.data = root;
   1274 
   1275       po->tc = TALER_MHD_typst (TALER_MERCHANT_project_data (),
   1276                                 TMH_cfg,
   1277                                 false,  /* remove on exit */
   1278                                 "merchant",
   1279                                 1,     /* one document */
   1280                                 &doc,
   1281                                 &pdf_cb,
   1282                                 po);
   1283       json_decref (root);
   1284       if (NULL == po->tc)
   1285       {
   1286         GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
   1287                     "Client requested PDF, but Typst is unavailable\n");
   1288         return TALER_MHD_reply_with_error (
   1289           connection,
   1290           MHD_HTTP_NOT_IMPLEMENTED,
   1291           TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK,
   1292           NULL);
   1293       }
   1294       GNUNET_CONTAINER_DLL_insert (pdf_head,
   1295                                    pdf_tail,
   1296                                    po);
   1297       MHD_suspend_connection (connection);
   1298       return MHD_YES;
   1299     }
   1300   } /* end switch */
   1301   GNUNET_assert (0);
   1302   return MHD_NO;
   1303 }
   1304 
   1305 
   1306 /**
   1307  * Handle a GET "/orders" request.
   1308  *
   1309  * @param rh context of the handler
   1310  * @param connection the MHD connection to handle
   1311  * @param[in,out] hc context with further information about the request
   1312  * @return MHD result code
   1313  */
   1314 enum MHD_Result
   1315 TMH_private_get_orders (const struct TMH_RequestHandler *rh,
   1316                         struct MHD_Connection *connection,
   1317                         struct TMH_HandlerContext *hc)
   1318 {
   1319   struct TMH_PendingOrder *po = hc->ctx;
   1320   struct TMH_MerchantInstance *mi = hc->instance;
   1321   enum GNUNET_DB_QueryStatus qs;
   1322 
   1323   if (NULL != po)
   1324   {
   1325     if (TALER_EC_NONE != po->result)
   1326     {
   1327       /* Resumed from long-polling with error */
   1328       GNUNET_break (0);
   1329       return TALER_MHD_reply_with_error (connection,
   1330                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
   1331                                          po->result,
   1332                                          NULL);
   1333     }
   1334     if (POF_PDF == po->format)
   1335     {
   1336       /* resumed from long-polling or from Typst PDF generation */
   1337       /* We really must have a response in this case */
   1338       if (NULL == po->response)
   1339       {
   1340         GNUNET_break (0);
   1341         return MHD_NO;
   1342       }
   1343       return MHD_queue_response (connection,
   1344                                  po->http_status,
   1345                                  po->response);
   1346     }
   1347     return reply_orders (po,
   1348                          connection,
   1349                          mi);
   1350   }
   1351   po = GNUNET_new (struct TMH_PendingOrder);
   1352   hc->ctx = po;
   1353   hc->cc = &cleanup;
   1354   po->con = connection;
   1355   po->pa = json_array ();
   1356   GNUNET_assert (NULL != po->pa);
   1357   po->instance_id = mi->settings.id;
   1358   po->mi = mi;
   1359 
   1360   /* Determine desired output format from Accept header */
   1361   {
   1362     const char *mime;
   1363 
   1364     mime = MHD_lookup_connection_value (connection,
   1365                                         MHD_HEADER_KIND,
   1366                                         MHD_HTTP_HEADER_ACCEPT);
   1367     if (NULL == mime)
   1368       mime = "application/json";
   1369     if (0 == strcmp (mime,
   1370                      "*/*"))
   1371       mime = "application/json";
   1372     if (0 == strcmp (mime,
   1373                      "application/json"))
   1374     {
   1375       po->format = POF_JSON;
   1376     }
   1377     else if (0 == strcmp (mime,
   1378                           "text/csv"))
   1379     {
   1380       po->format = POF_CSV;
   1381       GNUNET_buffer_write_str (&po->csv,
   1382                                CSV_HEADER);
   1383     }
   1384     else if (0 == strcmp (mime,
   1385                           "application/vnd.ms-excel"))
   1386     {
   1387       po->format = POF_XML;
   1388       GNUNET_buffer_write_str (&po->xml,
   1389                                XML_HEADER);
   1390     }
   1391     else if (0 == strcmp (mime,
   1392                           "application/pdf"))
   1393     {
   1394       po->format = POF_PDF;
   1395     }
   1396     else
   1397     {
   1398       GNUNET_break_op (0);
   1399       return TALER_MHD_REPLY_JSON_PACK (
   1400         connection,
   1401         MHD_HTTP_NOT_ACCEPTABLE,
   1402         GNUNET_JSON_pack_string ("hint",
   1403                                  mime));
   1404     }
   1405   }
   1406 
   1407   if (! (TALER_MHD_arg_to_yna (connection,
   1408                                "paid",
   1409                                TALER_EXCHANGE_YNA_ALL,
   1410                                &po->of.paid)) )
   1411   {
   1412     GNUNET_break_op (0);
   1413     return TALER_MHD_reply_with_error (connection,
   1414                                        MHD_HTTP_BAD_REQUEST,
   1415                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1416                                        "paid");
   1417   }
   1418   if (! (TALER_MHD_arg_to_yna (connection,
   1419                                "refunded",
   1420                                TALER_EXCHANGE_YNA_ALL,
   1421                                &po->of.refunded)) )
   1422   {
   1423     GNUNET_break_op (0);
   1424     return TALER_MHD_reply_with_error (connection,
   1425                                        MHD_HTTP_BAD_REQUEST,
   1426                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1427                                        "refunded");
   1428   }
   1429   if (! (TALER_MHD_arg_to_yna (connection,
   1430                                "wired",
   1431                                TALER_EXCHANGE_YNA_ALL,
   1432                                &po->of.wired)) )
   1433   {
   1434     GNUNET_break_op (0);
   1435     return TALER_MHD_reply_with_error (connection,
   1436                                        MHD_HTTP_BAD_REQUEST,
   1437                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1438                                        "wired");
   1439   }
   1440   po->of.delta = -20;
   1441   /* deprecated in protocol v12 */
   1442   TALER_MHD_parse_request_snumber (connection,
   1443                                    "delta",
   1444                                    &po->of.delta);
   1445   /* since protocol v12 */
   1446   TALER_MHD_parse_request_snumber (connection,
   1447                                    "limit",
   1448                                    &po->of.delta);
   1449   if ( (-MAX_DELTA > po->of.delta) ||
   1450        (po->of.delta > MAX_DELTA) )
   1451   {
   1452     GNUNET_break_op (0);
   1453     return TALER_MHD_reply_with_error (connection,
   1454                                        MHD_HTTP_BAD_REQUEST,
   1455                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1456                                        "limit");
   1457   }
   1458   {
   1459     const char *date_s_str;
   1460 
   1461     date_s_str = MHD_lookup_connection_value (connection,
   1462                                               MHD_GET_ARGUMENT_KIND,
   1463                                               "date_s");
   1464     if (NULL == date_s_str)
   1465     {
   1466       if (po->of.delta > 0)
   1467         po->of.date = GNUNET_TIME_UNIT_ZERO_TS;
   1468       else
   1469         po->of.date = GNUNET_TIME_UNIT_FOREVER_TS;
   1470     }
   1471     else
   1472     {
   1473       char dummy;
   1474       unsigned long long ll;
   1475 
   1476       if (1 !=
   1477           sscanf (date_s_str,
   1478                   "%llu%c",
   1479                   &ll,
   1480                   &dummy))
   1481       {
   1482         GNUNET_break_op (0);
   1483         return TALER_MHD_reply_with_error (connection,
   1484                                            MHD_HTTP_BAD_REQUEST,
   1485                                            TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1486                                            "date_s");
   1487       }
   1488 
   1489       po->of.date = GNUNET_TIME_absolute_to_timestamp (
   1490         GNUNET_TIME_absolute_from_s (ll));
   1491       if (GNUNET_TIME_absolute_is_never (po->of.date.abs_time))
   1492       {
   1493         GNUNET_break_op (0);
   1494         return TALER_MHD_reply_with_error (connection,
   1495                                            MHD_HTTP_BAD_REQUEST,
   1496                                            TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1497                                            "date_s");
   1498       }
   1499     }
   1500   }
   1501   if (po->of.delta > 0)
   1502   {
   1503     struct GNUNET_TIME_Relative duration
   1504       = GNUNET_TIME_UNIT_FOREVER_REL;
   1505     struct GNUNET_TIME_Absolute cut_off;
   1506 
   1507     TALER_MHD_parse_request_rel_time (connection,
   1508                                       "max_age",
   1509                                       &duration);
   1510     cut_off = GNUNET_TIME_absolute_subtract (GNUNET_TIME_absolute_get (),
   1511                                              duration);
   1512     po->of.date = GNUNET_TIME_timestamp_max (
   1513       po->of.date,
   1514       GNUNET_TIME_absolute_to_timestamp (cut_off));
   1515   }
   1516   if (po->of.delta > 0)
   1517     po->of.start_row = 0;
   1518   else
   1519     po->of.start_row = INT64_MAX;
   1520   /* deprecated in protocol v12 */
   1521   TALER_MHD_parse_request_number (connection,
   1522                                   "start",
   1523                                   &po->of.start_row);
   1524   /* since protocol v12 */
   1525   TALER_MHD_parse_request_number (connection,
   1526                                   "offset",
   1527                                   &po->of.start_row);
   1528   if (INT64_MAX < po->of.start_row)
   1529   {
   1530     GNUNET_break_op (0);
   1531     return TALER_MHD_reply_with_error (connection,
   1532                                        MHD_HTTP_BAD_REQUEST,
   1533                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1534                                        "offset");
   1535   }
   1536   po->summary_filter = tr (MHD_lookup_connection_value (connection,
   1537                                                         MHD_GET_ARGUMENT_KIND,
   1538                                                         "summary_filter"));
   1539   po->of.summary_filter = po->summary_filter; /* just an (read-only) alias! */
   1540   po->of.session_id
   1541     = MHD_lookup_connection_value (connection,
   1542                                    MHD_GET_ARGUMENT_KIND,
   1543                                    "session_id");
   1544   po->of.fulfillment_url
   1545     = MHD_lookup_connection_value (connection,
   1546                                    MHD_GET_ARGUMENT_KIND,
   1547                                    "fulfillment_url");
   1548   TALER_MHD_parse_request_timeout (connection,
   1549                                    &po->long_poll_timeout);
   1550   if (GNUNET_TIME_absolute_is_never (po->long_poll_timeout))
   1551   {
   1552     GNUNET_break_op (0);
   1553     return TALER_MHD_reply_with_error (connection,
   1554                                        MHD_HTTP_BAD_REQUEST,
   1555                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
   1556                                        "timeout_ms");
   1557   }
   1558   if ( (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) &&
   1559        (NULL == mi->po_eh) )
   1560   {
   1561     struct TMH_OrderChangeEventP change_eh = {
   1562       .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE),
   1563       .header.size = htons (sizeof (change_eh)),
   1564       .merchant_pub = mi->merchant_pub
   1565     };
   1566 
   1567     mi->po_eh = TALER_MERCHANTDB_event_listen (TMH_db,
   1568                                                &change_eh.header,
   1569                                                GNUNET_TIME_UNIT_FOREVER_REL,
   1570                                                &resume_by_event,
   1571                                                mi);
   1572   }
   1573 
   1574   po->of.timeout = GNUNET_TIME_absolute_get_remaining (po->long_poll_timeout);
   1575 
   1576   qs = TALER_MERCHANTDB_lookup_orders (TMH_db,
   1577                                        po->instance_id,
   1578                                        &po->of,
   1579                                        &add_order,
   1580                                        po);
   1581   if (0 > qs)
   1582   {
   1583     GNUNET_break (0);
   1584     po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
   1585   }
   1586   if (TALER_EC_NONE != po->result)
   1587   {
   1588     GNUNET_break (0);
   1589     return TALER_MHD_reply_with_error (connection,
   1590                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
   1591                                        po->result,
   1592                                        NULL);
   1593   }
   1594   if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) &&
   1595        (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) )
   1596   {
   1597     GNUNET_assert (NULL == po->order_timeout_task);
   1598     po->order_timeout_task
   1599       = GNUNET_SCHEDULER_add_at (po->long_poll_timeout,
   1600                                  &order_timeout,
   1601                                  po);
   1602     GNUNET_CONTAINER_DLL_insert (mi->po_head,
   1603                                  mi->po_tail,
   1604                                  po);
   1605     po->in_dll = true;
   1606     MHD_suspend_connection (connection);
   1607     return MHD_YES;
   1608   }
   1609   return reply_orders (po,
   1610                        connection,
   1611                        mi);
   1612 }
   1613 
   1614 
   1615 /* end of taler-merchant-httpd_get-private-orders.c */