Line data Source code
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 246 : TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi)
216 : {
217 : struct TMH_PendingOrder *po;
218 :
219 246 : while (NULL != (po = mi->po_head))
220 : {
221 0 : GNUNET_assert (po->in_dll);
222 0 : GNUNET_CONTAINER_DLL_remove (mi->po_head,
223 : mi->po_tail,
224 : po);
225 0 : MHD_resume_connection (po->con);
226 0 : po->in_dll = false;
227 : }
228 246 : if (NULL != mi->po_eh)
229 : {
230 0 : TALER_MERCHANTDB_event_listen_cancel (mi->po_eh);
231 0 : mi->po_eh = NULL;
232 : }
233 246 : }
234 :
235 :
236 : void
237 20 : TMH_force_get_orders_resume_typst ()
238 : {
239 : struct TMH_PendingOrder *po;
240 :
241 20 : while (NULL != (po = pdf_head))
242 : {
243 0 : GNUNET_CONTAINER_DLL_remove (pdf_head,
244 : pdf_tail,
245 : po);
246 0 : MHD_resume_connection (po->con);
247 : }
248 20 : }
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 0 : order_timeout (void *cls)
258 : {
259 0 : struct TMH_PendingOrder *po = cls;
260 0 : struct TMH_MerchantInstance *mi = po->mi;
261 :
262 0 : po->order_timeout_task = NULL;
263 0 : po->recheck = true;
264 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
265 : "Resuming long polled job due to timeout\n");
266 0 : GNUNET_assert (po->in_dll);
267 0 : GNUNET_CONTAINER_DLL_remove (mi->po_head,
268 : mi->po_tail,
269 : po);
270 0 : po->in_dll = false;
271 0 : MHD_resume_connection (po->con);
272 0 : TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
273 0 : }
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 24 : cleanup (void *ctx)
284 : {
285 24 : struct TMH_PendingOrder *po = ctx;
286 :
287 24 : if (po->in_dll)
288 : {
289 0 : struct TMH_MerchantInstance *mi = po->mi;
290 :
291 0 : GNUNET_CONTAINER_DLL_remove (mi->po_head,
292 : mi->po_tail,
293 : po);
294 0 : MHD_resume_connection (po->con);
295 : }
296 24 : if (NULL != po->order_timeout_task)
297 : {
298 2 : GNUNET_SCHEDULER_cancel (po->order_timeout_task);
299 2 : po->order_timeout_task = NULL;
300 : }
301 24 : json_decref (po->pa);
302 24 : TALER_amount_set_free (&po->total_amount);
303 24 : TALER_amount_set_free (&po->total_refund_amount);
304 24 : TALER_amount_set_free (&po->total_pending_refund_amount);
305 24 : GNUNET_free (po->summary_filter);
306 24 : switch (po->format)
307 : {
308 22 : case POF_JSON:
309 22 : break;
310 1 : case POF_CSV:
311 1 : GNUNET_buffer_clear (&po->csv);
312 1 : break;
313 1 : case POF_XML:
314 1 : GNUNET_buffer_clear (&po->xml);
315 1 : break;
316 0 : case POF_PDF:
317 0 : if (NULL != po->tc)
318 : {
319 0 : TALER_MHD_typst_cancel (po->tc);
320 0 : po->tc = NULL;
321 : }
322 0 : break;
323 : }
324 24 : if (NULL != po->response)
325 : {
326 0 : MHD_destroy_response (po->response);
327 0 : po->response = NULL;
328 : }
329 24 : GNUNET_free (po);
330 24 : }
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 0 : 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 0 : struct ProcessRefundsClosure *prc = cls;
381 :
382 0 : if (GNUNET_OK !=
383 0 : TALER_amount_cmp_currency (&prc->total_refund_amount,
384 : refund_amount))
385 : {
386 : /* Database error, refunds in mixed currency in DB. Not OK! */
387 0 : prc->ec = TALER_EC_GENERIC_DB_INVARIANT_FAILURE;
388 0 : GNUNET_break (0);
389 0 : return;
390 : }
391 0 : GNUNET_assert (0 <=
392 : TALER_amount_add (&prc->total_refund_amount,
393 : &prc->total_refund_amount,
394 : refund_amount));
395 0 : if (pending)
396 0 : 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 12 : accumulate_total (struct TMH_PendingOrder *po,
412 : const struct TALER_Amount *amount)
413 : {
414 12 : if (0 > TALER_amount_set_add (&po->total_amount,
415 : amount,
416 : NULL))
417 : {
418 0 : GNUNET_break (0);
419 0 : po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
420 : }
421 12 : }
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 12 : accumulate_refund_totals (struct TMH_PendingOrder *po,
435 : const struct TALER_Amount *refund,
436 : const struct TALER_Amount *pending)
437 : {
438 12 : if (TALER_EC_NONE != po->result)
439 0 : return;
440 12 : if (0 > TALER_amount_set_add (&po->total_refund_amount,
441 : refund,
442 : NULL))
443 : {
444 0 : GNUNET_break (0);
445 0 : po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT;
446 0 : return;
447 : }
448 12 : if (0 > TALER_amount_set_add (&po->total_pending_refund_amount,
449 : pending,
450 : NULL))
451 : {
452 0 : GNUNET_break (0);
453 0 : 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 26 : add_order (void *cls,
468 : const char *orig_order_id,
469 : uint64_t order_serial,
470 : struct GNUNET_TIME_Timestamp creation_time)
471 : {
472 26 : struct TMH_PendingOrder *po = cls;
473 26 : json_t *terms = NULL;
474 : struct TALER_PrivateContractHashP h_contract_terms;
475 : enum GNUNET_DB_QueryStatus qs;
476 26 : char *order_id = NULL;
477 26 : bool refundable = false;
478 : bool paid;
479 : bool wired;
480 26 : struct TALER_MERCHANT_Contract *contract = NULL;
481 26 : struct TALER_MERCHANT_Order *order = NULL;
482 26 : int16_t choice_index = -1;
483 26 : 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 26 : const struct TALER_MERCHANT_ContractBaseTerms *ct = NULL;
491 :
492 : /* Bail early if we already have an error */
493 26 : if (TALER_EC_NONE != po->result)
494 2 : return;
495 :
496 26 : 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 26 : qs = TALER_MERCHANTDB_set_instance (
502 : TMH_db,
503 : po->instance_id);
504 26 : if (qs <= 0)
505 : {
506 0 : GNUNET_break (0);
507 0 : po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
508 0 : return;
509 : }
510 26 : 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 26 : if (qs < 0)
517 : {
518 0 : GNUNET_break (0);
519 0 : po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
520 0 : goto cleanup;
521 : }
522 26 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
523 : {
524 : /* Contract terms don't exist, so the order cannot be paid. */
525 7 : paid = false;
526 7 : 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 2 : GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
531 : TALER_MERCHANTDB_set_instance (
532 : TMH_db,
533 : NULL));
534 2 : return;
535 : }
536 5 : 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 24 : 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 24 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
556 : {
557 19 : GNUNET_break (os == order_serial);
558 19 : contract = TALER_MERCHANT_contract_parse (terms);
559 19 : if (NULL == contract)
560 : {
561 0 : GNUNET_break (0);
562 0 : po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID;
563 0 : goto cleanup;
564 : }
565 19 : ct = contract->pc->base;
566 : }
567 : }
568 24 : 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 5 : paid = false;
574 5 : wired = false;
575 5 : qs = TALER_MERCHANTDB_get_order (TMH_db,
576 : po->instance_id,
577 : order_id,
578 : NULL,
579 : &unused,
580 : &terms);
581 5 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
582 : {
583 5 : order = TALER_MERCHANT_order_parse (terms);
584 5 : if (NULL == order)
585 : {
586 0 : GNUNET_break (0);
587 0 : po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID;
588 0 : goto cleanup;
589 : }
590 5 : ct = order->base;
591 : }
592 : }
593 24 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
594 : {
595 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
596 : "Order %llu disappeared during iteration. Skipping.\n",
597 : (unsigned long long) order_serial);
598 0 : goto cleanup;
599 : }
600 24 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
601 : {
602 0 : GNUNET_break (0);
603 0 : po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
604 0 : goto cleanup;
605 : }
606 :
607 :
608 24 : if (paid)
609 : {
610 : const struct TALER_Amount *brutto;
611 :
612 12 : GNUNET_assert (NULL != contract);
613 12 : switch (ct->version)
614 : {
615 12 : case TALER_MERCHANT_CONTRACT_VERSION_0:
616 12 : brutto = &contract->pc->details.v0.brutto;
617 12 : break;
618 0 : case TALER_MERCHANT_CONTRACT_VERSION_1:
619 : {
620 : struct TALER_MERCHANT_ContractChoice *choice;
621 :
622 0 : if (-1 == choice_index)
623 0 : choice_index = 0; /* default choice */
624 0 : GNUNET_assert (choice_index < contract->pc->details.v1.choices_len);
625 0 : choice = &contract->pc->details.v1.choices[choice_index];
626 0 : brutto = &choice->amount;
627 : }
628 0 : break;
629 0 : default:
630 0 : GNUNET_break (0);
631 0 : goto cleanup;
632 : }
633 12 : GNUNET_assert (GNUNET_OK ==
634 : TALER_amount_set_zero (brutto->currency,
635 : &prc.total_refund_amount));
636 12 : GNUNET_assert (GNUNET_OK ==
637 : TALER_amount_set_zero (brutto->currency,
638 : &prc.pending_refund_amount));
639 :
640 12 : qs = TALER_MERCHANTDB_iterate_refunds_detailed (TMH_db,
641 : po->instance_id,
642 : &h_contract_terms,
643 : &process_refunds_cb,
644 : &prc);
645 12 : if (0 > qs)
646 : {
647 0 : GNUNET_break (0);
648 0 : po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
649 0 : goto cleanup;
650 : }
651 12 : if (TALER_EC_NONE != prc.ec)
652 : {
653 0 : GNUNET_break (0);
654 0 : po->result = prc.ec;
655 0 : goto cleanup;
656 : }
657 12 : if (0 > TALER_amount_cmp (&prc.total_refund_amount,
658 12 : brutto) &&
659 12 : GNUNET_TIME_absolute_is_future (
660 12 : contract->pc->refund_deadline.abs_time))
661 3 : refundable = true;
662 : }
663 :
664 : /* compute amount totals */
665 24 : amount = NULL;
666 24 : switch (ct->version)
667 : {
668 24 : case TALER_MERCHANT_CONTRACT_VERSION_0:
669 : {
670 24 : amount = (NULL != contract)
671 19 : ? &contract->pc->details.v0.brutto
672 : : &order->details.v0.brutto;
673 :
674 24 : if (TALER_amount_is_zero (amount) &&
675 0 : (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 0 : goto cleanup;
682 : }
683 :
684 : /* Accumulate order total */
685 24 : if (paid)
686 12 : accumulate_total (po,
687 : amount);
688 24 : if (TALER_EC_NONE != po->result)
689 0 : goto cleanup;
690 : /* Accumulate refund totals (only meaningful for paid orders) */
691 24 : if (paid)
692 : {
693 12 : accumulate_refund_totals (po,
694 : &prc.total_refund_amount,
695 : &prc.pending_refund_amount);
696 12 : if (TALER_EC_NONE != po->result)
697 0 : goto cleanup;
698 : }
699 : }
700 24 : break;
701 0 : case TALER_MERCHANT_CONTRACT_VERSION_1:
702 0 : if (-1 == choice_index)
703 0 : choice_index = 0; /* default choice */
704 0 : if (NULL != contract)
705 : {
706 0 : struct TALER_MERCHANT_ContractChoice *choice
707 0 : = &contract->pc->details.v1.choices[choice_index];
708 :
709 0 : GNUNET_assert (choice_index < contract->pc->details.v1.choices_len);
710 0 : amount = &choice->amount;
711 : /* Accumulate order total */
712 0 : if (paid)
713 0 : accumulate_total (po,
714 : amount);
715 0 : if (TALER_EC_NONE != po->result)
716 0 : goto cleanup;
717 : /* Accumulate refund totals (only meaningful for paid orders) */
718 0 : if (paid)
719 : {
720 0 : accumulate_refund_totals (po,
721 : &prc.total_refund_amount,
722 : &prc.pending_refund_amount);
723 0 : if (TALER_EC_NONE != po->result)
724 0 : goto cleanup;
725 : }
726 : }
727 : else
728 : {
729 0 : GNUNET_assert (choice_index < order->details.v1.choices_len);
730 0 : amount = &order->details.v1.choices[choice_index].amount;
731 0 : if (TALER_EC_NONE != po->result)
732 0 : goto cleanup;
733 : }
734 0 : break;
735 0 : default:
736 0 : GNUNET_break (0);
737 0 : po->result = TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION;
738 0 : 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 24 : strcpy (amount_buf,
745 : TALER_amount2s (amount));
746 24 : if (paid)
747 12 : strcpy (refund_buf,
748 : TALER_amount2s (&prc.total_refund_amount));
749 24 : if (paid)
750 12 : strcpy (pending_buf,
751 : TALER_amount2s (&prc.pending_refund_amount));
752 :
753 24 : switch (po->format)
754 : {
755 18 : case POF_JSON:
756 : case POF_PDF:
757 18 : 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 18 : break;
796 3 : case POF_CSV:
797 : {
798 3 : size_t len = strlen (ct->summary);
799 3 : 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 3 : esummary = GNUNET_malloc (2 * len + 1);
806 18 : for (size_t off = 0; off<len; off++)
807 : {
808 15 : if ('"' == ct->summary[off])
809 0 : esummary[wpos++] = '"';
810 15 : esummary[wpos++] = ct->summary[off];
811 : }
812 3 : t = GNUNET_TIME_timestamp_to_s (creation_time);
813 3 : tm = localtime (&t);
814 9 : 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 3 : tm->tm_year + 1900,
820 3 : 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 3 : paid ? refund_buf : "",
828 3 : paid ? pending_buf : "",
829 : esummary,
830 : refundable ? "yes" : "no",
831 3 : paid ? "yes" : "no");
832 3 : GNUNET_free (esummary);
833 3 : break;
834 : }
835 3 : case POF_XML:
836 : {
837 3 : char *esummary = TALER_escape_xml (ct->summary);
838 : char creation_time_s[128];
839 : const struct tm *tm;
840 : time_t tt;
841 :
842 3 : tt = (time_t) GNUNET_TIME_timestamp_to_s (creation_time);
843 3 : tm = gmtime (&tt);
844 3 : strftime (creation_time_s,
845 : sizeof (creation_time_s),
846 : "%Y-%m-%dT%H:%M:%S",
847 : tm);
848 9 : 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 3 : paid ? refund_buf : "",
862 : NULL != esummary ? esummary : "",
863 3 : paid ? "TRUE" : "FALSE",
864 3 : paid ? "1" : "0");
865 3 : GNUNET_free (esummary);
866 : }
867 3 : break;
868 : } /* end switch po->format */
869 :
870 24 : cleanup:
871 24 : GNUNET_break (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
872 : TALER_MERCHANTDB_set_instance (
873 : TMH_db,
874 : NULL));
875 24 : json_decref (terms);
876 24 : GNUNET_free (order_id);
877 24 : if (NULL != order)
878 : {
879 5 : TALER_MERCHANT_order_free (order);
880 5 : order = NULL;
881 : }
882 24 : if (NULL != contract)
883 : {
884 19 : TALER_MERCHANT_contract_free (contract);
885 19 : 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 2 : resume_by_event (void *cls,
900 : const void *extra,
901 : size_t extra_size)
902 : {
903 2 : struct TMH_MerchantInstance *mi = cls;
904 2 : 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 2 : if (sizeof (*oce) != extra_size)
911 : {
912 0 : GNUNET_break (0);
913 0 : return;
914 : }
915 2 : osf = (enum TMH_OrderStateFlags) (int) ntohl (oce->order_state);
916 2 : order_serial_id = GNUNET_ntohll (oce->order_serial_id);
917 2 : date = GNUNET_TIME_timestamp_ntoh (oce->execution_date);
918 2 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
919 : "Received notification about order %llu\n",
920 : (unsigned long long) order_serial_id);
921 2 : for (struct TMH_PendingOrder *po = mi->po_head;
922 4 : NULL != po;
923 2 : po = pn)
924 : {
925 2 : pn = po->next;
926 4 : if (! ( ( ((TALER_EXCHANGE_YNA_YES == po->of.paid) ==
927 2 : (0 != (osf & TMH_OSF_PAID))) ||
928 0 : (TALER_EXCHANGE_YNA_ALL == po->of.paid) ) &&
929 2 : ( ((TALER_EXCHANGE_YNA_YES == po->of.refunded) ==
930 2 : (0 != (osf & TMH_OSF_REFUNDED))) ||
931 0 : (TALER_EXCHANGE_YNA_ALL == po->of.refunded) ) &&
932 2 : ( ((TALER_EXCHANGE_YNA_YES == po->of.wired) ==
933 2 : (0 != (osf & TMH_OSF_WIRED))) ||
934 0 : (TALER_EXCHANGE_YNA_ALL == po->of.wired) ) ) )
935 : {
936 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
937 : "Client %p waits on different order type\n",
938 : po);
939 0 : continue;
940 : }
941 2 : if (po->of.delta > 0)
942 : {
943 2 : if (order_serial_id < po->of.start_row)
944 : {
945 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
946 : "Client %p waits on different order row\n",
947 : po);
948 0 : continue;
949 : }
950 2 : if (GNUNET_TIME_timestamp_cmp (date,
951 : <,
952 : po->of.date))
953 : {
954 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
955 : "Client %p waits on different order date\n",
956 : po);
957 0 : continue;
958 : }
959 2 : if (TALER_EXCHANGE_YNA_ALL == po->of.expired)
960 2 : po->of.delta--;
961 : }
962 : else
963 : {
964 0 : if (order_serial_id > po->of.start_row)
965 : {
966 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
967 : "Client %p waits on different order row\n",
968 : po);
969 0 : continue;
970 : }
971 0 : if (GNUNET_TIME_timestamp_cmp (date,
972 : >,
973 : po->of.date))
974 : {
975 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
976 : "Client %p waits on different order date\n",
977 : po);
978 0 : continue;
979 : }
980 0 : if (TALER_EXCHANGE_YNA_ALL == po->of.expired)
981 0 : po->of.delta++;
982 : }
983 2 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
984 : "Waking up client %p!\n",
985 : po);
986 2 : 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 0 : po->recheck = true;
991 : }
992 : else
993 : {
994 2 : add_order (po,
995 : NULL,
996 : order_serial_id,
997 : date);
998 : }
999 2 : GNUNET_assert (po->in_dll);
1000 2 : GNUNET_CONTAINER_DLL_remove (mi->po_head,
1001 : mi->po_tail,
1002 : po);
1003 2 : po->in_dll = false;
1004 2 : MHD_resume_connection (po->con);
1005 2 : TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
1006 : }
1007 2 : if (NULL == mi->po_head)
1008 : {
1009 2 : TALER_MERCHANTDB_event_listen_cancel (mi->po_eh);
1010 2 : 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 242 : 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 484 : struct TMH_OrderChangeEventDetailsP oce = {
1031 242 : .order_serial_id = GNUNET_htonll (order_serial_id),
1032 242 : .execution_date = GNUNET_TIME_timestamp_hton (date),
1033 242 : .order_state = htonl (osf)
1034 : };
1035 242 : struct TMH_OrderChangeEventP eh = {
1036 242 : .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE),
1037 242 : .header.size = htons (sizeof (eh)),
1038 : .merchant_pub = mi->merchant_pub
1039 : };
1040 :
1041 242 : 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 242 : TALER_MERCHANTDB_event_notify (TMH_db,
1046 : &eh.header,
1047 : &oce,
1048 : sizeof (oce));
1049 242 : }
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 24 : tr (const char *input)
1062 : {
1063 : char *out;
1064 : size_t slen;
1065 : size_t wpos;
1066 :
1067 24 : if (NULL == input)
1068 24 : return NULL;
1069 0 : slen = strlen (input);
1070 0 : out = GNUNET_malloc (slen * 2 + 3);
1071 0 : wpos = 0;
1072 0 : out[wpos++] = '%';
1073 0 : for (size_t i = 0; i<slen; i++)
1074 : {
1075 0 : char c = input[i];
1076 :
1077 0 : if ( (c == '%') ||
1078 : (c == '_') )
1079 0 : out[wpos++] = '\\';
1080 0 : out[wpos++] = c;
1081 : }
1082 0 : out[wpos++] = '%';
1083 0 : GNUNET_assert (wpos < slen * 2 + 3);
1084 0 : 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 0 : pdf_cb (void *cls,
1096 : const struct TALER_MHD_TypstResponse *tr)
1097 : {
1098 0 : struct TMH_PendingOrder *po = cls;
1099 :
1100 0 : po->tc = NULL;
1101 0 : GNUNET_CONTAINER_DLL_remove (pdf_head,
1102 : pdf_tail,
1103 : po);
1104 0 : if (TALER_EC_NONE != tr->ec)
1105 : {
1106 : po->http_status
1107 0 : = TALER_ErrorCode_get_http_status (tr->ec);
1108 : po->response
1109 0 : = TALER_MHD_make_error (tr->ec,
1110 0 : tr->details.hint);
1111 : }
1112 : else
1113 : {
1114 0 : po->http_status = MHD_HTTP_OK;
1115 0 : po->response = TALER_MHD_response_from_pdf_file (tr->details.filename);
1116 : }
1117 0 : MHD_resume_connection (po->con);
1118 0 : TALER_MHD_daemon_trigger ();
1119 0 : }
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 24 : 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 24 : switch (po->format)
1145 : {
1146 22 : case POF_JSON:
1147 22 : return TALER_MHD_REPLY_JSON_PACK (
1148 : connection,
1149 : MHD_HTTP_OK,
1150 : GNUNET_JSON_pack_array_incref ("orders",
1151 : po->pa));
1152 1 : case POF_CSV:
1153 : {
1154 : struct MHD_Response *resp;
1155 : enum MHD_Result mret;
1156 :
1157 2 : for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
1158 : {
1159 1 : struct TALER_Amount *tai = &po->total_amount.taa[i];
1160 : const struct TALER_Amount *r;
1161 :
1162 1 : strcpy (total_buf,
1163 : TALER_amount2s (tai));
1164 1 : r = TALER_amount_set_find (tai->currency,
1165 1 : &po->total_refund_amount);
1166 1 : strcpy (refund_buf,
1167 : TALER_amount2s (r));
1168 1 : r = TALER_amount_set_find (tai->currency,
1169 1 : &po->total_pending_refund_amount);
1170 1 : strcpy (pending_buf,
1171 : TALER_amount2s (r));
1172 :
1173 1 : GNUNET_buffer_write_fstr (
1174 : &po->csv,
1175 : "Total (paid %s only),,,,%s,%s,%s,,,\r\n",
1176 1 : tai->currency,
1177 : total_buf,
1178 : refund_buf,
1179 : pending_buf);
1180 : }
1181 1 : GNUNET_buffer_write_str (&po->csv,
1182 : CSV_FOOTER);
1183 1 : resp = MHD_create_response_from_buffer (po->csv.position,
1184 1 : po->csv.mem,
1185 : MHD_RESPMEM_MUST_COPY);
1186 1 : TALER_MHD_add_global_headers (resp,
1187 : false);
1188 1 : GNUNET_break (MHD_YES ==
1189 : MHD_add_response_header (resp,
1190 : MHD_HTTP_HEADER_CONTENT_TYPE,
1191 : "text/csv"));
1192 1 : mret = MHD_queue_response (connection,
1193 : MHD_HTTP_OK,
1194 : resp);
1195 1 : MHD_destroy_response (resp);
1196 1 : return mret;
1197 : }
1198 1 : case POF_XML:
1199 : {
1200 : struct MHD_Response *resp;
1201 : enum MHD_Result mret;
1202 :
1203 2 : for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
1204 : {
1205 1 : struct TALER_Amount *tai = &po->total_amount.taa[i];
1206 : const struct TALER_Amount *r;
1207 :
1208 1 : strcpy (total_buf,
1209 : TALER_amount2s (tai));
1210 1 : r = TALER_amount_set_find (tai->currency,
1211 1 : &po->total_refund_amount);
1212 1 : strcpy (refund_buf,
1213 : TALER_amount2s (r));
1214 1 : r = TALER_amount_set_find (tai->currency,
1215 1 : &po->total_pending_refund_amount);
1216 1 : strcpy (pending_buf,
1217 : TALER_amount2s (r));
1218 :
1219 : /* Append totals row with paid and refunded amount columns */
1220 1 : 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 1 : tai->currency,
1231 : total_buf,
1232 : refund_buf);
1233 : }
1234 1 : GNUNET_buffer_write_str (&po->xml,
1235 : XML_FOOTER);
1236 1 : resp = MHD_create_response_from_buffer (po->xml.position,
1237 1 : po->xml.mem,
1238 : MHD_RESPMEM_MUST_COPY);
1239 1 : TALER_MHD_add_global_headers (resp,
1240 : false);
1241 1 : GNUNET_break (MHD_YES ==
1242 : MHD_add_response_header (resp,
1243 : MHD_HTTP_HEADER_CONTENT_TYPE,
1244 : "application/vnd.ms-excel"));
1245 1 : mret = MHD_queue_response (connection,
1246 : MHD_HTTP_OK,
1247 : resp);
1248 1 : MHD_destroy_response (resp);
1249 1 : return mret;
1250 : }
1251 0 : 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 0 : json_t *ta = json_array ();
1257 0 : json_t *ra = json_array ();
1258 0 : json_t *pa = json_array ();
1259 :
1260 0 : GNUNET_assert (NULL != ta);
1261 0 : GNUNET_assert (NULL != ra);
1262 0 : GNUNET_assert (NULL != pa);
1263 0 : for (unsigned int i = 0; i<po->total_amount.taa_size; i++)
1264 : {
1265 0 : struct TALER_Amount *tai = &po->total_amount.taa[i];
1266 : const struct TALER_Amount *r;
1267 :
1268 0 : GNUNET_assert (0 ==
1269 : json_array_append_new (ta,
1270 : TALER_JSON_from_amount (tai)));
1271 0 : r = TALER_amount_set_find (tai->currency,
1272 0 : &po->total_refund_amount);
1273 0 : GNUNET_assert (0 ==
1274 : json_array_append_new (ra,
1275 : TALER_JSON_from_amount (r)));
1276 0 : r = TALER_amount_set_find (tai->currency,
1277 0 : &po->total_pending_refund_amount);
1278 0 : GNUNET_assert (0 ==
1279 : json_array_append_new (pa,
1280 : TALER_JSON_from_amount (r)));
1281 : }
1282 0 : 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 0 : doc.form_name = "orders";
1294 0 : doc.form_version = "0.0.0";
1295 0 : doc.data = root;
1296 :
1297 0 : 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 0 : json_decref (root);
1306 0 : if (NULL == po->tc)
1307 : {
1308 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
1309 : "Client requested PDF, but Typst is unavailable\n");
1310 0 : 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 0 : GNUNET_CONTAINER_DLL_insert (pdf_head,
1317 : pdf_tail,
1318 : po);
1319 0 : MHD_suspend_connection (connection);
1320 0 : return MHD_YES;
1321 : }
1322 : } /* end switch */
1323 0 : 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 26 : TMH_private_get_orders (const struct TMH_RequestHandler *rh,
1338 : struct MHD_Connection *connection,
1339 : struct TMH_HandlerContext *hc)
1340 : {
1341 26 : struct TMH_PendingOrder *po = hc->ctx;
1342 26 : struct TMH_MerchantInstance *mi = hc->instance;
1343 : enum GNUNET_DB_QueryStatus qs;
1344 :
1345 26 : if (NULL != po)
1346 : {
1347 2 : if (po->recheck)
1348 : {
1349 0 : if (NULL != po->order_timeout_task)
1350 : {
1351 0 : GNUNET_SCHEDULER_cancel (po->order_timeout_task);
1352 0 : po->order_timeout_task = NULL;
1353 : }
1354 0 : po->recheck = false;
1355 0 : goto run_query;
1356 : }
1357 2 : if (TALER_EC_NONE != po->result)
1358 : {
1359 : /* Resumed from long-polling with error */
1360 0 : GNUNET_break (0);
1361 0 : return TALER_MHD_reply_with_error (connection,
1362 : MHD_HTTP_INTERNAL_SERVER_ERROR,
1363 : po->result,
1364 : NULL);
1365 : }
1366 2 : 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 0 : if (NULL == po->response)
1371 : {
1372 0 : GNUNET_break (0);
1373 0 : return MHD_NO;
1374 : }
1375 0 : return MHD_queue_response (connection,
1376 : po->http_status,
1377 : po->response);
1378 : }
1379 2 : return reply_orders (po,
1380 : connection,
1381 : mi);
1382 : }
1383 24 : po = GNUNET_new (struct TMH_PendingOrder);
1384 24 : hc->ctx = po;
1385 24 : hc->cc = &cleanup;
1386 24 : po->con = connection;
1387 24 : po->pa = json_array ();
1388 24 : GNUNET_assert (NULL != po->pa);
1389 24 : po->instance_id = mi->settings.id;
1390 24 : po->mi = mi;
1391 :
1392 : /* Determine desired output format from Accept header */
1393 : {
1394 : const char *mime;
1395 :
1396 24 : mime = MHD_lookup_connection_value (connection,
1397 : MHD_HEADER_KIND,
1398 : MHD_HTTP_HEADER_ACCEPT);
1399 24 : if (NULL == mime)
1400 0 : mime = "application/json";
1401 24 : if (0 == strcmp (mime,
1402 : "*/*"))
1403 22 : mime = "application/json";
1404 24 : if (0 == strcmp (mime,
1405 : "application/json"))
1406 : {
1407 22 : po->format = POF_JSON;
1408 : }
1409 2 : else if (0 == strcmp (mime,
1410 : "text/csv"))
1411 : {
1412 1 : po->format = POF_CSV;
1413 1 : GNUNET_buffer_write_str (&po->csv,
1414 : CSV_HEADER);
1415 : }
1416 1 : else if (0 == strcmp (mime,
1417 : "application/vnd.ms-excel"))
1418 : {
1419 1 : po->format = POF_XML;
1420 1 : GNUNET_buffer_write_str (&po->xml,
1421 : XML_HEADER);
1422 : }
1423 0 : else if (0 == strcmp (mime,
1424 : "application/pdf"))
1425 : {
1426 0 : po->format = POF_PDF;
1427 : }
1428 : else
1429 : {
1430 0 : GNUNET_break_op (0);
1431 0 : 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 24 : if (! (TALER_MHD_arg_to_yna (connection,
1440 : "paid",
1441 : TALER_EXCHANGE_YNA_ALL,
1442 : &po->of.paid)) )
1443 : {
1444 0 : GNUNET_break_op (0);
1445 0 : return TALER_MHD_reply_with_error (connection,
1446 : MHD_HTTP_BAD_REQUEST,
1447 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1448 : "paid");
1449 : }
1450 24 : if (! (TALER_MHD_arg_to_yna (connection,
1451 : "refunded",
1452 : TALER_EXCHANGE_YNA_ALL,
1453 : &po->of.refunded)) )
1454 : {
1455 0 : GNUNET_break_op (0);
1456 0 : return TALER_MHD_reply_with_error (connection,
1457 : MHD_HTTP_BAD_REQUEST,
1458 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1459 : "refunded");
1460 : }
1461 24 : if (! (TALER_MHD_arg_to_yna (connection,
1462 : "wired",
1463 : TALER_EXCHANGE_YNA_ALL,
1464 : &po->of.wired)) )
1465 : {
1466 0 : GNUNET_break_op (0);
1467 0 : return TALER_MHD_reply_with_error (connection,
1468 : MHD_HTTP_BAD_REQUEST,
1469 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1470 : "wired");
1471 : }
1472 24 : if (! (TALER_MHD_arg_to_yna (connection,
1473 : "expired",
1474 : TALER_EXCHANGE_YNA_ALL,
1475 : &po->of.expired)) )
1476 : {
1477 0 : GNUNET_break_op (0);
1478 0 : return TALER_MHD_reply_with_error (connection,
1479 : MHD_HTTP_BAD_REQUEST,
1480 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1481 : "expired");
1482 : }
1483 24 : po->of.delta = -20;
1484 : /* deprecated in protocol v12 */
1485 24 : TALER_MHD_parse_request_snumber (connection,
1486 : "delta",
1487 : &po->of.delta);
1488 : /* since protocol v12 */
1489 24 : TALER_MHD_parse_request_snumber (connection,
1490 : "limit",
1491 : &po->of.delta);
1492 24 : if ( (-MAX_DELTA > po->of.delta) ||
1493 24 : (po->of.delta > MAX_DELTA) )
1494 : {
1495 0 : GNUNET_break_op (0);
1496 0 : 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 24 : date_s_str = MHD_lookup_connection_value (connection,
1505 : MHD_GET_ARGUMENT_KIND,
1506 : "date_s");
1507 24 : if (NULL == date_s_str)
1508 : {
1509 24 : if (po->of.delta > 0)
1510 10 : po->of.date = GNUNET_TIME_UNIT_ZERO_TS;
1511 : else
1512 14 : po->of.date = GNUNET_TIME_UNIT_FOREVER_TS;
1513 : }
1514 : else
1515 : {
1516 : char dummy;
1517 : unsigned long long ll;
1518 :
1519 0 : if (1 !=
1520 0 : sscanf (date_s_str,
1521 : "%llu%c",
1522 : &ll,
1523 : &dummy))
1524 : {
1525 0 : GNUNET_break_op (0);
1526 0 : return TALER_MHD_reply_with_error (connection,
1527 : MHD_HTTP_BAD_REQUEST,
1528 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1529 : "date_s");
1530 : }
1531 :
1532 0 : po->of.date = GNUNET_TIME_absolute_to_timestamp (
1533 : GNUNET_TIME_absolute_from_s (ll));
1534 0 : if (GNUNET_TIME_absolute_is_never (po->of.date.abs_time))
1535 : {
1536 0 : GNUNET_break_op (0);
1537 0 : 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 24 : 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 10 : bool have_max_age
1550 : = (NULL !=
1551 10 : MHD_lookup_connection_value (connection,
1552 : MHD_GET_ARGUMENT_KIND,
1553 : "max_age"));
1554 10 : bool have_max_age_s
1555 : = (NULL !=
1556 10 : MHD_lookup_connection_value (connection,
1557 : MHD_GET_ARGUMENT_KIND,
1558 : "max_age_s"));
1559 10 : struct GNUNET_TIME_Relative duration = GNUNET_TIME_UNIT_ZERO;
1560 :
1561 10 : if (have_max_age)
1562 : {
1563 : /* deprecated in protocol v33, superseded by "max_age_s" */
1564 4 : TALER_MHD_parse_request_rel_time (connection,
1565 : "max_age",
1566 : &duration);
1567 : }
1568 10 : if (have_max_age_s)
1569 : {
1570 : /* since protocol v33 */
1571 4 : uint64_t max_age_s = 0;
1572 : struct GNUNET_TIME_Relative age;
1573 :
1574 4 : TALER_MHD_parse_request_number (connection,
1575 : "max_age_s",
1576 : &max_age_s);
1577 4 : age = GNUNET_TIME_relative_saturating_multiply (GNUNET_TIME_UNIT_SECONDS,
1578 : max_age_s);
1579 4 : if (have_max_age &&
1580 2 : GNUNET_TIME_relative_cmp (duration,
1581 : !=,
1582 : age))
1583 : {
1584 2 : unsigned long long max_age_ms
1585 2 : = duration.rel_value_us
1586 2 : / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us;
1587 :
1588 2 : 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 4 : duration = age;
1594 : }
1595 10 : if (have_max_age || have_max_age_s)
1596 : {
1597 : struct GNUNET_TIME_Absolute cut_off;
1598 :
1599 6 : cut_off = GNUNET_TIME_absolute_subtract (GNUNET_TIME_absolute_get (),
1600 : duration);
1601 6 : po->of.date = GNUNET_TIME_timestamp_max (
1602 : po->of.date,
1603 : GNUNET_TIME_absolute_to_timestamp (cut_off));
1604 : }
1605 : }
1606 24 : if (po->of.delta > 0)
1607 10 : po->of.start_row = 0;
1608 : else
1609 14 : po->of.start_row = INT64_MAX;
1610 : /* deprecated in protocol v12 */
1611 24 : TALER_MHD_parse_request_number (connection,
1612 : "start",
1613 : &po->of.start_row);
1614 : /* since protocol v12 */
1615 24 : TALER_MHD_parse_request_number (connection,
1616 : "offset",
1617 : &po->of.start_row);
1618 24 : if (INT64_MAX < po->of.start_row)
1619 : {
1620 0 : GNUNET_break_op (0);
1621 0 : return TALER_MHD_reply_with_error (connection,
1622 : MHD_HTTP_BAD_REQUEST,
1623 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1624 : "offset");
1625 : }
1626 24 : po->summary_filter = tr (MHD_lookup_connection_value (connection,
1627 : MHD_GET_ARGUMENT_KIND,
1628 : "summary_filter"));
1629 24 : po->of.summary_filter = po->summary_filter; /* just an (read-only) alias! */
1630 : po->of.session_id
1631 24 : = MHD_lookup_connection_value (connection,
1632 : MHD_GET_ARGUMENT_KIND,
1633 : "session_id");
1634 : po->of.fulfillment_url
1635 24 : = MHD_lookup_connection_value (connection,
1636 : MHD_GET_ARGUMENT_KIND,
1637 : "fulfillment_url");
1638 24 : TALER_MHD_parse_request_timeout (connection,
1639 : &po->long_poll_timeout);
1640 24 : if (GNUNET_TIME_absolute_is_never (po->long_poll_timeout))
1641 : {
1642 0 : GNUNET_break_op (0);
1643 0 : return TALER_MHD_reply_with_error (connection,
1644 : MHD_HTTP_BAD_REQUEST,
1645 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
1646 : "timeout_ms");
1647 : }
1648 24 : if ( (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) &&
1649 2 : (NULL == mi->po_eh) )
1650 : {
1651 2 : struct TMH_OrderChangeEventP change_eh = {
1652 2 : .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE),
1653 2 : .header.size = htons (sizeof (change_eh)),
1654 : .merchant_pub = mi->merchant_pub
1655 : };
1656 :
1657 2 : mi->po_eh = TALER_MERCHANTDB_event_listen (TMH_db,
1658 : &change_eh.header,
1659 2 : GNUNET_TIME_UNIT_FOREVER_REL,
1660 : &resume_by_event,
1661 : mi);
1662 : }
1663 :
1664 22 : run_query:
1665 24 : po->of.timeout = GNUNET_TIME_absolute_get_remaining (po->long_poll_timeout);
1666 :
1667 24 : qs = TALER_MERCHANTDB_iterate_orders (TMH_db,
1668 : po->instance_id,
1669 24 : &po->of,
1670 : &add_order,
1671 : po);
1672 24 : if (0 > qs)
1673 : {
1674 0 : GNUNET_break (0);
1675 0 : po->result = TALER_EC_GENERIC_DB_FETCH_FAILED;
1676 : }
1677 24 : if (TALER_EC_NONE != po->result)
1678 : {
1679 0 : GNUNET_break (0);
1680 0 : return TALER_MHD_reply_with_error (connection,
1681 : MHD_HTTP_INTERNAL_SERVER_ERROR,
1682 : po->result,
1683 : NULL);
1684 : }
1685 33 : if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) &&
1686 9 : (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) )
1687 : {
1688 2 : struct GNUNET_TIME_Absolute wakeup = po->long_poll_timeout;
1689 :
1690 2 : if (TALER_EXCHANGE_YNA_YES == po->of.expired)
1691 0 : wakeup = GNUNET_TIME_absolute_min (
1692 : wakeup,
1693 : GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_SECONDS));
1694 2 : GNUNET_assert (NULL == po->order_timeout_task);
1695 : po->order_timeout_task
1696 2 : = GNUNET_SCHEDULER_add_at (wakeup,
1697 : &order_timeout,
1698 : po);
1699 2 : GNUNET_CONTAINER_DLL_insert (mi->po_head,
1700 : mi->po_tail,
1701 : po);
1702 2 : po->in_dll = true;
1703 2 : MHD_suspend_connection (connection);
1704 2 : return MHD_YES;
1705 : }
1706 22 : return reply_orders (po,
1707 : connection,
1708 : mi);
1709 : }
1710 :
1711 :
1712 : /* end of taler-merchant-httpd_get-private-orders.c */
|