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