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