merchant

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

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


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