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