merchant

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

commit a1cf5ed17a55dd3bb43b97a476dd1d64c646c614
parent 590e22e63779f18c2dc79f1226439e248d9d11e6
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat, 27 Dec 2025 15:28:18 +0100

work on report generation, e-mail submission, etc.

Diffstat:
Mdebian/control | 3++-
Mdebian/rules | 1+
Adebian/taler-merchant.taler-merchant-report-generator.service | 21+++++++++++++++++++++
Mdebian/taler-merchant.taler-merchant.target | 1+
Mdoc/Makefile.am | 2++
Msrc/backend/.gitignore | 1+
Msrc/backend/Makefile.am | 17++++++++++++++++-
Msrc/backend/merchant.conf | 4++++
Msrc/backend/taler-merchant-httpd.c | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-delete-pot-ID.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-delete-pot-ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-pot-ID.c | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-pot-ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-pots.c | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-pots.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-patch-pot-ID.c | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-patch-pot-ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-post-pots.c | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-post-pots.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-report-generator-email | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-report-generator.c | 823+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_insert_money_pot.c | 5+++--
Msrc/backenddb/pg_select_money_pots.c | 6------
Msrc/backenddb/pg_update_money_pot.c | 25++++++++++++++++---------
Msrc/backenddb/pg_update_money_pot.h | 12++++++++----
Msrc/backenddb/pg_update_money_pot.sql | 34++++++++++++++++++++++++----------
Msrc/include/taler_merchantdb_plugin.h | 25++++++++++++++-----------
27 files changed, 1902 insertions(+), 44 deletions(-)

diff --git a/debian/control b/debian/control @@ -59,7 +59,8 @@ Depends: Recommends: postgresql (>=15.0), taler-terms-generator, - apache2 | nginx | httpd + apache2 | nginx | httpd, + sharutils Description: GNU's payment system merchant backend. . The GNU Taler merchant backend provides e-commerce diff --git a/debian/rules b/debian/rules @@ -43,6 +43,7 @@ override_dh_installsystemd: dh_installsystemd -ptaler-merchant --name=taler-merchant-donaukeyupdate --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-kyccheck --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-reconciliation --no-start --no-enable + dh_installsystemd -ptaler-merchant --name=taler-merchant-report-generator --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-webhook --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-wirewatch --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant --no-start --no-enable diff --git a/debian/taler-merchant.taler-merchant-report-generator.service b/debian/taler-merchant.taler-merchant-report-generator.service @@ -0,0 +1,21 @@ +[Unit] +Description=GNU Taler merchant report generation service +After=postgresql.service +PartOf=taler-merchant.target + +[Service] +User=taler-merchant-httpd +Type=simple +Restart=always +RestartMode=direct +RestartSec=1s +RestartPreventExitStatus=9 +ExecStart=/usr/bin/taler-merchant-report-generator -c /etc/taler-merchant/taler-merchant.conf -L INFO +PrivateTmp=yes +PrivateDevices=yes +ProtectSystem=full +RuntimeMaxSec=3600s +Slice=taler-merchant.slice + +StandardOutput=journal +StandardError=journal diff --git a/debian/taler-merchant.taler-merchant.target b/debian/taler-merchant.taler-merchant.target @@ -8,6 +8,7 @@ Wants=taler-merchant-donaukeyupdate.service Wants=taler-merchant-httpd.service Wants=taler-merchant-kyccheck.service Wants=taler-merchant-reconciliation.service +Wants=taler-merchant-report-generator.service Wants=taler-merchant-webhook.service Wants=taler-merchant-wirewatch.service diff --git a/doc/Makefile.am b/doc/Makefile.am @@ -13,6 +13,8 @@ man_MANS = \ prebuilt/man/taler-merchant-kyccheck.1 \ prebuilt/man/taler-merchant-passwd.1 \ prebuilt/man/taler-merchant-reconciliation.1 \ + prebuilt/man/taler-merchant-report-generator.1 \ + prebuilt/man/taler-merchant-report-generator-email.1 \ prebuilt/man/taler-merchant-rproxy-setup.1 \ prebuilt/man/taler-merchant-webhook.1 \ prebuilt/man/taler-merchant-wirewatch.1 diff --git a/src/backend/.gitignore b/src/backend/.gitignore @@ -5,3 +5,4 @@ taler-merchant-reconciliation taler-merchant-webhook taler-merchant-wirewatch taler-merchant-donaukeyupdate +taler-merchant-report-generator diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -13,8 +13,13 @@ pkgcfg_DATA = \ merchant.conf \ tops.conf + +bin_SCRIPTS = \ + taler-merchant-report-generator-email + EXTRA_DIST = \ - $(pkgcfg_DATA) + $(pkgcfg_DATA) \ + $(bin_SCRIPTS) bin_PROGRAMS = \ taler-merchant-depositcheck \ @@ -245,6 +250,16 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-patch-report-ID.h \ taler-merchant-httpd_private-post-reports.c \ taler-merchant-httpd_private-post-reports.h \ + taler-merchant-httpd_private-delete-pot-ID.c \ + taler-merchant-httpd_private-delete-pot-ID.h \ + taler-merchant-httpd_private-get-pot-ID.c \ + taler-merchant-httpd_private-get-pot-ID.h \ + taler-merchant-httpd_private-get-pots.c \ + taler-merchant-httpd_private-get-pots.h \ + taler-merchant-httpd_private-patch-pot-ID.c \ + taler-merchant-httpd_private-patch-pot-ID.h \ + taler-merchant-httpd_private-post-pots.c \ + taler-merchant-httpd_private-post-pots.h \ taler-merchant-httpd_private-delete-group-ID.c \ taler-merchant-httpd_private-delete-group-ID.h \ taler-merchant-httpd_private-get-groups.c \ diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -69,3 +69,7 @@ AML_FREQ = 6h # How long do we wait between AML status requests to the # exchange if we do not expect any AML status changes? AML_LOW_FREQ = 7d + + +[report-generator-email] +BINARY = taler-merchant-report-generator-email diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -117,6 +117,11 @@ #include "taler-merchant-httpd_private-get-reports.h" #include "taler-merchant-httpd_private-patch-report-ID.h" #include "taler-merchant-httpd_private-post-reports.h" +#include "taler-merchant-httpd_private-delete-pot-ID.h" +#include "taler-merchant-httpd_private-get-pot-ID.h" +#include "taler-merchant-httpd_private-get-pots.h" +#include "taler-merchant-httpd_private-patch-pot-ID.h" +#include "taler-merchant-httpd_private-post-pots.h" #include "taler-merchant-httpd_private-get-groups.h" #include "taler-merchant-httpd_private-post-groups.h" #include "taler-merchant-httpd_private-patch-group-ID.h" @@ -2231,6 +2236,41 @@ url_handler (void *cls, .have_id_segment = true, }, + /* Money pots endpoints */ + { + .url_prefix = "pots", + .method = MHD_HTTP_METHOD_GET, + .handler = &TMH_private_get_pots, + .permission = "pots-read", + }, + { + .url_prefix = "pots", + .method = MHD_HTTP_METHOD_POST, + .handler = &TMH_private_post_pots, + .permission = "pots-write" + }, + { + .url_prefix = "pots", + .method = MHD_HTTP_METHOD_GET, + .handler = &TMH_private_get_pot, + .have_id_segment = true, + .permission = "pots-read", + }, + { + .url_prefix = "pots", + .method = MHD_HTTP_METHOD_PATCH, + .handler = &TMH_private_patch_pot, + .have_id_segment = true, + .permission = "pots-write" + }, + { + .url_prefix = "pots", + .method = MHD_HTTP_METHOD_DELETE, + .handler = &TMH_private_delete_pot, + .have_id_segment = true, + .permission = "pots-write" + }, + { .url_prefix = "*", .method = MHD_HTTP_METHOD_OPTIONS, diff --git a/src/backend/taler-merchant-httpd_private-delete-pot-ID.c b/src/backend/taler-merchant-httpd_private-delete-pot-ID.c @@ -0,0 +1,75 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-delete-pot-ID.c + * @brief implementation of DELETE /private/pots/$POT_ID + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-delete-pot-ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_delete_pot (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *pot_id_str = hc->infix; + unsigned long long pot_id; + enum GNUNET_DB_QueryStatus qs; + char dummy; + + (void) rh; + if (1 != sscanf (pot_id_str, + "%llu%c", + &pot_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "pot_id"); + } + + qs = TMH_db->delete_money_pot (TMH_db->cls, + hc->instance->settings.id, + pot_id); + + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_money_pot"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + pot_id_str); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_private-delete-pot-ID.h b/src/backend/taler-merchant-httpd_private-delete-pot-ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-delete-pot-ID.h + * @brief HTTP serving layer for deleting money pots + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_POT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_POT_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Handle DELETE /private/pots/$POT_ID request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_delete_pot (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-pot-ID.c b/src/backend/taler-merchant-httpd_private-get-pot-ID.c @@ -0,0 +1,93 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-get-pot-ID.c + * @brief implementation of GET /private/pots/$POT_ID + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-pot-ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_get_pot (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *pot_id_str = hc->infix; + unsigned long long pot_id; + char *pot_name; + char *description; + struct TALER_Amount pot_total; + enum GNUNET_DB_QueryStatus qs; + char dummy; + + (void) rh; + if (1 != sscanf (pot_id_str, + "%llu%c", + &pot_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "pot_id"); + } + qs = TMH_db->select_money_pot (TMH_db->cls, + hc->instance->settings.id, + pot_id, + &pot_name, + &description, + &pot_total); + + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_money_pot"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + pot_id_str); + } + + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("description", + description), + GNUNET_JSON_pack_string ("pot_name", + pot_name), + TALER_JSON_pack_amount ("pot_total", + &pot_total)); + GNUNET_free (pot_name); + GNUNET_free (description); + return ret; + } +} diff --git a/src/backend/taler-merchant-httpd_private-get-pot-ID.h b/src/backend/taler-merchant-httpd_private-get-pot-ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-get-pot-ID.h + * @brief HTTP serving layer for getting pot details + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_POT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_POT_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Handle GET /private/pots/$POT_ID request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_pot (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-pots.c b/src/backend/taler-merchant-httpd_private-get-pots.c @@ -0,0 +1,122 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-get-pots.c + * @brief implementation of GET /private/pots + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-pots.h" +#include <taler/taler_json_lib.h> + + +/** + * Sensible bound on the limit. + */ +#define MAX_DELTA 1024 + + +/** + * Callback for listing money pots. + * + * @param cls closure with a `json_t *` + * @param money_pot_id unique identifier of the pot + * @param name name of the pot + * @param description human-readable description (ignored for listing) + * @param pot_total current total amount in the pot + */ +static void +add_pot (void *cls, + uint64_t money_pot_id, + const char *name, + const struct TALER_Amount *pot_total) +{ + json_t *pots = cls; + json_t *entry; + + entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("pot_serial", + money_pot_id), + GNUNET_JSON_pack_string ("pot_name", + name), + TALER_JSON_pack_amount ("pot_total", + pot_total)); + GNUNET_assert (NULL != entry); + GNUNET_assert (0 == + json_array_append_new (pots, + entry)); +} + + +MHD_RESULT +TMH_private_get_pots (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + int64_t limit = -20; + uint64_t offset; + json_t *pots; + + (void) rh; + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + if ( (-MAX_DELTA > limit) || + (limit > MAX_DELTA) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "limit"); + } + + pots = json_array (); + GNUNET_assert (NULL != pots); + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->select_money_pots (TMH_db->cls, + hc->instance->settings.id, + limit, + offset, + &add_pot, + pots); + if (qs < 0) + { + GNUNET_break (0); + json_decref (pots); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_money_pots"); + } + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("pots", + pots)); +} diff --git a/src/backend/taler-merchant-httpd_private-get-pots.h b/src/backend/taler-merchant-httpd_private-get-pots.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-get-pots.h + * @brief HTTP serving layer for listing money pots + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_POTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_POTS_H + +#include "taler-merchant-httpd.h" + +/** + * Handle GET /private/pots request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_pots (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-pot-ID.c b/src/backend/taler-merchant-httpd_private-patch-pot-ID.c @@ -0,0 +1,165 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-patch-pot.c + * @brief implementation of PATCH /private/pots/$POT_ID + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-patch-pot-ID.h" +#include <taler/taler_json_lib.h> + + +/** + * ISSUE: The database API and REST API have a conflict handling mismatch. + * + * REST API spec says: + * - Return 409 Conflict if "the pot total did not match the expected total" + * + * Database API provides: + * - conflict parameter that is set if old_pot_total doesn't match + * + * However, there's a semantic problem: + * - The conflict parameter is about pot_name uniqueness in update_product_group + * - But here it's about pot total mismatch in update_money_pot + * - This is inconsistent terminology across the database API + * + * The implementation below assumes 'conflict' means "pot total mismatch" + * for money pots, but this should be clarified. + */ + + +MHD_RESULT +TMH_private_patch_pot (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *pot_id_str = hc->infix; + unsigned long long pot_id; + const char *pot_name; + const char *description; + struct TALER_Amount expected_pot_total; + bool no_expected_total; + struct TALER_Amount new_pot_total; + bool no_new_total; + enum GNUNET_DB_QueryStatus qs; + bool conflict_total; + bool conflict_name; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("pot_name", + &pot_name), + GNUNET_JSON_spec_string ("description", + &description), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("expected_pot_total", + &expected_pot_total), + &no_expected_total), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("new_pot_total", + &new_pot_total), + &no_new_total), + GNUNET_JSON_spec_end () + }; + + (void) rh; + { + char dummy; + + if (1 != sscanf (pot_id_str, + "%llu%c", + &pot_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "pot_id"); + } + } + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + + qs = TMH_db->update_money_pot (TMH_db->cls, + hc->instance->settings.id, + pot_id, + pot_name, + description, + no_expected_total + ? NULL + : &expected_pot_total, + no_new_total + ? NULL + : &new_pot_total, + &conflict_total, + &conflict_name); + + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_money_pot"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + pot_id_str); + } + if (conflict_total) + { + /* Pot total mismatch - expected_pot_total didn't match current value */ + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_MONEY_POT_CONFLICTING_TOTAL, + NULL); + } + if (conflict_name) + { + /* Pot name conflict - name exists */ + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_MONEY_POT_CONFLICTING_NAME, + pot_name); + } + + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_private-patch-pot-ID.h b/src/backend/taler-merchant-httpd_private-patch-pot-ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-patch-pot-ID.h + * @brief HTTP serving layer for updating money pots + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_POT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_POT_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Handle PATCH /private/pots/$POT_ID request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_patch_pot (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_private-post-pots.c b/src/backend/taler-merchant-httpd_private-post-pots.c @@ -0,0 +1,105 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-post-pots.c + * @brief implementation of POST /private/pots + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler-merchant-httpd_private-post-pots.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_post_pots (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *pot_name; + const char *description; + const char *currency; + enum GNUNET_DB_QueryStatus qs; + uint64_t pot_id; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("pot_name", + &pot_name), + GNUNET_JSON_spec_string ("description", + &description), + GNUNET_JSON_spec_string ("currency", + &currency), + GNUNET_JSON_spec_end () + }; + + (void) rh; + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + + /* Validate currency string */ + if (GNUNET_OK != + TALER_check_currency (currency)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + currency); + } + + qs = TMH_db->insert_money_pot (TMH_db->cls, + hc->instance->settings.id, + pot_name, + description, + currency, + &pot_id); + + if (qs < 0) + { + /* NOTE: Like product groups, we cannot distinguish between a + * generic DB error and a unique constraint violation on pot_name. + */ + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_money_pot"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Zero will be returned on conflict */ + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_MONEY_POT_CONFLICTING_NAME, + pot_name); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("pot_serial_id", + pot_id)); +} diff --git a/src/backend/taler-merchant-httpd_private-post-pots.h b/src/backend/taler-merchant-httpd_private-post-pots.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + + You should have received a copy of the GNU Affero General Public License + along with TALER; see the file COPYING. If not, see + <http://www.gnu.org/licenses/> +*/ +/** + * @file merchant/backend/taler-merchant-httpd_private-post-pots.h + * @brief HTTP serving layer for creating money pots + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_POTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_POTS_H +#include "taler-merchant-httpd.h" + +/** + * Handle POST /private/pots request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_post_pots (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-report-generator-email b/src/backend/taler-merchant-report-generator-email @@ -0,0 +1,161 @@ +#!/bin/bash +# +# This file is part of TALER +# Copyright (C) 2025 Taler Systems SA +# +# TALER is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3, or (at your option) any later version. +# +# TALER is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# + +# +# Script to email Taler merchant reports using the UNIX mail command. +# Reads report data from stdin and sends it via email with appropriate formatting. +# +# Usage: taler-merchant-report-generator-email -d DESCRIPTION -m MIME_TYPE -t TARGET_ADDRESS +# + +set -eu + +DESCRIPTION="" +MIME_TYPE="" +TARGET_ADDRESS="" +TMPDIR="${TMPDIR:-/tmp}" + +while getopts "d:m:t:h" opt; do + case $opt in + d) + DESCRIPTION="$OPTARG" + ;; + m) + MIME_TYPE="$OPTARG" + ;; + t) + TARGET_ADDRESS="$OPTARG" + ;; + h) + echo "Usage: $0 -d DESCRIPTION -m MIME_TYPE -t EMAIL_ADDRESS" + echo "" + echo "Sends reports via email." + echo "" + echo "Options:" + echo " -d DESCRIPTION Subject line for the email" + echo " -m MIME_TYPE MIME type of the report (e.g., text/plain, application/pdf)" + echo " -t EMAIL_ADDRESS Email address to send the report to" + echo " -h Show this help message" + echo "" + echo "The report data is read from stdin." + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + echo "Use -h for help" >&2 + exit 1 + ;; + esac +done + +if [ -z "$DESCRIPTION" ]; +then + echo "Error: Description (-d) is required" >&2 + exit 1 +fi + +if [ -z "$MIME_TYPE" ]; +then + echo "Error: MIME type (-m) is required" >&2 + exit 1 +fi + +if [ -z "$TARGET_ADDRESS" ]; +then + echo "Error: Target address (-t) is required" >&2 + exit 1 +fi + +# Validate email address format (basic check) +if ! echo "$TARGET_ADDRESS" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; +then + echo "Error: Invalid email address format: $TARGET_ADDRESS" >&2 + exit 1 +fi + +if ! command -v mail >/dev/null 2>&1; +then + echo "Error: 'mail' command not found." >&2 + exit 1 +fi +if ! command -v uuencode >/dev/null 2>&1; +then + echo "Error: 'uuencode' command not found." >&2 + exit 1 +fi + +# Normalize MIME type to lowercase for comparison +MIME_TYPE=$(echo "$MIME_TYPE" | tr '[:upper:]' '[:lower:]') + +# Handle different MIME types +case "$MIME_TYPE" in + text/plain) + # For plain text, send directly as email body + mail -s "$DESCRIPTION" "$TARGET_ADDRESS" + ;; + + *) + # For all other MIME types, create a MIME attachment + # Create temporary files + TMPFILE=$(mktemp "$TMPDIR/taler-report.XXXXXX") + MIMEFILE=$(mktemp "$TMPDIR/taler-mime.XXXXXX") + + # Ensure cleanup on exit + trap "rm -f '$TMPFILE' '$MIMEFILE'" EXIT + + # Save stdin to temporary file + cat - > "$TMPFILE" + + # Determine file extension based on MIME type + case "$MIME_TYPE" in + application/pdf) + EXT="pdf" + ;; + application/json) + EXT="json" + ;; + text/html) + EXT="html" + ;; + text/csv) + EXT="csv" + ;; + application/xml) + EXT="xml" + ;; + application/zip) + EXT="zip" + ;; + image/png) + EXT="png" + ;; + image/jpeg) + EXT="jpg" + ;; + *) + EXT="dat" + ;; + esac + + FILENAME="report.$EXT" + + # Use uuencode method (works with traditional mail command) + uuencode "$TMPFILE" "$FILENAME" | mail -s "$DESCRIPTION" "$TARGET_ADDRESS" + ;; +esac + +exit 0 +\ No newline at end of file diff --git a/src/backend/taler-merchant-report-generator.c b/src/backend/taler-merchant-report-generator.c @@ -0,0 +1,823 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +/** + * @file taler-merchant-report-generator.c + * @brief Service for fetching and transmitting merchant reports + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_db_lib.h> +#include <gnunet/gnunet_curl_lib.h> +#include <taler/taler_merchantdb_plugin.h> +#include <taler/taler_merchantdb_lib.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_merchant_service.h> +#include <curl/curl.h> + + +/** + * Information about an active reporting activity. + */ +struct ReportActivity +{ + + /** + * Kept in a DLL. + */ + struct ReportActivity *next; + + /** + * Kept in a DLL. + */ + struct ReportActivity *prev; + + /** + * Transmission program that is running. + */ + struct GNUNET_OS_Process *proc; + + /** + * Handle to wait for @e proc to terminate. + */ + struct GNUNET_ChildWaitHandle *cwh; + + /** + * CURL easy handle for the HTTP request. + */ + CURL *eh; + + /** + * Job handle for the HTTP request. + */ + struct GNUNET_CURL_Job *job; + + /** + * URL of the request. + */ + char *url; + + /** + * ID of the instance we are working on. + */ + char *instance_id; + + /** + * Report program section. + */ + char *report_program_section; + + /** + * Report description. + */ + char *report_description; + + /** + * Target address for transmission. + */ + char *target_address; + + /** + * MIME type of the report. + */ + char *mime_type; + + /** + * Report we are working on. + */ + uint64_t report_id; + + /** + * Next transmission time, already calculated. + */ + struct GNUNET_TIME_Absolute next_transmission; + + /** + * HTTP response code. + */ + long response_code; + +}; + + +/** + * Global return value. + */ +static int global_ret; + +/** + * #GNUNET_YES if we are in test mode and should exit when idle. + */ +static int test_mode; + +/** + * Base URL of the merchant backend. + */ +static char *base_url; + +/** + * Our configuration. + */ +static const struct GNUNET_CONFIGURATION_Handle *cfg; + +/** + * Database plugin. + */ +static struct TALER_MERCHANTDB_Plugin *db_plugin; + +/** + * Event handler for database change notifications. + */ +static struct GNUNET_DB_EventHandler *eh; + +/** + * Task for checking pending reports. + */ +static struct GNUNET_SCHEDULER_Task *report_task; + +/** + * Context for CURL operations. + */ +static struct GNUNET_CURL_Context *curl_ctx; + +/** + * Reschedule context for CURL. + */ +static struct GNUNET_CURL_RescheduleContext *curl_rc; + +/** + * Head of DLL of active report activities. + */ +static struct ReportActivity *ra_head; + +/** + * Tail of DLL of active report activities. + */ +static struct ReportActivity *ra_tail; + + +/** + * Free a report activity structure. + * + * @param[in] ra report activity to free + */ +static void +free_ra (struct ReportActivity *ra) +{ + if (NULL != ra->cwh) + { + GNUNET_wait_child_cancel (ra->cwh); + ra->cwh = NULL; + } + if (NULL != ra->proc) + { + GNUNET_OS_process_kill (ra->proc, + SIGKILL); + GNUNET_OS_process_wait (ra->proc); + GNUNET_OS_process_destroy (ra->proc); + ra->proc = NULL; + } + if (NULL != ra->eh) + { + curl_easy_cleanup (ra->eh); + ra->eh = NULL; + } + if (NULL != ra->job) + { + GNUNET_CURL_job_cancel (ra->job); + ra->job = NULL; + } + GNUNET_CONTAINER_DLL_remove (ra_head, + ra_tail, + ra); + GNUNET_free (ra->instance_id); + GNUNET_free (ra->report_program_section); + GNUNET_free (ra->report_description); + GNUNET_free (ra->target_address); + GNUNET_free (ra->mime_type); + GNUNET_free (ra->url); + GNUNET_free (ra); +} + + +/** + * Finish transmission of a report and update database. + * + * @param[in] ra report activity to finish + * @param ec error code (#TALER_EC_NONE on success) + * @param error_details human-readable error details (NULL on success) + */ +static void +finish_transmission (struct ReportActivity *ra, + enum TALER_ErrorCode ec, + const char *error_details) +{ + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp next_ts; + + next_ts = GNUNET_TIME_absolute_to_timestamp (ra->next_transmission); + qs = db_plugin->update_report_status (db_plugin->cls, + ra->instance_id, + ra->report_id, + next_ts, + ec, + error_details); + free_ra (ra); + if (qs < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to update report status: %d\n", + qs); + global_ret = 1; + GNUNET_SCHEDULER_shutdown (); + return; + } +} + + +/** + * Callback invoked when the child process terminates. + * + * @param cls closure, a `struct ReportActivity *` + * @param type type of the process + * @param exit_code exit code of the process + */ +static void +child_completed_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct ReportActivity *ra = cls; + enum TALER_ErrorCode ec; + char *error_details = NULL; + + ra->cwh = NULL; + if ( (GNUNET_OS_PROCESS_EXITED != type) || + (0 != exit_code) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Report transmission program failed with status %d/%lu\n", + (int) type, + exit_code); + ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + GNUNET_asprintf (&error_details, + "Report transmission program exited with status %d/%lu", + (int) type, + exit_code); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Report transmitted successfully\n"); + ec = TALER_EC_NONE; + } + finish_transmission (ra, + ec, + error_details); + GNUNET_free (error_details); +} + + +/** + * Transmit a report using the respective report program. + * + * @param[in,out] ra which report activity are we working on + * @param report_len length of @a report + * @param report binary report data to transmit + */ +static void +transmit_report (struct ReportActivity *ra, + size_t report_len, + const void *report) +{ + const char *binary; + struct GNUNET_DISK_FileHandle *stdin_handle; + + { + char *section; + + GNUNET_asprintf (&section, + "report-generator-%s", + ra->report_program_section); + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (cfg, + section, + "BINARY", + (char **) &binary)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "BINARY"); + finish_transmission (ra, + TALER_EC_MERCHANT_GENERIC_REPORT_GENERATOR_UNCONFIGURED, + section); + GNUNET_free (section); + return; + } + GNUNET_free (section); + } + + { + struct GNUNET_DISK_PipeHandle *stdin_pipe; + + stdin_pipe = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); + if (NULL == stdin_pipe) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "pipe"); + finish_transmission (ra, + TALER_EC_GENERIC_OS_RESOURCE_ALLOCATION_FAILURE, + "pipe"); + return; + } + + ra->proc = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, + stdin_pipe, + NULL, + NULL, + binary, + binary, + "-d", + ra->report_description, + "-m", + ra->mime_type, + "-t", + ra->target_address, + NULL); + if (NULL == ra->proc) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "exec", + binary); + GNUNET_DISK_pipe_close (stdin_pipe); + finish_transmission (ra, + TALER_EC_MERCHANT_REPORT_GENERATOR_FAILED, + "Could not execute report generator binary"); + return; + } + + /* Write report data to stdin of child process */ + stdin_handle = GNUNET_DISK_pipe_detach_end (stdin_pipe, + GNUNET_DISK_PIPE_END_WRITE); + GNUNET_DISK_pipe_close (stdin_pipe); + } + + { + size_t off = 0; + + while (off < report_len) + { + ssize_t wrote; + + wrote = GNUNET_DISK_file_write (stdin_handle, + report, + report_len); + if (wrote <= 0) + break; + off += (size_t) wrote; + } + GNUNET_DISK_file_close (stdin_handle); + + if (off != report_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to write report data to child process stdin\n"); + finish_transmission (ra, + TALER_EC_MERCHANT_REPORT_GENERATOR_FAILED, + "Failed to write to transmission program"); + return; + } + } + + /* Wait for child to complete */ + ra->cwh = GNUNET_wait_child (ra->proc, + &child_completed_cb, + ra); +} + + +/** + * Callback invoked when CURL request completes. + * + * @param cls closure, a `struct ReportActivity *` + * @param response_code HTTP response code + * @param body http body of the response + * @param body_size number of bytes in @a body + */ +static void +curl_completed_cb (void *cls, + long response_code, + const void *body, + size_t body_size) +{ + struct ReportActivity *ra = cls; + + ra->eh = NULL; + ra->response_code = response_code; + if (MHD_HTTP_OK != response_code) + { + char *error_details; + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to fetch report data: HTTP %ld\n", + response_code); + GNUNET_asprintf (&error_details, + "HTTP request failed with status %ld from `%s'", + response_code, + ra->url); + finish_transmission (ra, + TALER_EC_MERCHANT_REPORT_FETCH_FAILED, + error_details); + GNUNET_free (error_details); + return; + } + transmit_report (ra, + body_size, + body); +} + + +/** + * Function to fetch data from @a data_source at @a instance_id + * and to send it to the @a target_address + * + * @param[in,out] ra which report activity are we working on + * @param mime_type mime type to request from @a data_source + * @param data_source relative URL path to request data from + */ +static void +fetch_and_transmit (struct ReportActivity *ra, + const char *mime_type, + const char *data_source) +{ + struct curl_slist *headers = NULL; + char *accept_header; + + GNUNET_asprintf (&ra->url, + "%sinstances/%s%s", + base_url, + ra->instance_id, + data_source); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Fetching report from %s\n", + ra->url); + ra->eh = curl_easy_init (); + if (NULL == ra->eh) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize CURL handle\n"); + finish_transmission (ra, + TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE, + "curl_easy_init"); + return; + } + + GNUNET_asprintf (&accept_header, + "Accept: %s", + mime_type); + headers = curl_slist_append (headers, + accept_header); + GNUNET_free (accept_header); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + ra->url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_HTTPHEADER, + headers)); + // FIXME: need to set Authorization header!!! + ra->job = GNUNET_CURL_job_add_raw (curl_ctx, + ra->eh, + headers, + &curl_completed_cb, + ra); + ra->eh = NULL; + curl_slist_free_all (headers); +} + + +/** + * Callback invoked for each pending report. + * + * @param cls closure + * @param instance_id name of the instance + * @param report_id serial number of the report + * @param report_program_section configuration section of program + * @param report_description text describing the report + * @param mime_type mime type to request + * @param data_source relative URL to request report data from + * @param target_address where to send report data + * @param frequency report frequency + * @param frequency_shift time shift from frequency multiple + */ +static void +process_pending_report (void *cls, + const char *instance_id, + uint64_t report_id, + const char *report_program_section, + const char *report_description, + const char *mime_type, + const char *data_source, + const char *target_address, + struct GNUNET_TIME_Relative frequency, + struct GNUNET_TIME_Relative frequency_shift, + struct GNUNET_TIME_Absolute next_transmission) +{ + struct GNUNET_TIME_Absolute *next = cls; + struct ReportActivity *ra; + + *next = next_transmission; + if (GNUNET_TIME_absolute_is_future (next_transmission)) + return; + *next = GNUNET_TIME_UNIT_ZERO_ABS; /* there might be more! */ + next_transmission = + GNUNET_TIME_absolute_add ( + GNUNET_TIME_absolute_round_down (GNUNET_TIME_absolute_get (), + frequency), + GNUNET_TIME_relative_add (frequency, + frequency_shift)); + if (! GNUNET_TIME_absolute_is_future (next_transmission)) + { + /* frequency near-zero!? */ + GNUNET_break (0); + next_transmission = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_MINUTES); + } + ra = GNUNET_new (struct ReportActivity); + ra->instance_id = GNUNET_strdup (instance_id); + ra->report_id = report_id; + ra->next_transmission = next_transmission; + ra->report_program_section = GNUNET_strdup (report_program_section); + ra->report_description = GNUNET_strdup (report_description); + ra->target_address = GNUNET_strdup (target_address); + ra->mime_type = GNUNET_strdup (mime_type); + GNUNET_CONTAINER_DLL_insert (ra_head, + ra_tail, + ra); + fetch_and_transmit (ra, + mime_type, + data_source); +} + + +/** + * Check for pending reports and process them. + * + * @param cls closure (unused) + */ +static void +check_pending_reports (void *cls) +{ + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute next; + + (void) cls; + report_task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking for pending reports...\n"); + next = GNUNET_TIME_UNIT_FOREVER_ABS; + qs = db_plugin->lookup_reports_pending (db_plugin->cls, + &process_pending_report, + &next); + if (qs < 0) + { + GNUNET_break (0); + global_ret = 1; + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_assert (NULL == report_task); + if (test_mode && + GNUNET_TIME_absolute_is_future (next) && + (NULL == ra_head)) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + report_task = GNUNET_SCHEDULER_add_at (next, + &check_pending_reports, + NULL); +} + + +/** + * Callback invoked when a MERCHANT_REPORT_UPDATE event is received. + * + * @param cls closure (unused) + * @param extra additional event data (unused) + * @param extra_size size of @a extra + */ +static void +report_update_cb (void *cls, + const void *extra, + size_t extra_size) +{ + (void) cls; + (void) extra; + (void) extra_size; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received MERCHANT_REPORT_UPDATE event\n"); + /* Cancel any pending check and schedule immediate execution */ + if (NULL != report_task) + GNUNET_SCHEDULER_cancel (report_task); + report_task = GNUNET_SCHEDULER_add_now (&check_pending_reports, + NULL); +} + + +/** + * Shutdown the service cleanly. + * + * @param cls closure (unused) + */ +static void +do_shutdown (void *cls) +{ + struct ReportActivity *ra; + + (void) cls; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shutting down report generator service\n"); + + while (NULL != (ra = ra_head)) + free_ra (ra); + + if (NULL != report_task) + { + GNUNET_SCHEDULER_cancel (report_task); + report_task = NULL; + } + if (NULL != curl_rc) + { + GNUNET_CURL_gnunet_rc_destroy (curl_rc); + curl_rc = NULL; + } + if (NULL != curl_ctx) + { + GNUNET_CURL_fini (curl_ctx); + curl_ctx = NULL; + } + if (NULL != eh) + { + db_plugin->event_listen_cancel (eh); + eh = NULL; + } + if (NULL != db_plugin) + { + TALER_MERCHANTDB_plugin_unload (db_plugin); + db_plugin = NULL; + } + GNUNET_free (base_url); + base_url = NULL; +} + + +/** + * Main function for the report generator service. + * + * @param cls closure + * @param args remaining command-line arguments + * @param cfgfile name of the configuration file used + * @param config configuration + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *config) +{ + (void) cls; + (void) args; + (void) cfgfile; + + cfg = config; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "merchant", + "BASE_URL", + &base_url)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "BASE_URL not configured in section [merchant]\n"); + global_ret = 1; + return; + } + + /* Ensure base_url ends with '/' */ + if ('/' != base_url[strlen (base_url) - 1]) + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s/", + base_url); + GNUNET_free (base_url); + base_url = tmp; + } + + GNUNET_SCHEDULER_add_shutdown (&do_shutdown, + NULL); + + curl_ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &curl_rc); + if (NULL == curl_ctx) + { + GNUNET_break (0); + global_ret = 1; + GNUNET_SCHEDULER_shutdown (); + return; + } + curl_rc = GNUNET_CURL_gnunet_rc_create (curl_ctx); + + db_plugin = TALER_MERCHANTDB_plugin_load (cfg); + if (NULL == db_plugin) + { + GNUNET_break (0); + global_ret = 1; + GNUNET_SCHEDULER_shutdown (); + return; + } + + { + struct GNUNET_DB_EventHeaderP ev = { + .size = htons (sizeof (ev)), + .type = htons (TALER_DBEVENT_MERCHANT_REPORT_UPDATE) + }; + + eh = db_plugin->event_listen (db_plugin->cls, + &ev, + GNUNET_TIME_UNIT_FOREVER_REL, + &report_update_cb, + NULL); + if (NULL == eh) + { + GNUNET_break (0); + global_ret = 1; + GNUNET_SCHEDULER_shutdown (); + return; + } + } + report_task = GNUNET_SCHEDULER_add_now (&check_pending_reports, + NULL); +} + + +/** + * The main function of the report generator service. + * + * @param argc number of arguments from the command line + * @param argv command line arguments + * @return 0 ok, 1 on error + */ +int +main (int argc, + char *const *argv) +{ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_flag ('t', + "test", + "run in test mode and exit when idle", + &test_mode), + GNUNET_GETOPT_option_timetravel ('T', + "timetravel"), + GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION), + GNUNET_GETOPT_OPTION_END + }; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_PROGRAM_run ( + TALER_MERCHANT_project_data (), + argc, argv, + "taler-merchant-report-generator", + "Fetch and transmit periodic merchant reports", + options, + &run, + NULL); + if (GNUNET_SYSERR == ret) + return EXIT_INVALIDARGUMENT; + if (GNUNET_NO == ret) + return EXIT_SUCCESS; + return global_ret; +} + + +/* end of taler-merchant-report-generator.c */ diff --git a/src/backenddb/pg_insert_money_pot.c b/src/backenddb/pg_insert_money_pot.c @@ -32,7 +32,7 @@ TMH_PG_insert_money_pot ( const char *instance_id, const char *name, const char *description, - const char *pod_currency, + const char *pot_currency, uint64_t *money_pot_id) { struct PostgresClosure *pg = cls; @@ -52,7 +52,7 @@ TMH_PG_insert_money_pot ( }; if (GNUNET_OK != - TALER_amount_set_zero (pod_currency, + TALER_amount_set_zero (pot_currency, &zero_c)) { GNUNET_break (0); @@ -69,6 +69,7 @@ TMH_PG_insert_money_pot ( " SELECT merchant_serial, $2, $3, $4" " FROM merchant_instances" " WHERE merchant_id=$1" + " ON CONFLICT DO NOTHING" " RETURNING money_pot_serial;"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "insert_money_pot", diff --git a/src/backenddb/pg_select_money_pots.c b/src/backenddb/pg_select_money_pots.c @@ -67,15 +67,12 @@ lookup_money_pots_cb (void *cls, { uint64_t money_pot_serial; char *money_pot_name; - char *money_pot_description; struct TALER_Amount pot_total; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("money_pot_serial", &money_pot_serial), GNUNET_PQ_result_spec_string ("money_pot_name", &money_pot_name), - GNUNET_PQ_result_spec_string ("money_pot_description", - &money_pot_description), TALER_PQ_result_spec_amount_with_currency ("pot_total", &pot_total), GNUNET_PQ_result_spec_end @@ -93,7 +90,6 @@ lookup_money_pots_cb (void *cls, plc->cb (plc->cb_cls, money_pot_serial, money_pot_name, - money_pot_description, &pot_total); GNUNET_PQ_cleanup_result (rs); } @@ -130,7 +126,6 @@ TMH_PG_select_money_pots (void *cls, "SELECT" " money_pot_serial" " ,money_pot_name" - " ,money_pot_description" " ,pot_total" " FROM merchant_money_pots" " JOIN merchant_instances" @@ -144,7 +139,6 @@ TMH_PG_select_money_pots (void *cls, "SELECT" " money_pot_serial" " ,money_pot_name" - " ,money_pot_description" " ,pot_total" " FROM merchant_money_pots" " JOIN merchant_instances" diff --git a/src/backenddb/pg_update_money_pot.c b/src/backenddb/pg_update_money_pot.c @@ -35,7 +35,8 @@ TMH_PG_update_money_pot ( const char *description, const struct TALER_Amount *old_pot_total, const struct TALER_Amount *new_pot_total, - bool *conflict) + bool *conflict_total, + bool *conflict_name) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -43,16 +44,22 @@ TMH_PG_update_money_pot ( GNUNET_PQ_query_param_uint64 (&money_pot_id), GNUNET_PQ_query_param_string (name), GNUNET_PQ_query_param_string (description), - TALER_PQ_query_param_amount_with_currency (pg->conn, - old_pot_total), - TALER_PQ_query_param_amount_with_currency (pg->conn, - new_pot_total), + (NULL == old_pot_total) + ? GNUNET_PQ_query_param_null () + : TALER_PQ_query_param_amount_with_currency (pg->conn, + old_pot_total), + (NULL == new_pot_total) + ? GNUNET_PQ_query_param_null () + : TALER_PQ_query_param_amount_with_currency (pg->conn, + new_pot_total), GNUNET_PQ_query_param_end }; bool not_found; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("conflict", - conflict), + GNUNET_PQ_result_spec_bool ("conflict_total", + conflict_total), + GNUNET_PQ_result_spec_bool ("conflict_name", + conflict_name), GNUNET_PQ_result_spec_bool ("not_found", &not_found), GNUNET_PQ_result_spec_end @@ -60,11 +67,11 @@ TMH_PG_update_money_pot ( enum GNUNET_DB_QueryStatus qs; check_connection (pg); - PREPARE (pg, "update_money_pot", "SELECT" - " out_conflict AS conflict" + " out_conflict_name AS conflict_name" + " out_conflict_total AS conflict_total" " out_not_found AS not_found" " FROM merchant_do_update_money_pot" "($1,$2,$3,$4,$5,$6);"); diff --git a/src/backenddb/pg_update_money_pot.h b/src/backenddb/pg_update_money_pot.h @@ -33,9 +33,12 @@ * @param money_pot_id serial number of the pot to delete * @param name set to name of the pot * @param description set to description of the pot - * @param old_pot_total amount expected currently in the pot - * @param new_pot_total new amount in the pot - * @param[out] conflict set to true if @a old_pot_total does not match + * @param old_pot_total amount expected currently in the pot, + * NULL to not check + * @param new_pot_total new amount in the pot, + * NULL to not update + * @param[out] conflict_total set to true if @a old_pot_total does not match + * @param[out] conflict_name set to true if @a name is used by another pot * @return database result code */ enum GNUNET_DB_QueryStatus @@ -47,7 +50,8 @@ TMH_PG_update_money_pot ( const char *description, const struct TALER_Amount *old_pot_total, const struct TALER_Amount *new_pot_total, - bool *conflict); + bool *conflict_total, + bool *conflict_name); #endif diff --git a/src/backenddb/pg_update_money_pot.sql b/src/backenddb/pg_update_money_pot.sql @@ -18,12 +18,13 @@ DROP FUNCTION IF EXISTS merchant_do_update_money_pot; CREATE FUNCTION merchant_do_update_money_pot ( IN in_instance_id TEXT, - IN in_money_pot_serial TEXT, + IN in_money_pot_serial INT8, IN in_name TEXT, IN in_description TEXT, - IN in_old_total taler_amount_currency, - IN in_new_total taler_amount_currency, - OUT out_conflict BOOL, + IN in_old_total taler_amount_currency, -- can be NULL! + IN in_new_total taler_amount_currency, -- can be NULL! + OUT out_conflict_total BOOL, + OUT out_conflict_name BOOL, OUT out_not_found BOOL) LANGUAGE plpgsql AS $$ @@ -36,32 +37,45 @@ SELECT merchant_serial FROM merchant_instances WHERE merchant_id=in_instance_id; +IF NOT FOUND +THEN + -- If instance does not exist, pot cannot exist + out_conflict_total = FALSE; + out_conflict_name = FALSE; + out_not_found = TRUE; + RETURN; +END IF; + BEGIN UPDATE merchant_money_pots SET money_pot_name=in_name ,money_pot_description=in_description - ,pot_total=in_new_total + ,pot_total=COALESCE(in_new_total, pot_total) WHERE merchant_serial=my_merchant_id AND money_pot_serial=in_money_pot_serial - AND pot_total=in_old_total; + AND ( (in_old_total IS NULL) OR (pot_total=in_old_total) ); IF NOT FOUND THEN -- Check if pot_total was the problem PERFORM FROM merchant_money_pots WHERE merchant_serial=my_merchant_id AND money_pot_serial=in_money_pot_serial; - out_conflict = FOUND; - out_not_found = FALSE; + out_conflict_total = FOUND; + out_not_found = NOT FOUND; + out_conflict_name = FALSE; ELSE - out_conflict = FALSE; + out_conflict_total = FALSE; out_not_found = FALSE; + out_conflict_name = FALSE; END IF; RETURN; EXCEPTION -- money_pot_name already used WHEN unique_violation THEN - out_conflict = TRUE; + out_conflict_name = TRUE; + out_conflict_total = FALSE; + out_not_found = FALSE; RETURN; END; diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -1337,15 +1337,13 @@ typedef void * * @param cls closure * @param name set to name of the pot - * @param description set to description of the pot - * @param pod_total amount currently in the pot + * @param pot_total amount currently in the pot */ typedef void (*TALER_MERCHANTDB_MoneyPotsCallback)( void *cls, uint64_t money_pot_id, const char *name, - const char *description, const struct TALER_Amount *pot_total); @@ -4920,7 +4918,7 @@ struct TALER_MERCHANTDB_Plugin * @param instance_id instance to lookup token families for * @param limit number of entries to return, negative for descending in execution time, * positive for ascending in execution time - * @param offset expected_transfer_serial number of the transfer we want to offset from + * @param offset number of the money pot we want to offset from * @param cb function to call on all money pots found * @param cb_cls closure for @a cb * @return database result code @@ -4973,9 +4971,12 @@ struct TALER_MERCHANTDB_Plugin * @param money_pot_id serial number of the pot to delete * @param name set to name of the pot * @param description set to description of the pot - * @param old_pot_total amount expected currently in the pot - * @param new_pot_total new amount in the pot - * @param[out] conflict set to true if @a old_pot_total does not match + * @param old_pot_total amount expected currently in the pot, + * NULL to not check + * @param new_pot_total new amount in the pot, + * NULL to not update + * @param[out] conflict_total set to true if @a old_pot_total does not match + * @param[out] conflict_name set to true if @a name is used by another pot * @return database result code */ enum GNUNET_DB_QueryStatus @@ -4987,7 +4988,8 @@ struct TALER_MERCHANTDB_Plugin const char *description, const struct TALER_Amount *old_pot_total, const struct TALER_Amount *new_pot_total, - bool *conflict); + bool *conflict_total, + bool *conflict_name); /** @@ -4997,9 +4999,10 @@ struct TALER_MERCHANTDB_Plugin * @param instance_id instance to insert pot for * @param name set to name of the pot * @param description set to description of the pot - * @param pod_currency the expected currency in the pot + * @param pot_currency the expected currency in the pot * @param[out] money_pot_id serial number of the new pot - * @return database result code + * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS + * on conflict (@a name already in use at @a instance_id). */ enum GNUNET_DB_QueryStatus (*insert_money_pot)( @@ -5007,7 +5010,7 @@ struct TALER_MERCHANTDB_Plugin const char *instance_id, const char *name, const char *description, - const char *pod_currency, + const char *pot_currency, uint64_t *money_pot_id); // Reports