merchant

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

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


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