merchant

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

commit 63bacf69034cdfa4da4ec794bc8c2ddc0ac5c15f
parent b29d3a74d6945731e105beb9cf6d0524d2ec8d84
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  5 Mar 2026 00:00:50 +0100

backend rename fest for consitency, starting on typst issue

Diffstat:
Mconfigure.ac | 3+++
Mcontrib/typst/Makefile.am | 10+---------
Acontrib/typst/common/lib.typ | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/common/typst.toml | 9+++++++++
Dcontrib/typst/orders.typ | 189-------------------------------------------------------------------------------
Acontrib/typst/orders/orders.typ | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/orders/template/main.typ | 39+++++++++++++++++++++++++++++++++++++++
Acontrib/typst/orders/typst.toml | 9+++++++++
Dcontrib/typst/transactions.typ | 355-------------------------------------------------------------------------------
Acontrib/typst/transactions/template/main.typ | 45+++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/transactions/transactions.typ | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/typst/transactions/typst.toml | 9+++++++++
Mdebian/control | 10++++++++++
Adebian/taler-merchant-typst.install | 4++++
Mdebian/taler-merchant.postinst | 83+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mdebian/taler-merchant.prerm | 10++++++----
Msrc/backend/Makefile.am | 383++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/backend/merchant.conf | 2+-
Msrc/backend/taler-merchant-httpd.c | 25+++++++++++++------------
Asrc/backend/taler-merchant-httpd_delete-management-instances-INSTANCE.c | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-management-instances-INSTANCE.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-accounts-H_WIRE.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-accounts-H_WIRE.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-categories-CATEGORY_ID.c | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-groups-GROUP_ID.c | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-groups-GROUP_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-orders-ORDER_ID.c | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-orders-ORDER_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-pots-POT_ID.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-pots-POT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-products-PRODUCT_ID.c | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-products-PRODUCT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-reports-REPORT_ID.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-reports-REPORT_ID.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-tokens-SERIAL.c | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-tokens-SERIAL.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-transfers-TID.c | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-transfers-TID.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-units-UNIT.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-units-UNIT.h | 33+++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_dispatcher.c | 191+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/backend/taler-merchant-httpd_exchanges.c | 4++--
Msrc/backend/taler-merchant-httpd_exchanges.h | 2+-
Msrc/backend/taler-merchant-httpd_get-config.c | 7++++---
Msrc/backend/taler-merchant-httpd_get-config.h | 2+-
Msrc/backend/taler-merchant-httpd_get-exchanges.c | 6+++---
Msrc/backend/taler-merchant-httpd_get-exchanges.h | 2+-
Asrc/backend/taler-merchant-httpd_get-management-instances-INSTANCE.c | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-management-instances-INSTANCE.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-management-instances.c | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-management-instances.h | 41+++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_get-orders-ID.c | 1713-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_get-orders-ID.h | 47-----------------------------------------------
Asrc/backend/taler-merchant-httpd_get-orders-ORDER_ID.c | 1713+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-orders-ORDER_ID.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-accounts-H_WIRE.c | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-accounts-H_WIRE.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-accounts.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-accounts.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-categories-CATEGORY_ID.c | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-categories-CATEGORY_ID.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-categories.c | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-categories.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-donau.c | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-donau.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-groups.c | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-groups.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-incoming-ID.c | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-incoming-ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-incoming.c | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-incoming.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-kyc.c | 1488+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-kyc.h | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.c | 1795+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-orders.c | 1533+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-orders.h | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.c | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-otp-devices.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-otp-devices.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-pos.c | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-pos.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-pots-POT_ID.c | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-pots-POT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-pots.c | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-pots.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-products-PRODUCT_ID.c | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-products-PRODUCT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-products.c | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-products.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.c | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-reports.c | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-reports.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-statistics-amount-SLUG.c | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-statistics-amount-SLUG.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-statistics-counter-SLUG.c | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-statistics-counter-SLUG.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-statistics-report-transactions.c | 763+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-statistics-report-transactions.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-templates-TEMPLATE_ID.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-templates.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-templates.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.c | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-tokenfamilies.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-tokenfamilies.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-tokens.c | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-tokens.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-transfers.c | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-transfers.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-units-UNIT.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-units-UNIT.h | 33+++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-units.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-units.h | 33+++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.c | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-webhooks.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-private-webhooks.h | 41+++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_get-products-HASH-image.c | 87-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_get-products-HASH-image.h | 32--------------------------------
Asrc/backend/taler-merchant-httpd_get-products-IMAGE_HASH-image.c | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-products-IMAGE_HASH-image.h | 32++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_get-sessions-ID.c | 312-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_get-sessions-ID.h | 48------------------------------------------------
Asrc/backend/taler-merchant-httpd_get-sessions-SESSION_ID.c | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-sessions-SESSION_ID.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_get-templates-ID.c | 568-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_get-templates-ID.h | 42------------------------------------------
Asrc/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c | 568+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.h | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_get-terms.c | 4++--
Msrc/backend/taler-merchant-httpd_get-terms.h | 2+-
Msrc/backend/taler-merchant-httpd_get-webui.c | 2+-
Msrc/backend/taler-merchant-httpd_get-webui.h | 2+-
Asrc/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.c | 514+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-accounts-H_WIRE.c | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-accounts-H_WIRE.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-categories-CATEGORY_ID.c | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-categories-CATEGORY_ID.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-groups-GROUP_ID.c | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-groups-GROUP_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.c | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.c | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-pots-POT_ID.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-pots-POT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.c | 482+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-reports-REPORT_ID.c | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-reports-REPORT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.c | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.c | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-units-UNIT.c | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-units-UNIT.h | 33+++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.c | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c | 342+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.h | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-management-instances.c | 694+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-management-instances.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_post-orders-ID-abort.c | 1043-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-abort.h | 49-------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-claim.c | 331-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-claim.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-paid.c | 198-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-paid.h | 40----------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 5308-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-pay.h | 49-------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-refund.c | 845-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-refund.h | 48------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-unclaim.c | 122-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-orders-ID-unclaim.h | 40----------------------------------------
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c | 1044+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-claim.c | 331+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-claim.h | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-paid.c | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-paid.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 5309+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.c | 846+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-accounts.c | 470+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-accounts.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-categories.c | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-categories.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-donau.c | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-donau.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-groups.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-groups.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c | 467+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-orders.c | 4900+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-orders.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-otp-devices.c | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-otp-devices.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-pots.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-pots.h | 40++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.c | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-products.c | 435+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-products.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-reports.c | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-reports.h | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-templates.c | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-templates.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-token.c | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-token.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-tokenfamilies.c | 384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-tokenfamilies.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-transfers.c | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-transfers.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-units.c | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-units.h | 33+++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-webhooks.c | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-private-webhooks.h | 43+++++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_post-reports-ID.c | 189-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-reports-ID.h | 41-----------------------------------------
Asrc/backend/taler-merchant-httpd_post-reports-REPORT_ID.c | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-reports-REPORT_ID.h | 41+++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_post-templates-ID.c | 1782-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_post-templates-ID.h | 40----------------------------------------
Asrc/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c | 1783+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.h | 40++++++++++++++++++++++++++++++++++++++++
Dsrc/backend/taler-merchant-httpd_private-delete-account-ID.c | 94-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-account-ID.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-categories-ID.c | 92-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-categories-ID.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-donau-instance-ID.c | 94-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-donau-instance-ID.h | 44--------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-group-ID.c | 74--------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-group-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-instances-ID-token.c | 164-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-instances-ID-token.h | 59-----------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-instances-ID.c | 163-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-instances-ID.h | 56--------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-orders-ID.c | 133-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-orders-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c | 78------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-otp-devices-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-pot-ID.c | 75---------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-pot-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-products-ID.c | 103-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-products-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-report-ID.c | 76----------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-report-ID.h | 40----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-templates-ID.c | 78------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-templates-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-token-families-SLUG.c | 75---------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-token-families-SLUG.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-transfers-ID.c | 90-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-transfers-ID.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-units-ID.c | 83-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-units-ID.h | 33---------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-webhooks-ID.c | 78------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-delete-webhooks-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-accounts-ID.c | 109-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-accounts-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-accounts.c | 84-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-accounts.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-categories-ID.c | 121-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-categories-ID.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-categories.c | 93-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-categories.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-donau-instances.c | 122-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-donau-instances.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-groups.c | 122-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-groups.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-incoming-ID.c | 236-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-incoming-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-incoming.c | 194-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-incoming.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c | 1487-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances-ID-kyc.h | 67-------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c | 118-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances-ID-tokens.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances-ID.c | 156-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances-ID.h | 56--------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances.c | 125-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-instances.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-orders-ID.c | 1795-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-orders-ID.h | 49-------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-orders.c | 1532-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-orders.h | 76----------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-otp-devices-ID.c | 110-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-otp-devices-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-otp-devices.c | 80-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-otp-devices.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-pos.c | 234-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-pos.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-pot-ID.c | 100-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-pot-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-pots.c | 128-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-pots.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-products-ID.c | 160-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-products-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-products.c | 149-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-products.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-report-ID.c | 135-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-report-ID.h | 40----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-reports.c | 119-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-reports.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.c | 254-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.c | 227-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c | 762-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-statistics-report-transactions.h | 49-------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-templates-ID.c | 80-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-templates-ID.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-templates.c | 79-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-templates.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-token-families-SLUG.c | 126-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-token-families-SLUG.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-token-families.c | 101-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-token-families.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-transfers.c | 189-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-transfers.h | 42------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-units-ID.c | 89-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-units-ID.h | 33---------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-units.c | 91-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-units.h | 33---------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-webhooks-ID.c | 92-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-webhooks-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-webhooks.c | 80-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-get-webhooks.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-accounts-ID.c | 149-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-accounts-ID.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-categories-ID.c | 120-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-categories-ID.h | 45---------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-group-ID.c | 110-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-group-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-instances-ID.c | 514-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-instances-ID.h | 59-----------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c | 243-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-orders-ID-forget.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-otp-devices-ID.c | 114-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-otp-devices-ID.h | 44--------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-pot-ID.c | 152-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-pot-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-products-ID.c | 482-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-products-ID.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-report-ID.c | 146-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-report-ID.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-templates-ID.c | 217-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-templates-ID.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-token-families-SLUG.c | 159-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-token-families-SLUG.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-units-ID.c | 242-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-units-ID.h | 33---------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-webhooks-ID.c | 188-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-patch-webhooks-ID.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-account.c | 470-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-account.h | 44--------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-categories.c | 170-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-categories.h | 45---------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-donau-instance.c | 352-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-donau-instance.h | 49-------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-groups.c | 88-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-groups.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-instances-ID-auth.c | 342-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-instances-ID-auth.h | 80-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-instances-ID-token.c | 192-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-instances-ID-token.h | 45---------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-instances.c | 694-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-instances.h | 59-----------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-orders-ID-refund.c | 466-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-orders-ID-refund.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-orders.c | 4899-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-orders.h | 50--------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-otp-devices.c | 199-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-otp-devices.h | 44--------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-pots.c | 91-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-pots.h | 40----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-products-ID-lock.c | 207-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-products-ID-lock.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-products.c | 435-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-products.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-reports.c | 147-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-reports.h | 41-----------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-templates.c | 252-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-templates.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-token-families.c | 384-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-token-families.h | 43-------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-transfers.c | 144-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-transfers.h | 44--------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-units.c | 219-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-units.h | 33---------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-webhooks.c | 215-------------------------------------------------------------------------------
Dsrc/backend/taler-merchant-httpd_private-post-webhooks.h | 43-------------------------------------------
410 files changed, 41589 insertions(+), 41596 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -495,6 +495,9 @@ AM_CONDITIONAL([HAVE_EXPERIMENTAL], [test "x$enable_experimental" = "xyes"]) AC_CONFIG_FILES([Makefile contrib/Makefile contrib/typst/Makefile +contrib/typst/common/Makefile +contrib/typst/orders/Makefile +contrib/typst/transactions/Makefile doc/Makefile doc/doxygen/Makefile src/Makefile diff --git a/contrib/typst/Makefile.am b/contrib/typst/Makefile.am @@ -1,9 +1 @@ -SUBDIRS = . - -formdatadir = $(datadir)/taler-merchant/typst-forms/ -dist_formdata_DATA = \ - orders.typ \ - transactions.typ - -EXTRA_DIST = \ - $(dist_formdata_DATA) +SUBDIRS = . common orders transactions diff --git a/contrib/typst/common/lib.typ b/contrib/typst/common/lib.typ @@ -0,0 +1,224 @@ +// Helper function to format timeframe +#let format_timeframe(d_us) = { + if d_us == "forever" { + "forever" + } else { + let us = int(d_us) + let s = calc.quo(us, 1000000) + let m = calc.quo(s, 60) + let h = calc.quo(m, 60) + let d = calc.quo(h, 24) + let w = calc.quo(d, 7) + + if calc.rem(us, 1000000) == 0 { + if calc.rem(s, 60) == 0 { + if calc.rem(m, 60) == 0 { + if calc.rem(h, 24) == 0 { + if calc.rem(d, 7) == 0 { + str(w) + " week" + if w != 1 { "s" } else { "" } + } else { + str(d) + " day" + if d != 1 { "s" } else { "" } + } + } else { + str(h) + " hour" + if h != 1 { "s" } else { "" } + } + } else { + str(m) + " minute" + if m != 1 { "s" } else { "" } + } + } else { + str(s) + " s" + } + } else { + str(us) + " μs" + } + } +} + +// Helper function to format timestamp; ignores leap seconds (too variable) +// Helper function to format a Taler timestamp object {t_s: ...} +#let format_timestamp(ts) = { + if type(ts) == dictionary and "t_s" in ts { + let t_s = ts.t_s + if t_s == "never" { + "never" + } else { + // Convert Unix timestamp to human-readable format + let seconds = int(t_s) + let days_since_epoch = calc.quo(seconds, 86400) + let remaining_seconds = calc.rem(seconds, 86400) + let hours = calc.quo(remaining_seconds, 3600) + let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) + let secs = calc.rem(remaining_seconds, 60) + + // Helper to check if year is leap year + let is_leap(y) = { + calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) + } + + // Calculate year, month, day + let year = 1970 + let days_left = days_since_epoch + + // Find the year + let done = false + while not done { + let days_in_year = if is_leap(year) { 366 } else { 365 } + if days_left >= days_in_year { + days_left = days_left - days_in_year + year = year + 1 + } else { + done = true + } + } + + // Days in each month + let days_in_months = if is_leap(year) { + (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } else { + (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } + + // Find month and day + let month = 1 + for days_in_month in days_in_months { + if days_left >= days_in_month { + days_left = days_left - days_in_month + month = month + 1 + } else { + break + } + } + let day = days_left + 1 + + // Format with leading zeros + let m_str = if month < 10 { "0" + str(month) } else { str(month) } + let d_str = if day < 10 { "0" + str(day) } else { str(day) } + let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } + let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } + let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } + + str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" + } + } else { + str(ts) + } +} + + +// Helper function to format timestamp; ignores leap seconds (too variable) +// Coarsen based on r_us (a value in milliseconds). +// If r_us is a minute, do not show seconds. +// If r_us is an hour, do not show minutes or seconds +// If r_us is a day, do not show hours, minutes or seconds. +// If r_us is a month, do not show days. +// If r_us is a year, do not show months. +// If mini is true, truly minify the result only showing +// the unit around the r_us granularity. +#let format_round_timestamp(ts, r_us, mini) = { + if type(ts) == dictionary and "t_s" in ts { + let t_s = ts.t_s + if t_s == "never" { + "never" + } else { + // Convert Unix timestamp to human-readable format + let seconds = int(t_s) + let days_since_epoch = calc.quo(seconds, 86400) + let remaining_seconds = calc.rem(seconds, 86400) + let hours = calc.quo(remaining_seconds, 3600) + let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) + let secs = calc.rem(remaining_seconds, 60) + + // Helper to check if year is leap year + let is_leap(y) = { + calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) + } + + // Calculate year, month, day + let year = 1970 + let days_left = days_since_epoch + + // Find the year + let done = false + while not done { + let days_in_year = if is_leap(year) { 366 } else { 365 } + if days_left >= days_in_year { + days_left = days_left - days_in_year + year = year + 1 + } else { + done = true + } + } + + // Days in each month + let days_in_months = if is_leap(year) { + (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } else { + (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } + + // Find month and day + let month = 1 + for days_in_month in days_in_months { + if days_left >= days_in_month { + days_left = days_left - days_in_month + month = month + 1 + } else { + break + } + } + let day = days_left + 1 + + // Format with leading zeros + let m_str = if month < 10 { "0" + str(month) } else { str(month) } + let d_str = if day < 10 { "0" + str(day) } else { str(day) } + let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } + let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } + let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } + + // Define thresholds in microseconds + let minute_us = 60 * 1000 * 1000 + let hour_us = 60 * minute_us + let day_us = 24 * hour_us + let month_us = 30 * day_us // Approximate: 30 days + let year_us = 365 * day_us // Approximate: 365 days + + // Build timestamp string based on r_us thresholds + if r_us >= year_us { + // Year or more: show only year + str(year) + } else if r_us >= month_us { + // Month or more: show year and month + if (mini) { m_str } else { str(year) + "-" + m_str } + } else if r_us >= day_us { + // Day or more: show year, month, and day + if (mini) { d_str + "d" } else { str(year) + "-" + m_str + "-" + d_str } + } else if r_us >= hour_us { + // Hour or more: show up to hours + if (mini) { h_str + "h" } else { str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":00 UTC" } + } else if r_us >= minute_us { + // Minute or more: show up to minutes + if (mini) { min_str + "m" } else { str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + " UTC" } + } else { + // Less than a minute: show full precision + if (mini) { s_str + "s" } else { str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" } + } + } + } else { + str(ts) + } +} + +// Format a Taler amount. +// Taler serialises amounts as a plain string "CURRENCY:VALUE.FRACTION", +// e.g. "EUR:5.50". If the value is null / none we render a dash. +#let format_amount(a) = { + if a == none { + "-" + } else { + // a is already a human-readable string like "EUR:5.50" + // Note: eventually we want it formatted *nicely*, but this + // needs the currency rendering data, so probably better done + // on the C side (where we don't yet have an amount renderer). + str(a).replace(":", " ") + } +} diff --git a/contrib/typst/common/typst.toml b/contrib/typst/common/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "common" +version = "0.0.0" +entrypoint = "lib.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Helper functions for GNU Taler merchant PDF generation" +repository = "git://git.taler.net/merchant" +keywords = ["Taler"] diff --git a/contrib/typst/orders.typ b/contrib/typst/orders.typ @@ -1,189 +0,0 @@ -// Helper function to format a Taler timestamp object {t_s: ...} -#let format_timestamp(ts) = { - if type(ts) == dictionary and "t_s" in ts { - let t_s = ts.t_s - if t_s == "never" { - "never" - } else { - let seconds = int(t_s) - let days_since_epoch = calc.quo(seconds, 86400) - let remaining_seconds = calc.rem(seconds, 86400) - let hours = calc.quo(remaining_seconds, 3600) - let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) - let secs = calc.rem(remaining_seconds, 60) - - let is_leap(y) = { - calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) - } - - let year = 1970 - let days_left = days_since_epoch - let done = false - while not done { - let days_in_year = if is_leap(year) { 366 } else { 365 } - if days_left >= days_in_year { - days_left = days_left - days_in_year - year = year + 1 - } else { - done = true - } - } - - let days_in_months = if is_leap(year) { - (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } else { - (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } - - let month = 1 - for days_in_month in days_in_months { - if days_left >= days_in_month { - days_left = days_left - days_in_month - month = month + 1 - } else { - break - } - } - let day = days_left + 1 - - let m_str = if month < 10 { "0" + str(month) } else { str(month) } - let d_str = if day < 10 { "0" + str(day) } else { str(day) } - let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } - let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } - let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } - - str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" - } - } else { - str(ts) - } -} - -#let breakable_hyp(s) = s.replace("-", "-#zwsp") - -// Format a Taler amount. -// Taler serialises amounts as a plain string "CURRENCY:VALUE.FRACTION", -// e.g. "EUR:5.50". If the value is null / none we render a dash. -#let format_amount(a) = { - if a == none { - "-" - } else { - // a is already a human-readable string like "EUR:5.50" - // Note: eventually we want it formatted *nicely*, but this - // needs the currency rendering data, so probably better done - // on the C side (where we don't yet have an amount renderer). - str(a).replace(":", " ") - } -} - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - heading(level: 1)[GNU Taler Merchant Orders: #data.business_name] - - v(0.5cm) - - table( - columns: (auto, auto, auto), - align: (left, right, right), - // Header row - table.header( - [*Order ID*], - [*Timestamp*], - [*Price* #text(fill: red)[$-$ *Refund*]], - ), - // Data rows - ..data.orders.map(o => ( - table.cell()[#text(8pt, o.order_id)], - table.cell(stroke: (bottom: none))[#format_timestamp(o.timestamp)], - table.cell(stroke: (bottom: none))[#format_amount(o.amount)], - table.cell(colspan:2, x: 0, stroke: (top: none))[ - #grid( - columns: (1fr, auto), - o.summary, - if o.paid { - text(fill: green)[✓ (paid)] - } else { - text(fill: red)[✗ (unpaid)] - }, - ) - ], - table.cell(stroke: (top: none))[#{ - let r = o.at("refund_amount", default: none) - if (r != none) { - text(fill: red)[$-$ #format_amount(r)] - } else { - "-" - } - }], - )).flatten(), - // Footer row - table.footer( - table.cell(colspan: 2, stroke: (bottom: none))[*Total (paid only)*], - table.cell(stroke: (bottom: none))[ - *#format_amount(data.total_amount)* - ], - table.cell(colspan: 2, x: 0, stroke: (top: none))[*Total (refunds)*], - table.cell(stroke: (top: none))[#{ - let r = data.total_refund_amount - if r != none { - text(fill: red)[$-$ *#format_amount(data.total_refund_amount)*] - } else { - ["-"] - } - }], - ), - ) -} - -// Example usage: -#form(( - business_name: "example.com", - orders: ( - ( - order_id: "2025.001-5asdasfa", - row_id: 1, - summary: "Some purchase", - timestamp: (t_s: 1764967786), - amount: "EUR:10.00", - paid: true, - refundable: false, - ), - ( - order_id: "2025.002-5asdasfa", - row_id: 2, - summary: "Refunded order", - timestamp: (t_s: 1764970000), - amount: "EUR:5.50", - refund_amount: "EUR:2.00", - pending_refund_amount: "EUR:1.00", - paid: true, - refundable: true, - ), - ( - order_id: "2025.003-5asdasfa", - row_id: 3, - summary: "Another order", - timestamp: (t_s: 1764975000), - amount: "EUR:3.25", - paid: false, - refundable: false, - ), - ), - total_amount: "EUR:18.75", - total_refund_amount: "EUR:2.00", - total_pending_refund_amount: "EUR:1.00", -)) diff --git a/contrib/typst/orders/orders.typ b/contrib/typst/orders/orders.typ @@ -0,0 +1,76 @@ +#import "@taler-merchant/common:0.0.0": format_timestamp, format_amount + +#let breakable_hyp(s) = s.replace("-", "-#zwsp") + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + heading(level: 1)[GNU Taler Merchant Orders: #data.business_name] + + v(0.5cm) + + table( + columns: (auto, auto, auto), + align: (left, right, right), + // Header row + table.header( + [*Order ID*], + [*Timestamp*], + [*Price* #text(fill: red)[$-$ *Refund*]], + ), + // Data rows + ..data.orders.map(o => ( + table.cell()[#text(8pt, o.order_id)], + table.cell(stroke: (bottom: none))[#format_timestamp(o.timestamp)], + table.cell(stroke: (bottom: none))[#format_amount(o.amount)], + table.cell(colspan:2, x: 0, stroke: (top: none))[ + #grid( + columns: (1fr, auto), + o.summary, + if o.paid { + text(fill: green)[✓ (paid)] + } else { + text(fill: red)[✗ (unpaid)] + }, + ) + ], + table.cell(stroke: (top: none))[#{ + let r = o.at("refund_amount", default: none) + if (r != none) { + text(fill: red)[$-$ #format_amount(r)] + } else { + "-" + } + }], + )).flatten(), + // Footer row + table.footer( + table.cell(colspan: 2, stroke: (bottom: none))[*Total (paid only)*], + table.cell(stroke: (bottom: none))[ + *#format_amount(data.total_amount)* + ], + table.cell(colspan: 2, x: 0, stroke: (top: none))[*Total (refunds)*], + table.cell(stroke: (top: none))[#{ + let r = data.total_refund_amount + if r != none { + text(fill: red)[$-$ *#format_amount(data.total_refund_amount)*] + } else { + ["-"] + } + }], + ), + ) +} diff --git a/contrib/typst/orders/template/main.typ b/contrib/typst/orders/template/main.typ @@ -0,0 +1,39 @@ +#import "@taler-merchant/orders:0.0.0": form + +#form(( + business_name: "example.com", + orders: ( + ( + order_id: "2025.001-5asdasfa", + row_id: 1, + summary: "Some purchase", + timestamp: (t_s: 1764967786), + amount: "EUR:10.00", + paid: true, + refundable: false, + ), + ( + order_id: "2025.002-5asdasfa", + row_id: 2, + summary: "Refunded order", + timestamp: (t_s: 1764970000), + amount: "EUR:5.50", + refund_amount: "EUR:2.00", + pending_refund_amount: "EUR:1.00", + paid: true, + refundable: true, + ), + ( + order_id: "2025.003-5asdasfa", + row_id: 3, + summary: "Another order", + timestamp: (t_s: 1764975000), + amount: "EUR:3.25", + paid: false, + refundable: false, + ), + ), + total_amount: "EUR:18.75", + total_refund_amount: "EUR:2.00", + total_pending_refund_amount: "EUR:1.00", +)) diff --git a/contrib/typst/orders/typst.toml b/contrib/typst/orders/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "orders" +version = "0.0.0" +entrypoint = "orders.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Order summary for GNU Taler merchant PDF generation" +repository = "git://git.taler.net/exchange" +keywords = ["accounting"] diff --git a/contrib/typst/transactions.typ b/contrib/typst/transactions.typ @@ -1,354 +0,0 @@ -#import "@preview/cetz:0.4.2": canvas, draw, palette -#import "@preview/cetz-plot:0.1.3": plot, chart - -#let form(data) = { - set page( - paper: "a4", - margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), - footer: context [ - #grid( - columns: (1fr, 1fr), - align: (left, right), - text(size: 8pt)[], - text(size: 8pt)[ - Page #here().page() of #counter(page).final().first() - ] - ) - ] - ) - - // Helper function to format timeframe - let format_timeframe(d_us) = { - if d_us == "forever" { - "forever" - } else { - let us = int(d_us) - let s = calc.quo(us, 1000000) - let m = calc.quo(s, 60) - let h = calc.quo(m, 60) - let d = calc.quo(h, 24) - let w = calc.quo(d, 7) - - if calc.rem(us, 1000000) == 0 { - if calc.rem(s, 60) == 0 { - if calc.rem(m, 60) == 0 { - if calc.rem(h, 24) == 0 { - if calc.rem(d, 7) == 0 { - str(w) + " week" + if w != 1 { "s" } else { "" } - } else { - str(d) + " day" + if d != 1 { "s" } else { "" } - } - } else { - str(h) + " hour" + if h != 1 { "s" } else { "" } - } - } else { - str(m) + " minute" + if m != 1 { "s" } else { "" } - } - } else { - str(s) + " s" - } - } else { - str(us) + " μs" - } - } - } - - // Helper function to format timestamp; ignores leap seconds (too variable) - let format_timestamp(ts) = { - if type(ts) == dictionary and "t_s" in ts { - let t_s = ts.t_s - if t_s == "never" { - "never" - } else { - // Convert Unix timestamp to human-readable format - let seconds = int(t_s) - let days_since_epoch = calc.quo(seconds, 86400) - let remaining_seconds = calc.rem(seconds, 86400) - let hours = calc.quo(remaining_seconds, 3600) - let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) - let secs = calc.rem(remaining_seconds, 60) - - // Helper to check if year is leap year - let is_leap(y) = { - calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) - } - - // Calculate year, month, day - let year = 1970 - let days_left = days_since_epoch - - // Find the year - let done = false - while not done { - let days_in_year = if is_leap(year) { 366 } else { 365 } - if days_left >= days_in_year { - days_left = days_left - days_in_year - year = year + 1 - } else { - done = true - } - } - - // Days in each month - let days_in_months = if is_leap(year) { - (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } else { - (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } - - // Find month and day - let month = 1 - for days_in_month in days_in_months { - if days_left >= days_in_month { - days_left = days_left - days_in_month - month = month + 1 - } else { - break - } - } - let day = days_left + 1 - - // Format with leading zeros - let m_str = if month < 10 { "0" + str(month) } else { str(month) } - let d_str = if day < 10 { "0" + str(day) } else { str(day) } - let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } - let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } - let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } - - str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" - } - } else { - str(ts) - } - } - - - // Helper function to format timestamp; ignores leap seconds (too variable) - // Coarsen based on r_us (a value in milliseconds). - // If r_us is a minute, do not show seconds. - // If r_us is an hour, do not show minutes or seconds - // If r_us is a day, do not show hours, minutes or seconds. - // If r_us is a month, do not show days. - // If r_us is a year, do not show months. - // If mini is true, truly minify the result only showing - // the unit around the r_us granularity. - let format_round_timestamp(ts, r_us, mini) = { - if type(ts) == dictionary and "t_s" in ts { - let t_s = ts.t_s - if t_s == "never" { - "never" - } else { - // Convert Unix timestamp to human-readable format - let seconds = int(t_s) - let days_since_epoch = calc.quo(seconds, 86400) - let remaining_seconds = calc.rem(seconds, 86400) - let hours = calc.quo(remaining_seconds, 3600) - let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) - let secs = calc.rem(remaining_seconds, 60) - - // Helper to check if year is leap year - let is_leap(y) = { - calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) - } - - // Calculate year, month, day - let year = 1970 - let days_left = days_since_epoch - - // Find the year - let done = false - while not done { - let days_in_year = if is_leap(year) { 366 } else { 365 } - if days_left >= days_in_year { - days_left = days_left - days_in_year - year = year + 1 - } else { - done = true - } - } - - // Days in each month - let days_in_months = if is_leap(year) { - (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } else { - (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - } - - // Find month and day - let month = 1 - for days_in_month in days_in_months { - if days_left >= days_in_month { - days_left = days_left - days_in_month - month = month + 1 - } else { - break - } - } - let day = days_left + 1 - - // Format with leading zeros - let m_str = if month < 10 { "0" + str(month) } else { str(month) } - let d_str = if day < 10 { "0" + str(day) } else { str(day) } - let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } - let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } - let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } - - // Define thresholds in microseconds - let minute_us = 60 * 1000 * 1000 - let hour_us = 60 * minute_us - let day_us = 24 * hour_us - let month_us = 30 * day_us // Approximate: 30 days - let year_us = 365 * day_us // Approximate: 365 days - - // Build timestamp string based on r_us thresholds - if r_us >= year_us { - // Year or more: show only year - str(year) - } else if r_us >= month_us { - // Month or more: show year and month - if (mini) { m_str } else { str(year) + "-" + m_str } - } else if r_us >= day_us { - // Day or more: show year, month, and day - if (mini) { d_str + "d" } else { str(year) + "-" + m_str + "-" + d_str } - } else if r_us >= hour_us { - // Hour or more: show up to hours - if (mini) { h_str + "h" } else { str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":00 UTC" } - } else if r_us >= minute_us { - // Minute or more: show up to minutes - if (mini) { min_str + "m" } else { str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + " UTC" } - } else { - // Less than a minute: show full precision - if (mini) { s_str + "s" } else { str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" } - } - } - } else { - str(ts) - } - } - - - heading(level: 1)[GNU Taler Merchant Accounting: #data.business_name] - - [Transaction report from - #underline([#format_round_timestamp(data.start_date,data.bucket_period.d_us,false)]) to - #underline([#format_round_timestamp(data.end_date,data.bucket_period.d_us,false)]) - with - #text(weight: "bold")[#format_timeframe(data.bucket_period.d_us)] - granularity. - ] - - let p = palette.new(colors: (blue, green, yellow, orange, purple, lime, teal, gray, red)) - v(1cm) - - for dchart in data.charts { - - heading(level: 2)[#dchart.chart_name] - - let data_groups = dchart.data_groups.map(dg => ( - values: dg.values, - start_date_str: format_round_timestamp(dg.start_date,data.bucket_period.d_us, false), - start_date_mini: format_round_timestamp(dg.start_date,data.bucket_period.d_us, true), - )) - - canvas(length: 1cm, { - import draw: * - - chart.columnchart( - size: (12, 8), - label-key: "start_date_mini", - value-key: "values", - x-label: [Time], - y-label: dchart.y_label, - x-tick-step: none, - y-tick-step: auto, - mode : if dchart.cumulative { "stacked" } else { "clustered" }, - // alternatives: "clustered" or "basic" or "stacked" - bar-style: p, - data_groups, - labels: dchart.labels, - legend: "east", - ) - }) - - v(1cm) - - let cols = (auto,) + dchart.labels.map(_ => auto) - if dchart.cumulative { - cols.push(auto) - } - - let aligns = (left,) + dchart.labels.map(_ => right) - if dchart.cumulative { - aligns.push(right) - } - - let header = ([*When*],) + dchart.labels.map(label => [*#label*]) - if dchart.cumulative { - header.push([*Total*]) - } - - let rows = data_groups.map(entry => { - let row = (entry.at("start_date_str"),) - row += entry.at("values").map(val => str(val)) - if dchart.cumulative { - row.push(str(entry.at("values").sum())) - } - row - }).flatten() - - align(center, - table( - columns: cols, - align: aligns, - ..header, - ..rows - )) - v(1cm) - - } // For all charts - -} - -// Example usage: -#form(( - business_name: "Example.com", - start_date: (t_s: 1764967786), - end_date: (t_s: 1767222000), - bucket_period: (d_us: 86400000000), - charts: ( - (chart_name: "Transaction volume", - y_label: "Volume", - data_groups: ( - (start_date: (t_s: 1766790000), - values: (10, 20, 30)), - (start_date: (t_s: 1766876400), - values: (10, 20, 30)), - (start_date: (t_s: 1766962800), - values: (10, 20, 30)), - (start_date: (t_s: 1767049200), - values: (10, 20, 30)), - (start_date: (t_s: 1767135600), - values: (10, 20, 30)), - ), - labels: ([EUR],[CHF],[HUF]), - cumulative: false, - ), - (chart_name: "Transaction rate", - y_label: "Rate", - data_groups: ( - (start_date: (t_s: 1764967786), - values: (10, 20, 30)), - (start_date: (t_s: 1766876400), - values: (10, 20, 30)), - (start_date: (t_s: 1766962800), - values: (10, 20, 30)), - (start_date: (t_s: 1767049200), - values: (10, 20, 30)), - (start_date: (t_s: 1767135600), - values: (10, 20, 30)), - ), - labels: ([Claimed],[Paid],[Settled]), - cumulative: true, - ), - ), -)) -\ No newline at end of file diff --git a/contrib/typst/transactions/template/main.typ b/contrib/typst/transactions/template/main.typ @@ -0,0 +1,44 @@ +#import "@taler-merchant/transactions:0.0.0": form + +#form(( + business_name: "Example.com", + start_date: (t_s: 1764967786), + end_date: (t_s: 1767222000), + bucket_period: (d_us: 86400000000), + charts: ( + (chart_name: "Transaction volume", + y_label: "Volume", + data_groups: ( + (start_date: (t_s: 1766790000), + values: (10, 20, 30)), + (start_date: (t_s: 1766876400), + values: (10, 20, 30)), + (start_date: (t_s: 1766962800), + values: (10, 20, 30)), + (start_date: (t_s: 1767049200), + values: (10, 20, 30)), + (start_date: (t_s: 1767135600), + values: (10, 20, 30)), + ), + labels: ([EUR],[CHF],[HUF]), + cumulative: false, + ), + (chart_name: "Transaction rate", + y_label: "Rate", + data_groups: ( + (start_date: (t_s: 1764967786), + values: (10, 20, 30)), + (start_date: (t_s: 1766876400), + values: (10, 20, 30)), + (start_date: (t_s: 1766962800), + values: (10, 20, 30)), + (start_date: (t_s: 1767049200), + values: (10, 20, 30)), + (start_date: (t_s: 1767135600), + values: (10, 20, 30)), + ), + labels: ([Claimed],[Paid],[Settled]), + cumulative: true, + ), + ), +)) +\ No newline at end of file diff --git a/contrib/typst/transactions/transactions.typ b/contrib/typst/transactions/transactions.typ @@ -0,0 +1,101 @@ +#import "@preview/cetz:0.4.2": canvas, draw, palette +#import "@preview/cetz-plot:0.1.3": plot, chart +#import "@taler-merchant/common:0.0.0": format_timeframe, format_timestamp, format_round_timestamp + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + heading(level: 1)[GNU Taler Merchant Accounting: #data.business_name] + + [Transaction report from + #underline([#format_round_timestamp(data.start_date,data.bucket_period.d_us,false)]) to + #underline([#format_round_timestamp(data.end_date,data.bucket_period.d_us,false)]) + with + #text(weight: "bold")[#format_timeframe(data.bucket_period.d_us)] + granularity. + ] + + let p = palette.new(colors: (blue, green, yellow, orange, purple, lime, teal, gray, red)) + v(1cm) + + for dchart in data.charts { + + heading(level: 2)[#dchart.chart_name] + + let data_groups = dchart.data_groups.map(dg => ( + values: dg.values, + start_date_str: format_round_timestamp(dg.start_date,data.bucket_period.d_us, false), + start_date_mini: format_round_timestamp(dg.start_date,data.bucket_period.d_us, true), + )) + + canvas(length: 1cm, { + import draw: * + + chart.columnchart( + size: (12, 8), + label-key: "start_date_mini", + value-key: "values", + x-label: [Time], + y-label: dchart.y_label, + x-tick-step: none, + y-tick-step: auto, + mode : if dchart.cumulative { "stacked" } else { "clustered" }, + // alternatives: "clustered" or "basic" or "stacked" + bar-style: p, + data_groups, + labels: dchart.labels, + legend: "east", + ) + }) + + v(1cm) + + let cols = (auto,) + dchart.labels.map(_ => auto) + if dchart.cumulative { + cols.push(auto) + } + + let aligns = (left,) + dchart.labels.map(_ => right) + if dchart.cumulative { + aligns.push(right) + } + + let header = ([*When*],) + dchart.labels.map(label => [*#label*]) + if dchart.cumulative { + header.push([*Total*]) + } + + let rows = data_groups.map(entry => { + let row = (entry.at("start_date_str"),) + row += entry.at("values").map(val => str(val)) + if dchart.cumulative { + row.push(str(entry.at("values").sum())) + } + row + }).flatten() + + align(center, + table( + columns: cols, + align: aligns, + ..header, + ..rows + )) + v(1cm) + + } // For all charts + +} diff --git a/contrib/typst/transactions/typst.toml b/contrib/typst/transactions/typst.toml @@ -0,0 +1,9 @@ +[package] +name = "transactions" +version = "0.0.0" +entrypoint = "transactions.typ" +authors = ["Christian Grothoff <https://grothoff.org/christian/>"] +license = "GPLv3+" +description = "Transaction overview for GNU Taler merchant PDF generation" +repository = "git://git.taler.net/exchange" +keywords = ["accounting"] diff --git a/debian/control b/debian/control @@ -68,6 +68,16 @@ Description: GNU's payment system merchant backend. payments using GNU Taler. This package provides the merchant backend. +Package: taler-merchant-typst +Architecture: any +Recommends: + pdftk +Description: Typst packages for GNU Taler merchant. + . + This package contains Typst packages used by the + merchant for PDF generation. It should be installed + alongside Typst and pdftk for PDF generation. + Package: libtalermerchant-dev Section: libdevel Architecture: any diff --git a/debian/taler-merchant-typst.install b/debian/taler-merchant-typst.install @@ -0,0 +1,4 @@ +# The local Typst packages must actually be in the $HOME +# of the user, not in the PREFIX. Move them to the right +# location. +usr/.local/share/typst/packages/taler-merchant/* /var/lib/taler-merchant/.local/share/typst/packages/taler-merchant/ diff --git a/debian/taler-merchant.postinst b/debian/taler-merchant.postinst @@ -2,46 +2,38 @@ set -e -MARKER="/run/taler/merchant.was-enabled" +MARKER="/run/taler-merchant/.was-enabled" -if [ -d /run/systemd/system ]; then +SERVICES="taler-merchant-depositcheck taler-merchant-exchangekeyupdate taler-merchant-donaukeyupdate taler-merchant-httpd taler-merchant-kychcekc taler-merchant-reconciliation taler-merchant-report-generator taler-merchant-webhook taler-merchant-wirewatch" + +if [ -d /run/systemd/system ]; +then systemctl --system daemon-reload >/dev/null || true fi -if [ "$1" = "remove" ]; then - if [ -x "/usr/bin/deb-systemd-helper" ]; then - deb-systemd-helper mask 'taler-merchant-depositcheck.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-exchangekeyupdate.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-donaukeyupdate.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-httpd.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-kyccheck.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-reconciliation.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-webhook.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant-wirewatch.service' >/dev/null || true - deb-systemd-helper mask 'taler-merchant.target' >/dev/null || true - fi +if [ "$1" = "remove" ]; +then + if [ -x "/usr/bin/deb-systemd-helper" ]; + then + for SERVICE in "$SERVICES" + do + deb-systemd-helper mask "${SERVICE}.service" >/dev/null || true + done + deb-systemd-helper mask 'taler-merchant.target' >/dev/null || true + fi fi -if [ "$1" = "purge" ]; then - if [ -x "/usr/bin/deb-systemd-helper" ]; then - deb-systemd-helper purge 'taler-merchant-depositcheck.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-depositcheck.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-donaukeyupdate.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-donaukeyupdate.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-exchangekeyupdate.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-exchangekeyupdate.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-httpd.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-httpd.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-kyccheck.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-kyccheck.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-reconciliation.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-reconciliation.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-webhook.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-webhook.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant-wirewatch.service' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant-wirewatch.service' >/dev/null || true - deb-systemd-helper purge 'taler-merchant.target' >/dev/null || true - deb-systemd-helper unmask 'taler-merchant.target' >/dev/null || true - fi +if [ "$1" = "purge" ]; +then + if [ -x "/usr/bin/deb-systemd-helper" ]; + then + for SERVICE in "$SERVICES" + do + deb-systemd-helper purge "${SERVICE}.service" >/dev/null || true + deb-systemd-helper unmask "${SERVICE}.service" >/dev/null || true + done + deb-systemd-helper purge 'taler-merchant.target' >/dev/null || true + deb-systemd-helper unmask 'taler-merchant.target' >/dev/null || true + fi fi TALER_HOME="/var/lib/taler-merchant" @@ -52,22 +44,33 @@ case "${1}" in configure) # Creating taler users if needed - if ! getent passwd taler-merchant-httpd >/dev/null; then - adduser --quiet --system --ingroup www-data --no-create-home --home ${TALER_HOME} taler-merchant-httpd + if ! getent passwd taler-merchant-httpd >/dev/null; + then + adduser \ + --quiet \ + --system \ + --ingroup www-data \ + --no-create-home \ + --home ${TALER_HOME} \ + taler-merchant-httpd fi if ! dpkg-statoverride --list /etc/taler-merchant/secrets/merchant-db.secret.conf >/dev/null 2>&1 then - dpkg-statoverride --add --update \ + dpkg-statoverride \ + --add \ + --update \ taler-merchant-httpd root 460 \ /etc/taler-merchant/secrets/merchant-db.secret.conf fi - if [ -x /usr/bin/taler-merchant-dbinit ]; then + if [ -x /usr/bin/taler-merchant-dbinit ]; + then /usr/bin/taler-merchant-dbinit >/dev/null 2>&1 || true fi - if [ -f "$MARKER" ] && grep -q "enabled" "$MARKER"; then + if [ -f "$MARKER" ] && grep -q "enabled" "$MARKER"; + then echo "taler-merchant-httpd was previously enabled, running DB config." systemctl enable --now taler-merchant.target || true diff --git a/debian/taler-merchant.prerm b/debian/taler-merchant.prerm @@ -1,14 +1,16 @@ #!/bin/sh set -e -MARKER="/run/taler/merchant.was-enabled" +MARKER="/run/taler-merchant/.was-enabled" -if [ -d /run/systemd/system ]; then +if [ -d /run/systemd/system ]; +then case "$1" in remove|upgrade|deconfigure) - if systemctl is-enabled --quiet taler-merchant.target; then + if systemctl is-enabled --quiet taler-merchant.target; + then echo "taler-merchant.target was enabled before $1." - mkdir -p /run/taler + mkdir -p /run/taler-merchant echo enabled > "$MARKER" systemctl disable --now taler-merchant.target || true fi diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -81,19 +81,20 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_contract.c taler-merchant-httpd_contract.h \ taler-merchant-httpd_dispatcher.c \ taler-merchant-httpd_dispatcher.h \ - taler-merchant-httpd_exchanges.c taler-merchant-httpd_exchanges.h \ - taler-merchant-httpd_get-orders-ID.c \ - taler-merchant-httpd_get-orders-ID.h \ - taler-merchant-httpd_get-sessions-ID.c \ - taler-merchant-httpd_get-sessions-ID.h \ - taler-merchant-httpd_get-products-HASH-image.c \ - taler-merchant-httpd_get-products-HASH-image.h \ + taler-merchant-httpd_exchanges.c \ + taler-merchant-httpd_exchanges.h \ + taler-merchant-httpd_get-orders-ORDER_ID.c \ + taler-merchant-httpd_get-orders-ORDER_ID.h \ + taler-merchant-httpd_get-sessions-SESSION_ID.c \ + taler-merchant-httpd_get-sessions-SESSION_ID.h \ + taler-merchant-httpd_get-products-IMAGE_HASH-image.c \ + taler-merchant-httpd_get-products-IMAGE_HASH-image.h \ taler-merchant-httpd_get-config.c \ taler-merchant-httpd_get-config.h \ taler-merchant-httpd_get-exchanges.c \ taler-merchant-httpd_get-exchanges.h \ - taler-merchant-httpd_get-templates-ID.c \ - taler-merchant-httpd_get-templates-ID.h \ + taler-merchant-httpd_get-templates-TEMPLATE_ID.c \ + taler-merchant-httpd_get-templates-TEMPLATE_ID.h \ taler-merchant-httpd_helper.c \ taler-merchant-httpd_helper.h \ taler-merchant-httpd_mhd.c \ @@ -102,190 +103,190 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_get-terms.h \ taler-merchant-httpd_mfa.c \ taler-merchant-httpd_mfa.h \ - taler-merchant-httpd_private-delete-account-ID.c \ - taler-merchant-httpd_private-delete-account-ID.h \ - taler-merchant-httpd_private-delete-categories-ID.c \ - taler-merchant-httpd_private-delete-categories-ID.h \ - taler-merchant-httpd_private-delete-units-ID.c \ - taler-merchant-httpd_private-delete-units-ID.h \ - taler-merchant-httpd_private-delete-instances-ID.c \ - taler-merchant-httpd_private-delete-instances-ID.h \ - taler-merchant-httpd_private-delete-instances-ID-token.c \ - taler-merchant-httpd_private-delete-instances-ID-token.h \ - taler-merchant-httpd_private-delete-products-ID.c \ - taler-merchant-httpd_private-delete-products-ID.h \ - taler-merchant-httpd_private-delete-orders-ID.c \ - taler-merchant-httpd_private-delete-orders-ID.h \ - taler-merchant-httpd_private-delete-otp-devices-ID.c \ - taler-merchant-httpd_private-delete-otp-devices-ID.h \ - taler-merchant-httpd_private-delete-templates-ID.c \ - taler-merchant-httpd_private-delete-templates-ID.h \ - taler-merchant-httpd_private-delete-token-families-SLUG.c \ - taler-merchant-httpd_private-delete-token-families-SLUG.h \ - taler-merchant-httpd_private-delete-transfers-ID.c \ - taler-merchant-httpd_private-delete-transfers-ID.h \ - taler-merchant-httpd_private-delete-webhooks-ID.c \ - taler-merchant-httpd_private-delete-webhooks-ID.h \ - taler-merchant-httpd_private-get-accounts.c \ - taler-merchant-httpd_private-get-accounts.h \ - taler-merchant-httpd_private-get-accounts-ID.c \ - taler-merchant-httpd_private-get-accounts-ID.h \ - taler-merchant-httpd_private-get-categories.c \ - taler-merchant-httpd_private-get-categories.h \ - taler-merchant-httpd_private-get-units.c \ - taler-merchant-httpd_private-get-units.h \ - taler-merchant-httpd_private-get-categories-ID.c \ - taler-merchant-httpd_private-get-categories-ID.h \ - taler-merchant-httpd_private-get-units-ID.c \ - taler-merchant-httpd_private-get-units-ID.h \ - taler-merchant-httpd_private-get-instances.c \ - taler-merchant-httpd_private-get-instances.h \ - taler-merchant-httpd_private-get-instances-ID.c \ - taler-merchant-httpd_private-get-instances-ID.h \ - taler-merchant-httpd_private-get-instances-ID-kyc.c \ - taler-merchant-httpd_private-get-instances-ID-kyc.h \ - taler-merchant-httpd_private-get-instances-ID-tokens.c \ - taler-merchant-httpd_private-get-instances-ID-tokens.h \ - taler-merchant-httpd_private-get-pos.c \ - taler-merchant-httpd_private-get-pos.h \ - taler-merchant-httpd_private-get-products.c \ - taler-merchant-httpd_private-get-products.h \ - taler-merchant-httpd_private-get-products-ID.c \ - taler-merchant-httpd_private-get-products-ID.h \ - taler-merchant-httpd_private-get-orders.c \ - taler-merchant-httpd_private-get-orders.h \ - taler-merchant-httpd_private-get-orders-ID.c \ - taler-merchant-httpd_private-get-orders-ID.h \ - taler-merchant-httpd_private-get-otp-devices.c \ - taler-merchant-httpd_private-get-otp-devices.h \ - taler-merchant-httpd_private-get-otp-devices-ID.c \ - taler-merchant-httpd_private-get-otp-devices-ID.h \ - taler-merchant-httpd_private-get-incoming.c \ - taler-merchant-httpd_private-get-incoming.h \ - taler-merchant-httpd_private-get-incoming-ID.c \ - taler-merchant-httpd_private-get-incoming-ID.h \ - taler-merchant-httpd_private-get-transfers.c \ - taler-merchant-httpd_private-get-transfers.h \ - taler-merchant-httpd_private-get-templates.c \ - taler-merchant-httpd_private-get-templates.h \ - taler-merchant-httpd_private-get-templates-ID.c \ - taler-merchant-httpd_private-get-templates-ID.h \ - taler-merchant-httpd_private-get-token-families.c \ - taler-merchant-httpd_private-get-token-families.h \ - taler-merchant-httpd_private-get-token-families-SLUG.c \ - taler-merchant-httpd_private-get-token-families-SLUG.h \ - taler-merchant-httpd_private-get-webhooks.c \ - taler-merchant-httpd_private-get-webhooks.h \ - taler-merchant-httpd_private-get-webhooks-ID.c \ - taler-merchant-httpd_private-get-webhooks-ID.h \ - taler-merchant-httpd_private-patch-accounts-ID.c \ - taler-merchant-httpd_private-patch-accounts-ID.h \ - taler-merchant-httpd_private-patch-categories-ID.c \ - taler-merchant-httpd_private-patch-categories-ID.h \ - taler-merchant-httpd_private-patch-units-ID.c \ - taler-merchant-httpd_private-patch-units-ID.h \ - taler-merchant-httpd_private-patch-instances-ID.c \ - taler-merchant-httpd_private-patch-instances-ID.h \ - taler-merchant-httpd_private-patch-orders-ID-forget.c \ - taler-merchant-httpd_private-patch-orders-ID-forget.h \ - taler-merchant-httpd_private-patch-otp-devices-ID.c \ - taler-merchant-httpd_private-patch-otp-devices-ID.h \ - taler-merchant-httpd_private-patch-products-ID.c \ - taler-merchant-httpd_private-patch-products-ID.h \ - taler-merchant-httpd_private-patch-templates-ID.c \ - taler-merchant-httpd_private-patch-templates-ID.h \ - taler-merchant-httpd_private-patch-token-families-SLUG.c \ - taler-merchant-httpd_private-patch-token-families-SLUG.h \ - taler-merchant-httpd_private-patch-webhooks-ID.c \ - taler-merchant-httpd_private-patch-webhooks-ID.h \ - taler-merchant-httpd_private-post-account.c \ - taler-merchant-httpd_private-post-account.h \ - taler-merchant-httpd_private-post-categories.c \ - taler-merchant-httpd_private-post-categories.h \ - taler-merchant-httpd_private-post-units.c \ - taler-merchant-httpd_private-post-units.h \ - taler-merchant-httpd_private-post-instances.c \ - taler-merchant-httpd_private-post-instances.h \ - taler-merchant-httpd_private-post-instances-ID-auth.c \ - taler-merchant-httpd_private-post-instances-ID-auth.h \ - taler-merchant-httpd_private-post-instances-ID-token.c \ - taler-merchant-httpd_private-post-instances-ID-token.h \ - taler-merchant-httpd_private-post-orders-ID-refund.c \ - taler-merchant-httpd_private-post-orders-ID-refund.h \ - taler-merchant-httpd_private-post-orders.c \ - taler-merchant-httpd_private-post-orders.h \ - taler-merchant-httpd_private-post-products.c \ - taler-merchant-httpd_private-post-products.h \ - taler-merchant-httpd_private-post-otp-devices.c \ - taler-merchant-httpd_private-post-otp-devices.h \ - taler-merchant-httpd_private-post-products-ID-lock.c \ - taler-merchant-httpd_private-post-products-ID-lock.h \ - taler-merchant-httpd_private-post-templates.c \ - taler-merchant-httpd_private-post-templates.h \ - taler-merchant-httpd_private-post-token-families.c \ - taler-merchant-httpd_private-post-token-families.h \ - taler-merchant-httpd_private-post-transfers.c \ - taler-merchant-httpd_private-post-transfers.h \ - taler-merchant-httpd_private-post-webhooks.c \ - taler-merchant-httpd_private-post-webhooks.h \ + taler-merchant-httpd_delete-private-accounts-H_WIRE.c \ + taler-merchant-httpd_delete-private-accounts-H_WIRE.h \ + taler-merchant-httpd_delete-private-categories-CATEGORY_ID.c \ + taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h \ + taler-merchant-httpd_delete-private-units-UNIT.c \ + taler-merchant-httpd_delete-private-units-UNIT.h \ + taler-merchant-httpd_delete-management-instances-INSTANCE.c \ + taler-merchant-httpd_delete-management-instances-INSTANCE.h \ + taler-merchant-httpd_delete-private-tokens-SERIAL.c \ + taler-merchant-httpd_delete-private-tokens-SERIAL.h \ + taler-merchant-httpd_delete-private-products-PRODUCT_ID.c \ + taler-merchant-httpd_delete-private-products-PRODUCT_ID.h \ + taler-merchant-httpd_delete-private-orders-ORDER_ID.c \ + taler-merchant-httpd_delete-private-orders-ORDER_ID.h \ + taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.c \ + taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h \ + taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.c \ + taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h \ + taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.c \ + taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h \ + taler-merchant-httpd_delete-private-transfers-TID.c \ + taler-merchant-httpd_delete-private-transfers-TID.h \ + taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.c \ + taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h \ + taler-merchant-httpd_get-private-accounts.c \ + taler-merchant-httpd_get-private-accounts.h \ + taler-merchant-httpd_get-private-accounts-H_WIRE.c \ + taler-merchant-httpd_get-private-accounts-H_WIRE.h \ + taler-merchant-httpd_get-private-categories.c \ + taler-merchant-httpd_get-private-categories.h \ + taler-merchant-httpd_get-private-units.c \ + taler-merchant-httpd_get-private-units.h \ + taler-merchant-httpd_get-private-categories-CATEGORY_ID.c \ + taler-merchant-httpd_get-private-categories-CATEGORY_ID.h \ + taler-merchant-httpd_get-private-units-UNIT.c \ + taler-merchant-httpd_get-private-units-UNIT.h \ + taler-merchant-httpd_get-management-instances.c \ + taler-merchant-httpd_get-management-instances.h \ + taler-merchant-httpd_get-management-instances-INSTANCE.c \ + taler-merchant-httpd_get-management-instances-INSTANCE.h \ + taler-merchant-httpd_get-private-kyc.c \ + taler-merchant-httpd_get-private-kyc.h \ + taler-merchant-httpd_get-private-tokens.c \ + taler-merchant-httpd_get-private-tokens.h \ + taler-merchant-httpd_get-private-pos.c \ + taler-merchant-httpd_get-private-pos.h \ + taler-merchant-httpd_get-private-products.c \ + taler-merchant-httpd_get-private-products.h \ + taler-merchant-httpd_get-private-products-PRODUCT_ID.c \ + taler-merchant-httpd_get-private-products-PRODUCT_ID.h \ + taler-merchant-httpd_get-private-orders.c \ + taler-merchant-httpd_get-private-orders.h \ + taler-merchant-httpd_get-private-orders-ORDER_ID.c \ + taler-merchant-httpd_get-private-orders-ORDER_ID.h \ + taler-merchant-httpd_get-private-otp-devices.c \ + taler-merchant-httpd_get-private-otp-devices.h \ + taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.c \ + taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h \ + taler-merchant-httpd_get-private-incoming.c \ + taler-merchant-httpd_get-private-incoming.h \ + taler-merchant-httpd_get-private-incoming-ID.c \ + taler-merchant-httpd_get-private-incoming-ID.h \ + taler-merchant-httpd_get-private-transfers.c \ + taler-merchant-httpd_get-private-transfers.h \ + taler-merchant-httpd_get-private-templates.c \ + taler-merchant-httpd_get-private-templates.h \ + taler-merchant-httpd_get-private-templates-TEMPLATE_ID.c \ + taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h \ + taler-merchant-httpd_get-private-tokenfamilies.c \ + taler-merchant-httpd_get-private-tokenfamilies.h \ + taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.c \ + taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h \ + taler-merchant-httpd_get-private-webhooks.c \ + taler-merchant-httpd_get-private-webhooks.h \ + taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.c \ + taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h \ + taler-merchant-httpd_patch-private-accounts-H_WIRE.c \ + taler-merchant-httpd_patch-private-accounts-H_WIRE.h \ + taler-merchant-httpd_patch-private-categories-CATEGORY_ID.c \ + taler-merchant-httpd_patch-private-categories-CATEGORY_ID.h \ + taler-merchant-httpd_patch-private-units-UNIT.c \ + taler-merchant-httpd_patch-private-units-UNIT.h \ + taler-merchant-httpd_patch-management-instances-INSTANCE.c \ + taler-merchant-httpd_patch-management-instances-INSTANCE.h \ + taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.c \ + taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.h \ + taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.c \ + taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.h \ + taler-merchant-httpd_patch-private-products-PRODUCT_ID.c \ + taler-merchant-httpd_patch-private-products-PRODUCT_ID.h \ + taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.c \ + taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.h \ + taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.c \ + taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h \ + taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.c \ + taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.h \ + taler-merchant-httpd_post-private-accounts.c \ + taler-merchant-httpd_post-private-accounts.h \ + taler-merchant-httpd_post-private-categories.c \ + taler-merchant-httpd_post-private-categories.h \ + taler-merchant-httpd_post-private-units.c \ + taler-merchant-httpd_post-private-units.h \ + taler-merchant-httpd_post-management-instances.c \ + taler-merchant-httpd_post-management-instances.h \ + taler-merchant-httpd_post-management-instances-INSTANCE-auth.c \ + taler-merchant-httpd_post-management-instances-INSTANCE-auth.h \ + taler-merchant-httpd_post-private-token.c \ + taler-merchant-httpd_post-private-token.h \ + taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c \ + taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h \ + taler-merchant-httpd_post-private-orders.c \ + taler-merchant-httpd_post-private-orders.h \ + taler-merchant-httpd_post-private-products.c \ + taler-merchant-httpd_post-private-products.h \ + taler-merchant-httpd_post-private-otp-devices.c \ + taler-merchant-httpd_post-private-otp-devices.h \ + taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.c \ + taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.h \ + taler-merchant-httpd_post-private-templates.c \ + taler-merchant-httpd_post-private-templates.h \ + taler-merchant-httpd_post-private-tokenfamilies.c \ + taler-merchant-httpd_post-private-tokenfamilies.h \ + taler-merchant-httpd_post-private-transfers.c \ + taler-merchant-httpd_post-private-transfers.h \ + taler-merchant-httpd_post-private-webhooks.c \ + taler-merchant-httpd_post-private-webhooks.h \ taler-merchant-httpd_post-challenge-ID.c \ taler-merchant-httpd_post-challenge-ID.h \ taler-merchant-httpd_post-challenge-ID-confirm.c \ taler-merchant-httpd_post-challenge-ID-confirm.h \ - taler-merchant-httpd_post-orders-ID-abort.c \ - taler-merchant-httpd_post-orders-ID-abort.h \ - taler-merchant-httpd_post-orders-ID-claim.c \ - taler-merchant-httpd_post-orders-ID-claim.h \ - taler-merchant-httpd_post-orders-ID-pay.c \ - taler-merchant-httpd_post-orders-ID-pay.h \ - taler-merchant-httpd_post-orders-ID-paid.c \ - taler-merchant-httpd_post-orders-ID-paid.h \ - taler-merchant-httpd_post-orders-ID-refund.c \ - taler-merchant-httpd_post-orders-ID-refund.h \ - taler-merchant-httpd_post-orders-ID-unclaim.c \ - taler-merchant-httpd_post-orders-ID-unclaim.h \ - taler-merchant-httpd_post-templates-ID.c \ - taler-merchant-httpd_post-templates-ID.h \ - taler-merchant-httpd_post-reports-ID.c \ - taler-merchant-httpd_post-reports-ID.h \ - taler-merchant-httpd_private-get-statistics-amount-SLUG.c \ - taler-merchant-httpd_private-get-statistics-amount-SLUG.h \ - taler-merchant-httpd_private-get-statistics-counter-SLUG.c \ - taler-merchant-httpd_private-get-statistics-counter-SLUG.h \ - taler-merchant-httpd_private-get-statistics-report-transactions.c \ - taler-merchant-httpd_private-get-statistics-report-transactions.h \ + taler-merchant-httpd_post-orders-ORDER_ID-abort.c \ + taler-merchant-httpd_post-orders-ORDER_ID-abort.h \ + taler-merchant-httpd_post-orders-ORDER_ID-claim.c \ + taler-merchant-httpd_post-orders-ORDER_ID-claim.h \ + taler-merchant-httpd_post-orders-ORDER_ID-pay.c \ + taler-merchant-httpd_post-orders-ORDER_ID-pay.h \ + taler-merchant-httpd_post-orders-ORDER_ID-paid.c \ + taler-merchant-httpd_post-orders-ORDER_ID-paid.h \ + taler-merchant-httpd_post-orders-ORDER_ID-refund.c \ + taler-merchant-httpd_post-orders-ORDER_ID-refund.h \ + taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c \ + taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h \ + taler-merchant-httpd_post-templates-TEMPLATE_ID.c \ + taler-merchant-httpd_post-templates-TEMPLATE_ID.h \ + taler-merchant-httpd_post-reports-REPORT_ID.c \ + taler-merchant-httpd_post-reports-REPORT_ID.h \ + taler-merchant-httpd_get-private-statistics-amount-SLUG.c \ + taler-merchant-httpd_get-private-statistics-amount-SLUG.h \ + taler-merchant-httpd_get-private-statistics-counter-SLUG.c \ + taler-merchant-httpd_get-private-statistics-counter-SLUG.h \ + taler-merchant-httpd_get-private-statistics-report-transactions.c \ + taler-merchant-httpd_get-private-statistics-report-transactions.h \ taler-merchant-httpd_qr.c \ taler-merchant-httpd_qr.h \ taler-merchant-httpd_get-webui.c \ taler-merchant-httpd_get-webui.h \ - taler-merchant-httpd_private-delete-report-ID.c \ - taler-merchant-httpd_private-delete-report-ID.h \ - taler-merchant-httpd_private-get-report-ID.c \ - taler-merchant-httpd_private-get-report-ID.h \ - taler-merchant-httpd_private-get-reports.c \ - taler-merchant-httpd_private-get-reports.h \ - taler-merchant-httpd_private-patch-report-ID.c \ - 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 \ - taler-merchant-httpd_private-get-groups.h \ - taler-merchant-httpd_private-patch-group-ID.c \ - taler-merchant-httpd_private-patch-group-ID.h \ - taler-merchant-httpd_private-post-groups.c \ - taler-merchant-httpd_private-post-groups.h \ + taler-merchant-httpd_delete-private-reports-REPORT_ID.c \ + taler-merchant-httpd_delete-private-reports-REPORT_ID.h \ + taler-merchant-httpd_get-private-reports-REPORT_ID.c \ + taler-merchant-httpd_get-private-reports-REPORT_ID.h \ + taler-merchant-httpd_get-private-reports.c \ + taler-merchant-httpd_get-private-reports.h \ + taler-merchant-httpd_patch-private-reports-REPORT_ID.c \ + taler-merchant-httpd_patch-private-reports-REPORT_ID.h \ + taler-merchant-httpd_post-private-reports.c \ + taler-merchant-httpd_post-private-reports.h \ + taler-merchant-httpd_delete-private-pots-POT_ID.c \ + taler-merchant-httpd_delete-private-pots-POT_ID.h \ + taler-merchant-httpd_get-private-pots-POT_ID.c \ + taler-merchant-httpd_get-private-pots-POT_ID.h \ + taler-merchant-httpd_get-private-pots.c \ + taler-merchant-httpd_get-private-pots.h \ + taler-merchant-httpd_patch-private-pots-POT_ID.c \ + taler-merchant-httpd_patch-private-pots-POT_ID.h \ + taler-merchant-httpd_post-private-pots.c \ + taler-merchant-httpd_post-private-pots.h \ + taler-merchant-httpd_delete-private-groups-GROUP_ID.c \ + taler-merchant-httpd_delete-private-groups-GROUP_ID.h \ + taler-merchant-httpd_get-private-groups.c \ + taler-merchant-httpd_get-private-groups.h \ + taler-merchant-httpd_patch-private-groups-GROUP_ID.c \ + taler-merchant-httpd_patch-private-groups-GROUP_ID.h \ + taler-merchant-httpd_post-private-groups.c \ + taler-merchant-httpd_post-private-groups.h \ taler-merchant-httpd_statics.c \ taler-merchant-httpd_statics.h @@ -315,12 +316,12 @@ taler_merchant_httpd_LDADD += \ -ldonaujson taler_merchant_httpd_SOURCES += \ - taler-merchant-httpd_private-get-donau-instances.c \ - taler-merchant-httpd_private-get-donau-instances.h \ - taler-merchant-httpd_private-post-donau-instance.c \ - taler-merchant-httpd_private-post-donau-instance.h \ - taler-merchant-httpd_private-delete-donau-instance-ID.c \ - taler-merchant-httpd_private-delete-donau-instance-ID.h + taler-merchant-httpd_get-private-donau.c \ + taler-merchant-httpd_get-private-donau.h \ + taler-merchant-httpd_post-private-donau.c \ + taler-merchant-httpd_post-private-donau.h \ + taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.c \ + taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h endif taler_merchant_httpd_CFLAGS = \ diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -61,7 +61,7 @@ DEFAULT_WIRE_TRANSFER_DELAY = 1 week DEFAULT_PAY_DELAY = 1 day # Where are the Typst templates for form rendering. -TYPST_TEMPLATES = ${DATADIR}typst-forms/ +TYPST_TEMPLATES = @taler-merchant [merchant-kyccheck] diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -31,24 +31,25 @@ #include "taler/taler_merchant_util.h" #include "taler-merchant-httpd_auth.h" #include "taler-merchant-httpd_dispatcher.h" +#include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_mfa.h" -#include "taler-merchant-httpd_private-post-orders.h" -#include "taler-merchant-httpd_post-orders-ID-abort.h" +#include "taler-merchant-httpd_post-private-orders.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-abort.h" #include "taler-merchant-httpd_post-challenge-ID.h" -#include "taler-merchant-httpd_get-orders-ID.h" -#include "taler-merchant-httpd_get-sessions-ID.h" -#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_get-orders-ORDER_ID.h" +#include "taler-merchant-httpd_get-sessions-SESSION_ID.h" +#include "taler-merchant-httpd_get-exchanges.h" #include "taler-merchant-httpd_get-webui.h" #include "taler-merchant-httpd_get-terms.h" -#include "taler-merchant-httpd_private-get-instances-ID-kyc.h" -#include "taler-merchant-httpd_private-get-statistics-report-transactions.h" -#include "taler-merchant-httpd_private-post-donau-instance.h" -#include "taler-merchant-httpd_private-get-orders-ID.h" -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler-merchant-httpd_post-orders-ID-pay.h" -#include "taler-merchant-httpd_post-orders-ID-refund.h" +#include "taler-merchant-httpd_get-private-kyc.h" +#include "taler-merchant-httpd_get-private-statistics-report-transactions.h" +#include "taler-merchant-httpd_post-private-donau.h" +#include "taler-merchant-httpd_get-private-orders-ORDER_ID.h" +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-pay.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-refund.h" /** diff --git a/src/backend/taler-merchant-httpd_delete-management-instances-INSTANCE.c b/src/backend/taler-merchant-httpd_delete-management-instances-INSTANCE.c @@ -0,0 +1,163 @@ +/* + This file is part of TALER + (C) 2020-2026 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-httpd_delete-management-instances-INSTANCE.c + * @brief implement DELETE /instances/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-management-instances-INSTANCE.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include "taler-merchant-httpd_mfa.h" + +/** + * Handle a DELETE "/instances/$ID" request. + * + * @param[in,out] hc http request context + * @param mfa_check true if a MFA check is required + * @param mi instance to delete + * @param connection the MHD connection to handle + * @return MHD result code + */ +static MHD_RESULT +delete_instances_ID (struct TMH_HandlerContext *hc, + bool mfa_check, + struct TMH_MerchantInstance *mi, + struct MHD_Connection *connection) +{ + bool purge; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + if (mfa_check) + { + enum GNUNET_GenericReturnValue ret = + TMH_mfa_check_simple (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_DELETION, + mi); + + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + { + const char *purge_s; + + purge_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "purge"); + if (NULL == purge_s) + purge_s = "no"; + purge = (0 == strcasecmp (purge_s, + "yes")); + } + if (purge) + qs = TMH_db->purge_instance (TMH_db->cls, + mi->settings.id); + else + qs = TMH_db->delete_instance_private_key (TMH_db->cls, + mi->settings.id); + { + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + }; + + TMH_db->event_notify (TMH_db->cls, + &es, + NULL, + 0); + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete private key"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + purge + ? "Instance unknown" + : "Private key unknown"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + TMH_reload_instances (mi->settings.id); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +MHD_RESULT +TMH_private_delete_instances_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + (void) rh; + return delete_instances_ID (hc, + true, + mi, + connection); +} + + +MHD_RESULT +TMH_private_delete_instances_default_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi; + + (void) rh; + mi = TMH_lookup_instance (hc->infix); + if (NULL == mi) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); + } + return delete_instances_ID (hc, + false, + mi, + connection); +} + + +/* end of taler-merchant-httpd_delete-management-instances-INSTANCE.c */ diff --git a/src/backend/taler-merchant-httpd_delete-management-instances-INSTANCE.h b/src/backend/taler-merchant-httpd_delete-management-instances-INSTANCE.h @@ -0,0 +1,56 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_delete-management-instances-INSTANCE.h + * @brief implement DELETE /instances/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/instances/$ID/private" 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_instances_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Handle a DELETE "/management/instances/$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_instances_default_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/* end of taler-merchant-httpd_delete-management-instances-INSTANCE.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-accounts-H_WIRE.c b/src/backend/taler-merchant-httpd_delete-private-accounts-H_WIRE.c @@ -0,0 +1,94 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_delete-private-accounts-H_WIRE.c + * @brief implement DELETE /account/$H_WIRE + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-accounts-H_WIRE.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> + + +MHD_RESULT +TMH_private_delete_account_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MerchantWireHashP h_wire; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (hc->infix, + strlen (hc->infix), + &h_wire, + sizeof (h_wire))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "h_wire"); + } + GNUNET_assert (NULL != mi); + qs = TMH_db->inactivate_account (TMH_db->cls, + mi->settings.id, + &h_wire); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "inactivate_account"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT, + "account unknown"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + { + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + }; + + TMH_db->event_notify (TMH_db->cls, + &es, + NULL, + 0); + } + TMH_reload_instances (mi->settings.id); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_delete-private-accounts-H_WIRE.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-accounts-H_WIRE.h b/src/backend/taler-merchant-httpd_delete-private-accounts-H_WIRE.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_delete-private-accounts-H_WIRE.h + * @brief implement DELETE /account/$PAYTO + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ACCOUNT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ACCOUNT_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/private/account/$H_WIRE" 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_account_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/* end of taler-merchant-httpd_delete-private-accounts-H_WIRE.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-categories-CATEGORY_ID.c b/src/backend/taler-merchant-httpd_delete-private-categories-CATEGORY_ID.c @@ -0,0 +1,92 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_delete-private-categories-CATEGORY_ID.c + * @brief implement DELETE /private/categories/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/categories/$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_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long cnum; + char dummy; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &cnum, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "category_id must be a number"); + } + qs = TMH_db->delete_category (TMH_db->cls, + mi->settings.id, + cnum); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_category"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_category"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-categories-CATEGORY_ID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h b/src/backend/taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_delete-private-categories-CATEGORY_ID.h + * @brief implement DELETE /private/categories/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_CATEGORIES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_CATEGORIES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/categories/$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_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.c b/src/backend/taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.c @@ -0,0 +1,93 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_delete-private-donau-DONAU_SERIAL.c + * @brief implement DELETE /private/donau/$donau_serial_id + * @author Bohdan Potuzhnyi + * @author Vlada Svirsh + */ + +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> + +/** + * Handle a DELETE "/donau/$donau_serial_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_donau_instance_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + uint64_t donau_serial_id; + char dummy; + + GNUNET_assert (NULL != mi); + + if (1 != sscanf (hc->infix, + "%lu%c", + &donau_serial_id, + &dummy)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + hc->infix); + } + + qs = TMH_db->delete_donau_instance (TMH_db->cls, + hc->instance->settings.id, + donau_serial_id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_donau_instance"); + + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "delete_donau_instance (soft)"); + + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); + + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} +\ No newline at end of file diff --git a/src/backend/taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h b/src/backend/taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_delete-private-donau-DONAU_SERIAL.h + * @brief implement DELETE /private/donau/$charity_id/ + * @author Bohdan Potuzhnyi + * @author Vlada Svirsh + */ + +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_DONAU_INSTANCE_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_DONAU_INSTANCE_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/donau/$donau_serial_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_donau_instance_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-groups-GROUP_ID.c b/src/backend/taler-merchant-httpd_delete-private-groups-GROUP_ID.c @@ -0,0 +1,74 @@ +/* + 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 taler-merchant-httpd_delete-private-groups-GROUP_ID.c + * @brief implementation of DELETE /private/groups/$GROUP_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-groups-GROUP_ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_delete_group (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *group_id_str = hc->infix; + unsigned long long group_id; + enum GNUNET_DB_QueryStatus qs; + char dummy; + + (void) rh; + if (1 != sscanf (group_id_str, + "%llu%c", + &group_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "group_id"); + } + qs = TMH_db->delete_product_group (TMH_db->cls, + hc->instance->settings.id, + group_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_product_group"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, + group_id_str); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_delete-private-groups-GROUP_ID.h b/src/backend/taler-merchant-httpd_delete-private-groups-GROUP_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 taler-merchant-httpd_delete-private-groups-GROUP_ID.h + * @brief HTTP serving layer for deleting product groups + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_GROUP_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_GROUP_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Handle DELETE /private/groups/$GROUP_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_group (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-orders-ORDER_ID.c b/src/backend/taler-merchant-httpd_delete-private-orders-ORDER_ID.c @@ -0,0 +1,133 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_delete-private-orders-ORDER_ID.c + * @brief implement DELETE /orders/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-orders-ORDER_ID.h" +#include <stdint.h> +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/orders/$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_orders_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + const char *force_s; + bool force; + + (void) rh; + force_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "force"); + if (NULL == force_s) + force_s = "no"; + force = (0 == strcasecmp (force_s, + "yes")); + + GNUNET_assert (NULL != mi); + qs = TMH_db->delete_order (TMH_db->cls, + mi->settings.id, + hc->infix, + force); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + qs = TMH_db->delete_contract_terms (TMH_db->cls, + mi->settings.id, + hc->infix, + TMH_legal_expiration); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + struct TALER_MerchantPostDataHashP unused; + uint64_t order_serial; + bool paid = false; + bool wired = false; + bool matches = false; + int16_t choice_index; + + qs = TMH_db->lookup_order (TMH_db->cls, + mi->settings.id, + hc->infix, + NULL, + &unused, + NULL); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, + mi->settings.id, + hc->infix, + NULL, + NULL, + &order_serial, + &paid, + &wired, + &matches, + NULL, + &choice_index); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + hc->infix); + if (paid) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID, + hc->infix); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT, + hc->infix); + } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-orders-ORDER_ID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-orders-ORDER_ID.h b/src/backend/taler-merchant-httpd_delete-private-orders-ORDER_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_delete-private-orders-ORDER_ID.h + * @brief implement DELETE /orders/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ORDERS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ORDERS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/orders/$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_orders_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-orders-ORDER_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.c b/src/backend/taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.c @@ -0,0 +1,78 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_delete-private-otp-devices-DEVICE_ID.c + * @brief implement DELETE /otp-devices/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->delete_otp (TMH_db->cls, + mi->settings.id, + hc->infix); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_otp"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_otp (soft)"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h b/src/backend/taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_delete-private-otp-devices-DEVICE_ID.h + * @brief implement DELETE /otp-devices/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_OTP_DEVICES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_OTP_DEVICES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-pots-POT_ID.c b/src/backend/taler-merchant-httpd_delete-private-pots-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 taler-merchant-httpd_delete-private-pots-POT_ID.c + * @brief implementation of DELETE /private/pots/$POT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-pots-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_delete-private-pots-POT_ID.h b/src/backend/taler-merchant-httpd_delete-private-pots-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 taler-merchant-httpd_delete-private-pots-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_delete-private-products-PRODUCT_ID.c b/src/backend/taler-merchant-httpd_delete-private-products-PRODUCT_ID.c @@ -0,0 +1,103 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_delete-private-products-PRODUCT_ID.c + * @brief implement DELETE /products/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-products-PRODUCT_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/products/$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_products_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->delete_product (TMH_db->cls, + mi->settings.id, + hc->infix); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_product"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_product (soft)"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + size_t num_categories = 0; + uint64_t *categories = NULL; + + /* check if deletion must have failed because of locks by + checking if the product exists */ + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + hc->infix, + NULL, + &num_categories, + &categories); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "lookup_product"); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + hc->infix); + GNUNET_free (categories); + } + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-products-PRODUCT_ID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-products-PRODUCT_ID.h b/src/backend/taler-merchant-httpd_delete-private-products-PRODUCT_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_delete-private-products-PRODUCT_ID.h + * @brief implement DELETE /products/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_PRODUCTS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_PRODUCTS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/products/$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_products_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-products-PRODUCT_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-reports-REPORT_ID.c b/src/backend/taler-merchant-httpd_delete-private-reports-REPORT_ID.c @@ -0,0 +1,76 @@ +/* + 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 taler-merchant-httpd_delete-private-reports-REPORT_ID.c + * @brief implementation of DELETE /private/reports/$REPORT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-reports-REPORT_ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_delete_report (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *report_id_str = hc->infix; + unsigned long long report_id; + enum GNUNET_DB_QueryStatus qs; + char dummy; + + (void) rh; + + if (1 != + sscanf (report_id_str, + "%llu%c", + &report_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "report_id"); + } + + qs = TMH_db->delete_report (TMH_db->cls, + hc->instance->settings.id, + (uint64_t) report_id); + if (qs < 0) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_report"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, + report_id_str); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_delete-private-reports-REPORT_ID.h b/src/backend/taler-merchant-httpd_delete-private-reports-REPORT_ID.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 taler-merchant-httpd_delete-private-reports-REPORT_ID.h + * @brief HTTP serving layer for deleting reports + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_REPORT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_REPORT_ID_H +#include "taler-merchant-httpd.h" + +/** + * Handle DELETE /private/reports/$REPORT_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_report (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.c @@ -0,0 +1,78 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_delete-private-templates-TEMPLATE_ID.c + * @brief implement DELETE /templates/$ID + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/templates/$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_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->delete_template (TMH_db->cls, + mi->settings.id, + hc->infix); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_template"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_template (soft)"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h b/src/backend/taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_delete-private-templates-TEMPLATE_ID.h + * @brief implement DELETE /templates/$ID/ + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TEMPLATES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TEMPLATES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/templates/$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_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.c b/src/backend/taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.c @@ -0,0 +1,75 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.c + * @brief implement DELETE /tokenfamilies/$SLUG + * @author Christian Blättler + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h" +#include <gnunet/gnunet_db_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/tokenfamilies/$SLUG" 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_token_families_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->delete_token_family (TMH_db->cls, + mi->settings.id, + hc->infix); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_token_family"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_token_family (soft)"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h b/src/backend/taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h + * @brief implement DELETE /tokenfamilies/$SLUG/ + * @author Christian Blättler + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TOKEN_FAMILIES_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TOKEN_FAMILIES_SLUG_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/tokenfamilies/$SLUG" 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_token_families_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-tokens-SERIAL.c b/src/backend/taler-merchant-httpd_delete-private-tokens-SERIAL.c @@ -0,0 +1,164 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems SA + + GNU 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. + + GNU 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-httpd_delete-private-tokens-SERIAL.c + * @brief implementing DELETE /instances/$ID/token request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-tokens-SERIAL.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_delete_instances_ID_token_SERIAL (const struct TMH_RequestHandler * + rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long serial; + char dummy; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &serial, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "serial must be a number"); + } + + + qs = TMH_db->delete_login_token_serial (TMH_db->cls, + mi->settings.id, + serial); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_login_token_by_serial"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_break (0); + return MHD_NO; +} + + +MHD_RESULT +TMH_private_delete_instances_ID_token (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *bearer = "Bearer "; + struct TMH_MerchantInstance *mi = hc->instance; + const char *tok; + struct TALER_MERCHANTDB_LoginTokenP btoken; + enum GNUNET_DB_QueryStatus qs; + + tok = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_AUTHORIZATION); + /* This was presumably checked before... */ + if (0 != + strncmp (tok, + bearer, + strlen (bearer))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "login token (in 'Authorization' header)"); + } + tok += strlen (bearer); + while (' ' == *tok) + tok++; + if (0 != strncasecmp (tok, + RFC_8959_PREFIX, + strlen (RFC_8959_PREFIX))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "login token (in 'Authorization' header)"); + } + tok += strlen (RFC_8959_PREFIX); + + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (tok, + strlen (tok), + &btoken, + sizeof (btoken))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "login token (in 'Authorization' header)"); + } + qs = TMH_db->delete_login_token (TMH_db->cls, + mi->settings.id, + &btoken); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_login_token"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* No 404, as the login token must have existed + when we got the request as it was accepted as + valid. So we can only get here due to concurrent + modification, and then the client should still + simply see the success. Hence, fall-through */ + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_break (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-tokens-SERIAL.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-tokens-SERIAL.h b/src/backend/taler-merchant-httpd_delete-private-tokens-SERIAL.h @@ -0,0 +1,59 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems SA + + GNU 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. + + GNU 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-httpd_delete-private-tokens-SERIAL.h + * @brief implements DELETE /instances/$ID/token request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_TOKEN_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_TOKEN_H +#include "taler-merchant-httpd.h" + +/** + * Delete login token for an instance by serial. + * + * @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_instances_ID_token_SERIAL ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Delete login token for an instance. + * + * @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_instances_ID_token ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-transfers-TID.c b/src/backend/taler-merchant-httpd_delete-private-transfers-TID.c @@ -0,0 +1,90 @@ +/* + This file is part of TALER + (C) 2021 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-httpd_delete-private-transfers-TID.c + * @brief implement DELETE /transfers/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-transfers-TID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_delete_transfers_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long serial; + char dummy; + + (void) rh; + GNUNET_assert (NULL != mi); + if (1 != + sscanf (hc->infix, + "%llu%c", + &serial, + &dummy)) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + hc->infix); + } + qs = TMH_db->delete_transfer (TMH_db->cls, + mi->settings.id, + serial); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + qs = TMH_db->check_transfer_exists (TMH_db->cls, + mi->settings.id, + serial); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TRANSFER_UNKNOWN, + hc->infix); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-transfers-TID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-transfers-TID.h b/src/backend/taler-merchant-httpd_delete-private-transfers-TID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_delete-private-transfers-TID.h + * @brief implement DELETE /transfers/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TRANSFERS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TRANSFERS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/transfers/$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_transfers_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-transfers-TID.h */ + +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-units-UNIT.c b/src/backend/taler-merchant-httpd_delete-private-units-UNIT.c @@ -0,0 +1,83 @@ +/* + 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-httpd_delete-private-units-UNIT.c + * @brief implement DELETE /private/units/$UNIT + * @author Bohdan Potuzhnyi + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-units-UNIT.h" + + +MHD_RESULT +TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + enum GNUNET_DB_QueryStatus qs; + bool no_instance = false; + bool no_unit = false; + bool builtin_conflict = false; + + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->delete_unit (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &no_instance, + &no_unit, + &builtin_conflict); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "delete_unit"); + case GNUNET_DB_STATUS_HARD_ERROR: + default: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_unit"); + } + if (no_instance) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->instance->settings.id); + if (no_unit) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, + hc->infix); + if (builtin_conflict) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN, + hc->infix); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_delete-private-units-UNIT.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-units-UNIT.h b/src/backend/taler-merchant-httpd_delete-private-units-UNIT.h @@ -0,0 +1,33 @@ +/* + 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-httpd_delete-private-units-UNIT.h + * @brief implement DELETE /private/units/$UNIT + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_UNITS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_UNITS_ID_H + +#include "taler-merchant-httpd.h" + + +MHD_RESULT +TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-units-UNIT.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.c b/src/backend/taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.c @@ -0,0 +1,78 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_delete-private-webhooks-WEBHOOK_ID.c + * @brief implement DELETE /webhooks/$ID + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a DELETE "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->delete_webhook (TMH_db->cls, + mi->settings.id, + hc->infix); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_webhook"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "delete_webhook (soft)"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_WEBHOOK_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.c */ diff --git a/src/backend/taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h b/src/backend/taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_delete-private-webhooks-WEBHOOK_ID.h + * @brief implement DELETE /webhooks/$ID/ + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_WEBHOOKS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_WEBHOOKS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a DELETE "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_dispatcher.c b/src/backend/taler-merchant-httpd_dispatcher.c @@ -22,110 +22,109 @@ #include "taler-merchant-httpd_get-config.h" #include "taler-merchant-httpd_get-exchanges.h" #include "taler-merchant-httpd_dispatcher.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_get-orders-ID.h" -#include "taler-merchant-httpd_get-sessions-ID.h" -#include "taler-merchant-httpd_get-products-HASH-image.h" -#include "taler-merchant-httpd_get-templates-ID.h" +#include "taler-merchant-httpd_get-orders-ORDER_ID.h" +#include "taler-merchant-httpd_get-sessions-SESSION_ID.h" +#include "taler-merchant-httpd_get-products-IMAGE_HASH-image.h" +#include "taler-merchant-httpd_get-templates-TEMPLATE_ID.h" #include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_private-delete-account-ID.h" -#include "taler-merchant-httpd_private-delete-categories-ID.h" -#include "taler-merchant-httpd_private-delete-units-ID.h" -#include "taler-merchant-httpd_private-delete-instances-ID.h" -#include "taler-merchant-httpd_private-delete-instances-ID-token.h" -#include "taler-merchant-httpd_private-delete-products-ID.h" -#include "taler-merchant-httpd_private-delete-orders-ID.h" -#include "taler-merchant-httpd_private-delete-otp-devices-ID.h" -#include "taler-merchant-httpd_private-delete-templates-ID.h" -#include "taler-merchant-httpd_private-delete-token-families-SLUG.h" -#include "taler-merchant-httpd_private-delete-transfers-ID.h" -#include "taler-merchant-httpd_private-delete-webhooks-ID.h" -#include "taler-merchant-httpd_private-get-accounts.h" -#include "taler-merchant-httpd_private-get-accounts-ID.h" -#include "taler-merchant-httpd_private-get-categories.h" -#include "taler-merchant-httpd_private-get-categories-ID.h" -#include "taler-merchant-httpd_private-get-units.h" -#include "taler-merchant-httpd_private-get-units-ID.h" -#include "taler-merchant-httpd_private-get-incoming.h" -#include "taler-merchant-httpd_private-get-incoming-ID.h" -#include "taler-merchant-httpd_private-get-instances.h" -#include "taler-merchant-httpd_private-get-instances-ID.h" -#include "taler-merchant-httpd_private-get-instances-ID-kyc.h" -#include "taler-merchant-httpd_private-get-instances-ID-tokens.h" -#include "taler-merchant-httpd_private-get-pos.h" -#include "taler-merchant-httpd_private-get-products.h" -#include "taler-merchant-httpd_private-get-products-ID.h" -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler-merchant-httpd_private-get-orders-ID.h" -#include "taler-merchant-httpd_private-get-otp-devices.h" -#include "taler-merchant-httpd_private-get-otp-devices-ID.h" -#include "taler-merchant-httpd_private-get-statistics-amount-SLUG.h" -#include "taler-merchant-httpd_private-get-statistics-counter-SLUG.h" -#include "taler-merchant-httpd_private-get-statistics-report-transactions.h" -#include "taler-merchant-httpd_private-get-templates.h" -#include "taler-merchant-httpd_private-get-templates-ID.h" -#include "taler-merchant-httpd_private-get-token-families.h" -#include "taler-merchant-httpd_private-get-token-families-SLUG.h" -#include "taler-merchant-httpd_private-get-transfers.h" -#include "taler-merchant-httpd_private-get-webhooks.h" -#include "taler-merchant-httpd_private-get-webhooks-ID.h" -#include "taler-merchant-httpd_private-patch-accounts-ID.h" -#include "taler-merchant-httpd_private-patch-categories-ID.h" -#include "taler-merchant-httpd_private-patch-units-ID.h" -#include "taler-merchant-httpd_private-patch-instances-ID.h" -#include "taler-merchant-httpd_private-patch-orders-ID-forget.h" -#include "taler-merchant-httpd_private-patch-otp-devices-ID.h" -#include "taler-merchant-httpd_private-patch-products-ID.h" -#include "taler-merchant-httpd_private-patch-templates-ID.h" -#include "taler-merchant-httpd_private-patch-token-families-SLUG.h" -#include "taler-merchant-httpd_private-patch-webhooks-ID.h" -#include "taler-merchant-httpd_private-post-account.h" -#include "taler-merchant-httpd_private-post-categories.h" -#include "taler-merchant-httpd_private-post-units.h" -#include "taler-merchant-httpd_private-post-instances.h" -#include "taler-merchant-httpd_private-post-instances-ID-auth.h" -#include "taler-merchant-httpd_private-post-instances-ID-token.h" -#include "taler-merchant-httpd_private-post-otp-devices.h" -#include "taler-merchant-httpd_private-post-orders.h" -#include "taler-merchant-httpd_private-post-orders-ID-refund.h" -#include "taler-merchant-httpd_private-post-products.h" -#include "taler-merchant-httpd_private-post-products-ID-lock.h" -#include "taler-merchant-httpd_private-post-templates.h" -#include "taler-merchant-httpd_private-post-token-families.h" -#include "taler-merchant-httpd_private-post-transfers.h" -#include "taler-merchant-httpd_private-post-webhooks.h" +#include "taler-merchant-httpd_delete-private-accounts-H_WIRE.h" +#include "taler-merchant-httpd_delete-private-categories-CATEGORY_ID.h" +#include "taler-merchant-httpd_delete-private-units-UNIT.h" +#include "taler-merchant-httpd_delete-management-instances-INSTANCE.h" +#include "taler-merchant-httpd_delete-private-tokens-SERIAL.h" +#include "taler-merchant-httpd_delete-private-products-PRODUCT_ID.h" +#include "taler-merchant-httpd_delete-private-orders-ORDER_ID.h" +#include "taler-merchant-httpd_delete-private-otp-devices-DEVICE_ID.h" +#include "taler-merchant-httpd_delete-private-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_delete-private-tokenfamilies-TOKEN_FAMILY_SLUG.h" +#include "taler-merchant-httpd_delete-private-transfers-TID.h" +#include "taler-merchant-httpd_delete-private-webhooks-WEBHOOK_ID.h" +#include "taler-merchant-httpd_get-private-accounts.h" +#include "taler-merchant-httpd_get-private-accounts-H_WIRE.h" +#include "taler-merchant-httpd_get-private-categories.h" +#include "taler-merchant-httpd_get-private-categories-CATEGORY_ID.h" +#include "taler-merchant-httpd_get-private-units.h" +#include "taler-merchant-httpd_get-private-units-UNIT.h" +#include "taler-merchant-httpd_get-private-incoming.h" +#include "taler-merchant-httpd_get-private-incoming-ID.h" +#include "taler-merchant-httpd_get-management-instances.h" +#include "taler-merchant-httpd_get-management-instances-INSTANCE.h" +#include "taler-merchant-httpd_get-private-kyc.h" +#include "taler-merchant-httpd_get-private-tokens.h" +#include "taler-merchant-httpd_get-private-pos.h" +#include "taler-merchant-httpd_get-private-products.h" +#include "taler-merchant-httpd_get-private-products-PRODUCT_ID.h" +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler-merchant-httpd_get-private-orders-ORDER_ID.h" +#include "taler-merchant-httpd_get-private-otp-devices.h" +#include "taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h" +#include "taler-merchant-httpd_get-private-statistics-amount-SLUG.h" +#include "taler-merchant-httpd_get-private-statistics-counter-SLUG.h" +#include "taler-merchant-httpd_get-private-statistics-report-transactions.h" +#include "taler-merchant-httpd_get-private-templates.h" +#include "taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_get-private-tokenfamilies.h" +#include "taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h" +#include "taler-merchant-httpd_get-private-transfers.h" +#include "taler-merchant-httpd_get-private-webhooks.h" +#include "taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h" +#include "taler-merchant-httpd_patch-private-accounts-H_WIRE.h" +#include "taler-merchant-httpd_patch-private-categories-CATEGORY_ID.h" +#include "taler-merchant-httpd_patch-private-units-UNIT.h" +#include "taler-merchant-httpd_patch-management-instances-INSTANCE.h" +#include "taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.h" +#include "taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.h" +#include "taler-merchant-httpd_patch-private-products-PRODUCT_ID.h" +#include "taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h" +#include "taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.h" +#include "taler-merchant-httpd_post-private-accounts.h" +#include "taler-merchant-httpd_post-private-categories.h" +#include "taler-merchant-httpd_post-private-units.h" +#include "taler-merchant-httpd_post-management-instances.h" +#include "taler-merchant-httpd_post-management-instances-INSTANCE-auth.h" +#include "taler-merchant-httpd_post-private-token.h" +#include "taler-merchant-httpd_post-private-otp-devices.h" +#include "taler-merchant-httpd_post-private-orders.h" +#include "taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h" +#include "taler-merchant-httpd_post-private-products.h" +#include "taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.h" +#include "taler-merchant-httpd_post-private-templates.h" +#include "taler-merchant-httpd_post-private-tokenfamilies.h" +#include "taler-merchant-httpd_post-private-transfers.h" +#include "taler-merchant-httpd_post-private-webhooks.h" #include "taler-merchant-httpd_post-challenge-ID.h" #include "taler-merchant-httpd_post-challenge-ID-confirm.h" -#include "taler-merchant-httpd_post-orders-ID-abort.h" -#include "taler-merchant-httpd_post-orders-ID-claim.h" -#include "taler-merchant-httpd_post-orders-ID-paid.h" -#include "taler-merchant-httpd_post-orders-ID-pay.h" -#include "taler-merchant-httpd_post-orders-ID-unclaim.h" -#include "taler-merchant-httpd_post-templates-ID.h" -#include "taler-merchant-httpd_post-orders-ID-refund.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-abort.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-claim.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-paid.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-pay.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h" +#include "taler-merchant-httpd_post-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-refund.h" #include "taler-merchant-httpd_get-webui.h" #include "taler-merchant-httpd_statics.h" #include "taler-merchant-httpd_get-terms.h" -#include "taler-merchant-httpd_post-reports-ID.h" -#include "taler-merchant-httpd_private-delete-report-ID.h" -#include "taler-merchant-httpd_private-get-report-ID.h" -#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" -#include "taler-merchant-httpd_private-delete-group-ID.h" +#include "taler-merchant-httpd_post-reports-REPORT_ID.h" +#include "taler-merchant-httpd_delete-private-reports-REPORT_ID.h" +#include "taler-merchant-httpd_get-private-reports-REPORT_ID.h" +#include "taler-merchant-httpd_get-private-reports.h" +#include "taler-merchant-httpd_patch-private-reports-REPORT_ID.h" +#include "taler-merchant-httpd_post-private-reports.h" +#include "taler-merchant-httpd_delete-private-pots-POT_ID.h" +#include "taler-merchant-httpd_get-private-pots-POT_ID.h" +#include "taler-merchant-httpd_get-private-pots.h" +#include "taler-merchant-httpd_patch-private-pots-POT_ID.h" +#include "taler-merchant-httpd_post-private-pots.h" +#include "taler-merchant-httpd_get-private-groups.h" +#include "taler-merchant-httpd_post-private-groups.h" +#include "taler-merchant-httpd_patch-private-groups-GROUP_ID.h" +#include "taler-merchant-httpd_delete-private-groups-GROUP_ID.h" #ifdef HAVE_DONAU_DONAU_SERVICE_H -#include "taler-merchant-httpd_private-get-donau-instances.h" -#include "taler-merchant-httpd_private-post-donau-instance.h" -#include "taler-merchant-httpd_private-delete-donau-instance-ID.h" +#include "taler-merchant-httpd_get-private-donau.h" +#include "taler-merchant-httpd_post-private-donau.h" +#include "taler-merchant-httpd_delete-private-donau-DONAU_SERIAL.h" #endif diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_exchanges.c + * @file taler-merchant-httpd_get-exchanges.c * @brief logic this HTTPD keeps for each exchange we interact with * @author Marcello Stanisci * @author Christian Grothoff @@ -1241,4 +1241,4 @@ TMH_EXCHANGES_done () } -/* end of taler-merchant-httpd_exchanges.c */ +/* end of taler-merchant-httpd_get-exchanges.c */ diff --git a/src/backend/taler-merchant-httpd_exchanges.h b/src/backend/taler-merchant-httpd_exchanges.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_exchanges.h + * @file taler-merchant-httpd_get-exchanges.h * @brief logic this HTTPD keeps for each exchange we interact with * @author Marcello Stanisci * @author Christian Grothoff diff --git a/src/backend/taler-merchant-httpd_get-config.c b/src/backend/taler-merchant-httpd_get-config.c @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_config.c + * @file taler-merchant-httpd_get-config.c * @brief implement API for querying configuration data of the backend * @author Florian Dold */ @@ -24,8 +24,9 @@ #include <taler/taler_json_lib.h> #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_get-config.h" -#include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_get-exchanges.h" /** @@ -208,4 +209,4 @@ MH_handler_config (const struct TMH_RequestHandler *rh, } -/* end of taler-merchant-httpd_config.c */ +/* end of taler-merchant-httpd_get-config.c */ diff --git a/src/backend/taler-merchant-httpd_get-config.h b/src/backend/taler-merchant-httpd_get-config.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_config.h + * @file taler-merchant-httpd_get-config.h * @brief headers for /config handler * @author Florian Dold */ diff --git a/src/backend/taler-merchant-httpd_get-exchanges.c b/src/backend/taler-merchant-httpd_get-exchanges.c @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_exchanges.c + * @file taler-merchant-httpd_get-exchanges.c * @brief implement API for querying exchange status * @author Christian Grothoff */ @@ -25,7 +25,7 @@ #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_get-exchanges.h" #include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_get-exchanges.h" /** @@ -106,4 +106,4 @@ MH_handler_exchanges (const struct TMH_RequestHandler *rh, } -/* end of taler-merchant-httpd_exchanges.c */ +/* end of taler-merchant-httpd_get-exchanges.c */ diff --git a/src/backend/taler-merchant-httpd_get-exchanges.h b/src/backend/taler-merchant-httpd_get-exchanges.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_exchanges.h + * @file taler-merchant-httpd_get-exchanges.h * @brief headers for /exchanges handler * @author Christian Grothoff */ diff --git a/src/backend/taler-merchant-httpd_get-management-instances-INSTANCE.c b/src/backend/taler-merchant-httpd_get-management-instances-INSTANCE.c @@ -0,0 +1,156 @@ +/* + This file is part of TALER + (C) 2019-2023 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-httpd_get-management-instances-INSTANCE.c + * @brief implement GET /instances/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-management-instances-INSTANCE.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/instances/$ID" request. + * + * @param mi instance to return information about + * @param connection the MHD connection to handle + * @return MHD result code + */ +static MHD_RESULT +get_instances_ID (struct TMH_MerchantInstance *mi, + struct MHD_Connection *connection) +{ + json_t *ja; + json_t *auth; + + GNUNET_assert (NULL != mi); + ja = json_array (); + GNUNET_assert (NULL != ja); + for (struct TMH_WireMethod *wm = mi->wm_head; + NULL != wm; + wm = wm->next) + { + GNUNET_assert ( + 0 == + json_array_append_new ( + ja, + GNUNET_JSON_PACK ( + TALER_JSON_pack_full_payto ( + "payto_uri", + wm->payto_uri), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "credit_facade_url", + wm->credit_facade_url)), + GNUNET_JSON_pack_data_auto ("h_wire", + &wm->h_wire), + GNUNET_JSON_pack_data_auto ( + "salt", + &wm->wire_salt), + GNUNET_JSON_pack_bool ("active", + wm->active)))); + } + if (GNUNET_YES == TMH_strict_v19) + { + // When pre v19 is deprecated this if guard can be removed + // and the code below should never return "external" + GNUNET_assert (! GNUNET_is_zero (&mi->auth.auth_hash)); + } + auth = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("method", + GNUNET_is_zero (&mi->auth.auth_hash) + ? "external" + : "token")); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("accounts", + ja), + GNUNET_JSON_pack_string ("name", + mi->settings.name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("website", + mi->settings.website)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("email", + mi->settings.email)), + GNUNET_JSON_pack_bool ("email_validated", + mi->settings.email_validated), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("phone_number", + mi->settings.phone)), + GNUNET_JSON_pack_bool ("phone_validated", + mi->settings.phone_validated), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("logo", + mi->settings.logo)), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &mi->merchant_pub), + GNUNET_JSON_pack_object_incref ("address", + mi->settings.address), + GNUNET_JSON_pack_object_incref ("jurisdiction", + mi->settings.jurisdiction), + GNUNET_JSON_pack_bool ("use_stefan", + mi->settings.use_stefan), + GNUNET_JSON_pack_time_rel ("default_wire_transfer_delay", + mi->settings.default_wire_transfer_delay), + GNUNET_JSON_pack_time_rel ("default_pay_delay", + mi->settings.default_pay_delay), + GNUNET_JSON_pack_time_rel ("default_refund_delay", + mi->settings.default_refund_delay), + GNUNET_JSON_pack_time_rounder_interval ( + "default_wire_transfer_rounding_interval", + mi->settings.default_wire_transfer_rounding_interval), + GNUNET_JSON_pack_object_steal ("auth", + auth)); +} + + +MHD_RESULT +TMH_private_get_instances_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + return get_instances_ID (mi, + connection); +} + + +MHD_RESULT +TMH_private_get_instances_default_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi; + + mi = TMH_lookup_instance (hc->infix); + if ( (NULL == mi) || + (mi->deleted) ) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); + } + return get_instances_ID (mi, + connection); +} + + +/* end of taler-merchant-httpd_get-management-instances-INSTANCE.c */ diff --git a/src/backend/taler-merchant-httpd_get-management-instances-INSTANCE.h b/src/backend/taler-merchant-httpd_get-management-instances-INSTANCE.h @@ -0,0 +1,56 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_get-management-instances-INSTANCE.h + * @brief implement GET /instances/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/instances/$ID/private" 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_instances_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Handle a GET "/management/instances/$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_instances_default_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/* end of taler-merchant-httpd_get-management-instances-INSTANCE.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-management-instances.c b/src/backend/taler-merchant-httpd_get-management-instances.c @@ -0,0 +1,125 @@ +/* + This file is part of TALER + (C) 2019-2023 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-httpd_get-management-instances.c + * @brief implement GET /instances + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-management-instances.h" + +/** + * Add merchant instance to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param key unused + * @param value a `struct TMH_MerchantInstance *` + * @return #GNUNET_OK (continue to iterate) + */ +static enum GNUNET_GenericReturnValue +add_instance (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + json_t *ja = cls; + struct TMH_MerchantInstance *mi = value; + json_t *pta; + + (void) key; + /* Compile array of all unique wire methods supported by this + instance */ + pta = json_array (); + GNUNET_assert (NULL != pta); + for (struct TMH_WireMethod *wm = mi->wm_head; + NULL != wm; + wm = wm->next) + { + bool duplicate = false; + + if (! wm->active) + break; + /* Yes, O(n^2), but really how many bank accounts can an + instance realistically have for this to matter? */ + for (struct TMH_WireMethod *pm = mi->wm_head; + pm != wm; + pm = pm->next) + if (0 == strcasecmp (pm->wire_method, + wm->wire_method)) + { + duplicate = true; + break; + } + if (duplicate) + continue; + GNUNET_assert (0 == + json_array_append_new (pta, + json_string (wm->wire_method))); + } + GNUNET_assert (0 == + json_array_append_new ( + ja, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + mi->settings.name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("website", + mi->settings.website)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("logo", + mi->settings.logo)), + GNUNET_JSON_pack_string ("id", + mi->settings.id), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &mi->merchant_pub), + GNUNET_JSON_pack_array_steal ("payment_targets", + pta), + GNUNET_JSON_pack_bool ("deleted", + mi->deleted)))); + return GNUNET_OK; +} + + +/** + * Handle a GET "/instances" 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_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *ia; + + (void) rh; + (void) hc; + ia = json_array (); + GNUNET_assert (NULL != ia); + GNUNET_CONTAINER_multihashmap_iterate (TMH_by_id_map, + &add_instance, + ia); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("instances", + ia)); +} + + +/* end of taler-merchant-httpd_get-management-instances.c */ diff --git a/src/backend/taler-merchant-httpd_get-management-instances.h b/src/backend/taler-merchant-httpd_get-management-instances.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_get-management-instances.h + * @brief implement GET /instances + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/instances" 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_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-management-instances.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -1,1713 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2024 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-httpd_get-orders-ID.c - * @brief implementation of GET /orders/$ID - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <gnunet/gnunet_uri_lib.h> -#include <gnunet/gnunet_common.h> -#include <taler/taler_signatures.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_templating_lib.h> -#include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_get-orders-ID.h" -#include "taler-merchant-httpd_mhd.h" -#include "taler-merchant-httpd_qr.h" -#include "taler/taler_error_codes.h" -#include "taler/taler_util.h" -#include "taler/taler_merchant_util.h" - -/** - * How often do we retry DB transactions on serialization failures? - */ -#define MAX_RETRIES 5 - - -/** - * The different phases in which we handle the request. - */ -enum Phase -{ - GOP_INIT = 0, - GOP_LOOKUP_TERMS = 1, - GOP_PARSE_CONTRACT = 2, - GOP_CHECK_CLIENT_ACCESS = 3, - GOP_CHECK_PAID = 4, - GOP_REDIRECT_TO_PAID_ORDER = 5, - GOP_HANDLE_UNPAID = 6, - GOP_CHECK_REFUNDED = 7, - GOP_RETURN_STATUS = 8, - GOP_RETURN_MHD_YES = 9, - GOP_RETURN_MHD_NO = 10 -}; - - -/** - * Context for the operation. - */ -struct GetOrderData -{ - - /** - * Hashed version of contract terms. All zeros if not provided. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Claim token used for access control. All zeros if not provided. - */ - struct TALER_ClaimTokenP claim_token; - - /** - * DLL of (suspended) requests. - */ - struct GetOrderData *next; - - /** - * DLL of (suspended) requests. - */ - struct GetOrderData *prev; - - /** - * Context of the request. - */ - struct TMH_HandlerContext *hc; - - /** - * Entry in the #resume_timeout_heap for this check payment, if we are - * suspended. - */ - struct TMH_SuspendedConnection sc; - - /** - * Database event we are waiting on to be resuming on payment. - */ - struct GNUNET_DB_EventHandler *pay_eh; - - /** - * Database event we are waiting on to be resuming for refunds. - */ - struct GNUNET_DB_EventHandler *refund_eh; - - /** - * Database event we are waiting on to be resuming for repurchase - * detection updating some equivalent order (same fulfillment URL) - * to our session. - */ - struct GNUNET_DB_EventHandler *session_eh; - - /** - * Which merchant instance is this for? - */ - struct MerchantInstance *mi; - - /** - * order ID for the payment - */ - const char *order_id; - - /** - * session of the client - */ - const char *session_id; - - /** - * choice index (contract v1) - */ - int16_t choice_index; - - /** - * Contract terms of the payment we are checking. NULL when they - * are not (yet) known. - */ - json_t *contract_terms_json; - - /** - * Parsed contract terms, NULL when parsing failed. - */ - struct TALER_MERCHANT_Contract *contract_terms; - - /** - * Total refunds granted for this payment. Only initialized - * if @e refunded is set to true. - */ - struct TALER_Amount refund_amount; - - /** - * Total refunds already collected. - * if @e refunded is set to true. - */ - struct TALER_Amount refund_taken; - - /** - * Phase in which we currently are handling this - * request. - */ - enum Phase phase; - - /** - * Return code: #TALER_EC_NONE if successful. - */ - enum TALER_ErrorCode ec; - - /** - * Did we suspend @a connection and are thus in - * the #god_head DLL (#GNUNET_YES). Set to - * #GNUNET_NO if we are not suspended, and to - * #GNUNET_SYSERR if we should close the connection - * without a response due to shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * Set to YES if refunded orders should be included when - * doing repurchase detection. - */ - enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; - - /** - * Set to true if the client passed 'h_contract'. - */ - bool h_contract_provided; - - /** - * Set to true if the client passed a 'claim' token. - */ - bool claim_token_provided; - - /** - * Set to true if we are dealing with a claimed order - * (and thus @e h_contract_terms is set, otherwise certain - * DB queries will not work). - */ - bool claimed; - - /** - * Set to true if this order was paid. - */ - bool paid; - - /** - * Set to true if this order has been refunded and - * @e refund_amount is initialized. - */ - bool refunded; - - /** - * Set to true if a refund is still available for the - * wallet for this payment. - * @deprecated: true if refund_taken < refund_amount - */ - bool refund_pending; - - /** - * Set to true if the client requested HTML, otherwise we generate JSON. - */ - bool generate_html; - - /** - * Did we parse the contract terms? - */ - bool contract_parsed; - - /** - * Set to true if the refunds found in the DB have - * a different currency then the main contract. - */ - bool bad_refund_currency_in_db; - - /** - * Did the hash of the contract match the contract - * hash supplied by the client? - */ - bool contract_match; - - /** - * True if we had a claim token and the claim token - * provided by the client matched our claim token. - */ - bool token_match; - - /** - * True if we found a (claimed) contract for the order, - * false if we had an unclaimed order. - */ - bool contract_available; - -}; - - -/** - * Head of DLL of (suspended) requests. - */ -static struct GetOrderData *god_head; - -/** - * Tail of DLL of (suspended) requests. - */ -static struct GetOrderData *god_tail; - - -void -TMH_force_wallet_get_order_resume (void) -{ - struct GetOrderData *god; - - while (NULL != (god = god_head)) - { - GNUNET_CONTAINER_DLL_remove (god_head, - god_tail, - god); - GNUNET_assert (god->suspended); - god->suspended = GNUNET_SYSERR; - MHD_resume_connection (god->sc.con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - } -} - - -/** - * Suspend this @a god until the trigger is satisfied. - * - * @param god request to suspend - */ -static void -suspend_god (struct GetOrderData *god) -{ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending GET /orders/%s\n", - god->order_id); - /* We reset the contract terms and start by looking them up - again, as while we are suspended fundamental things could - change (such as the contract being claimed) */ - if (NULL != god->contract_terms_json) - { - json_decref (god->contract_terms_json); - god->contract_terms_json = NULL; - god->contract_parsed = false; - } - if (NULL != god->contract_terms) - { - TALER_MERCHANT_contract_free (god->contract_terms); - god->contract_terms = NULL; - } - GNUNET_assert (! god->suspended); - god->contract_parsed = false; - god->contract_match = false; - god->token_match = false; - god->contract_available = false; - god->phase = GOP_LOOKUP_TERMS; - god->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (god_head, - god_tail, - god); - MHD_suspend_connection (god->sc.con); -} - - -/** - * Clean up the session state for a GET /orders/$ID request. - * - * @param cls must be a `struct GetOrderData *` - */ -static void -god_cleanup (void *cls) -{ - struct GetOrderData *god = cls; - - if (NULL != god->contract_terms_json) - { - json_decref (god->contract_terms_json); - god->contract_terms_json = NULL; - } - if (NULL != god->contract_terms) - { - TALER_MERCHANT_contract_free (god->contract_terms); - god->contract_terms = NULL; - } - if (NULL != god->session_eh) - { - TMH_db->event_listen_cancel (god->session_eh); - god->session_eh = NULL; - } - if (NULL != god->refund_eh) - { - TMH_db->event_listen_cancel (god->refund_eh); - god->refund_eh = NULL; - } - if (NULL != god->pay_eh) - { - TMH_db->event_listen_cancel (god->pay_eh); - god->pay_eh = NULL; - } - GNUNET_free (god); -} - - -/** - * Finish the request by returning @a mret as the - * final result. - * - * @param[in,out] god request we are processing - * @param mret MHD result to return - */ -static void -phase_end (struct GetOrderData *god, - MHD_RESULT mret) -{ - god->phase = (MHD_YES == mret) - ? GOP_RETURN_MHD_YES - : GOP_RETURN_MHD_NO; -} - - -/** - * Finish the request by returning an error @a ec - * with HTTP status @a http_status and @a message. - * - * @param[in,out] god request we are processing - * @param http_status HTTP status code to return - * @param ec error code to return - * @param message human readable hint to return, can be NULL - */ -static void -phase_fail (struct GetOrderData *god, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *message) -{ - phase_end (god, - TALER_MHD_reply_with_error (god->sc.con, - http_status, - ec, - message)); -} - - -/** - * We have received a trigger from the database - * that we should (possibly) resume the request. - * - * @param cls a `struct GetOrderData` to resume - * @param extra string encoding refund amount (or NULL) - * @param extra_size number of bytes in @a extra - */ -static void -resume_by_event (void *cls, - const void *extra, - size_t extra_size) -{ - struct GetOrderData *god = cls; - struct GNUNET_AsyncScopeSave old; - - GNUNET_async_scope_enter (&god->hc->async_scope_id, - &old); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received event for %s with argument `%.*s`\n", - god->order_id, - (int) extra_size, - (const char *) extra); - if (! god->suspended) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Not suspended, ignoring event\n"); - GNUNET_async_scope_restore (&old); - return; /* duplicate event is possible */ - } - if (GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout) && - god->sc.awaiting_refund) - { - char *as; - struct TALER_Amount a; - - if (0 == extra_size) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "No amount given, but need refund above threshold\n"); - GNUNET_async_scope_restore (&old); - return; /* not relevant */ - } - as = GNUNET_strndup (extra, - extra_size); - if (GNUNET_OK != - TALER_string_to_amount (as, - &a)) - { - GNUNET_break (0); - GNUNET_async_scope_restore (&old); - GNUNET_free (as); - return; - } - GNUNET_free (as); - if (GNUNET_OK != - TALER_amount_cmp_currency (&god->sc.refund_expected, - &a)) - { - GNUNET_break (0); - GNUNET_async_scope_restore (&old); - return; /* bad currency!? */ - } - if (1 == TALER_amount_cmp (&god->sc.refund_expected, - &a)) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Amount too small to trigger resuming\n"); - GNUNET_async_scope_restore (&old); - return; /* refund too small */ - } - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming (%s/%s) by event with argument `%.*s`\n", - GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout) - ? "future" - : "past", - god->sc.awaiting_refund - ? "awaiting refund" - : "not waiting for refund", - (int) extra_size, - (const char *) extra); - god->suspended = GNUNET_NO; - GNUNET_CONTAINER_DLL_remove (god_head, - god_tail, - god); - MHD_resume_connection (god->sc.con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - GNUNET_async_scope_restore (&old); -} - - -/** - * First phase (after request parsing). - * Set up long-polling. - * - * @param[in,out] god request context - */ -static void -phase_init (struct GetOrderData *god) -{ - god->phase++; - if (god->generate_html) - return; /* If HTML is requested, we never actually long poll. */ - if (! GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout)) - return; /* long polling not requested */ - - if (god->sc.awaiting_refund || - god->sc.awaiting_refund_obtained) - { - struct TMH_OrderPayEventP refund_eh = { - .header.size = htons (sizeof (refund_eh)), - .header.type = htons (god->sc.awaiting_refund_obtained - ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED - : TALER_DBEVENT_MERCHANT_ORDER_REFUND), - .merchant_pub = god->hc->instance->merchant_pub - }; - - GNUNET_CRYPTO_hash (god->order_id, - strlen (god->order_id), - &refund_eh.h_order_id); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing %p to refunds on %s\n", - god, - god->order_id); - god->refund_eh - = TMH_db->event_listen ( - TMH_db->cls, - &refund_eh.header, - GNUNET_TIME_absolute_get_remaining ( - god->sc.long_poll_timeout), - &resume_by_event, - god); - } - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), - .merchant_pub = god->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing to payments on %s\n", - god->order_id); - GNUNET_CRYPTO_hash (god->order_id, - strlen (god->order_id), - &pay_eh.h_order_id); - god->pay_eh - = TMH_db->event_listen ( - TMH_db->cls, - &pay_eh.header, - GNUNET_TIME_absolute_get_remaining ( - god->sc.long_poll_timeout), - &resume_by_event, - god); - } -} - - -/** - * Lookup contract terms and check client has the - * right to access this order (by claim token or - * contract hash). - * - * @param[in,out] god request context - */ -static void -phase_lookup_terms (struct GetOrderData *god) -{ - uint64_t order_serial; - struct TALER_ClaimTokenP db_claim_token; - - /* Convert order_id to h_contract_terms */ - TMH_db->preflight (TMH_db->cls); - GNUNET_assert (NULL == god->contract_terms_json); - - { - enum GNUNET_DB_QueryStatus qs; - - bool paid; - bool wired; - bool session_matches; - qs = TMH_db->lookup_contract_terms3 ( - TMH_db->cls, - god->hc->instance->settings.id, - god->order_id, - NULL, - &god->contract_terms_json, - &order_serial, - &paid, - &wired, - &session_matches, - &db_claim_token, - &god->choice_index); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_contract_terms"); - return; - } - /* Note: when "!ord.requireClaimToken" and the client does not provide - a claim token (all zeros!), then token_match==TRUE below: */ - god->token_match - = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - && (0 == GNUNET_memcmp (&db_claim_token, - &god->claim_token)); - } - - /* Check if client provided the right hash code of the contract terms */ - if (NULL != god->contract_terms_json) - { - god->contract_available = true; - if (GNUNET_YES == - GNUNET_is_zero (&god->h_contract_terms)) - { - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms_json, - &god->h_contract_terms)) - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); - return; - } - } - else - { - struct TALER_PrivateContractHashP h; - - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms_json, - &h)) - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); - return; - } - god->contract_match = (0 == - GNUNET_memcmp (&h, - &god->h_contract_terms)); - if (! god->contract_match) - { - GNUNET_break_op (0); - phase_fail (god, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - return; - } - } - } - - if (god->contract_available) - { - god->claimed = true; - } - else - { - struct TALER_MerchantPostDataHashP unused; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_order ( - TMH_db->cls, - god->hc->instance->settings.id, - god->order_id, - &db_claim_token, - &unused, - (NULL == god->contract_terms_json) - ? &god->contract_terms_json - : NULL); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order"); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Unknown order id given: `%s'\n", - god->order_id); - phase_fail (god, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - god->order_id); - return; - } - /* Note: when "!ord.requireClaimToken" and the client does not provide - a claim token (all zeros!), then token_match==TRUE below: */ - god->token_match - = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && - (0 == GNUNET_memcmp (&db_claim_token, - &god->claim_token)); - } /* end unclaimed order logic */ - god->phase++; -} - - -/** - * Parse contract terms. - * - * @param[in,out] god request context - */ -static void -phase_parse_contract (struct GetOrderData *god) -{ - GNUNET_break (NULL == god->contract_terms); - god->contract_terms = TALER_MERCHANT_contract_parse ( - god->contract_terms_json, - true); - - if (NULL == god->contract_terms) - { - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - god->order_id); - return; - } - god->contract_parsed = true; - if ( (NULL != god->session_id) && - (NULL != god->contract_terms->fulfillment_url) && - (NULL == god->session_eh) ) - { - struct TMH_SessionEventP session_eh = { - .header.size = htons (sizeof (session_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), - .merchant_pub = god->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing to session triggers for %p\n", - god); - GNUNET_CRYPTO_hash (god->session_id, - strlen (god->session_id), - &session_eh.h_session_id); - GNUNET_CRYPTO_hash (god->contract_terms->fulfillment_url, - strlen (god->contract_terms->fulfillment_url), - &session_eh.h_fulfillment_url); - god->session_eh - = TMH_db->event_listen ( - TMH_db->cls, - &session_eh.header, - GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout), - &resume_by_event, - god); - } - god->phase++; -} - - -/** - * Check that this order is unclaimed or claimed by - * this client. - * - * @param[in,out] god request context - */ -static void -phase_check_client_access (struct GetOrderData *god) -{ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n", - god->token_match, - god->contract_available, - god->contract_match, - god->claimed); - - if (god->claim_token_provided && ! god->token_match) - { - /* Authentication provided but wrong. */ - GNUNET_break_op (0); - phase_fail (god, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "authentication with claim token provided but wrong"); - return; - } - - if (god->h_contract_provided && ! god->contract_match) - { - /* Authentication provided but wrong. */ - GNUNET_break_op (0); - phase_fail (god, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH, - NULL); - return; - } - - if (! (god->token_match || - god->contract_match) ) - { - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Neither claim token nor contract matched\n"); - /* Client has no rights to this order */ - if (NULL == god->contract_terms->public_reorder_url) - { - /* We cannot give the client a new order, just fail */ - if (! GNUNET_is_zero (&god->h_contract_terms)) - { - GNUNET_break_op (0); - phase_fail (god, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - return; - } - GNUNET_break_op (0); - phase_fail (god, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, - "no 'public_reorder_url'"); - return; - } - /* We have a fulfillment URL, redirect the client there, maybe - the frontend can generate a fresh order for this new customer */ - if (god->generate_html) - { - /* Contract was claimed (maybe by another device), so this client - cannot get the status information. Redirect to fulfillment page, - where the client may be able to pickup a fresh order -- or might - be able authenticate via session ID */ - struct MHD_Response *reply; - MHD_RESULT ret; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Contract claimed, redirecting to fulfillment page for order %s\n", - god->order_id); - reply = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - if (NULL == reply) - { - GNUNET_break (0); - phase_end (god, - MHD_NO); - return; - } - GNUNET_break (MHD_YES == - MHD_add_response_header ( - reply, - MHD_HTTP_HEADER_LOCATION, - god->contract_terms->public_reorder_url)); - ret = MHD_queue_response (god->sc.con, - MHD_HTTP_FOUND, - reply); - MHD_destroy_response (reply); - phase_end (god, - ret); - return; - } - /* Need to generate JSON reply */ - phase_end (god, - TALER_MHD_REPLY_JSON_PACK ( - god->sc.con, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ( - "public_reorder_url", - god->contract_terms->public_reorder_url))); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Claim token or contract matched\n"); - god->phase++; -} - - -/** - * Return the order summary of the contract of @a god in the - * preferred language of the HTTP client. - * - * @param god order to extract summary from - * @return dummy error message summary if no summary was provided in the contract - */ -static const char * -get_order_summary (const struct GetOrderData *god) -{ - const char *language_pattern; - const char *ret; - - language_pattern = MHD_lookup_connection_value (god->sc.con, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT_LANGUAGE); - if (NULL == language_pattern) - language_pattern = "en"; - ret = json_string_value (TALER_JSON_extract_i18n (god->contract_terms_json, - language_pattern, - "summary")); - if (NULL == ret) - { - /* Upon order creation (and insertion into the database), the presence - of a summary should have been checked. So if we get here, someone - did something fishy to our database... */ - GNUNET_break (0); - ret = "<bug: no summary>"; - } - return ret; -} - - -/** - * The client did not yet pay, send it the payment request. - * - * @param god check pay request context - * @param already_paid_order_id if for the fulfillment URI there is - * already a paid order, this is the order ID to redirect - * the wallet to; NULL if not applicable - * @return true to exit due to suspension - */ -static bool -send_pay_request (struct GetOrderData *god, - const char *already_paid_order_id) -{ - MHD_RESULT ret; - char *taler_pay_uri; - char *order_status_url; - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); - if ( (! GNUNET_TIME_relative_is_zero (remaining)) && - (NULL == already_paid_order_id) ) - { - /* long polling: do not queue a response, suspend connection instead */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending request: long polling for payment\n"); - suspend_god (god); - return true; - } - - /* Check if resource_id has been paid for in the same session - * with another order_id. - */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Sending payment request\n"); - taler_pay_uri = TMH_make_taler_pay_uri ( - god->sc.con, - god->order_id, - god->session_id, - god->hc->instance->settings.id, - &god->claim_token); - order_status_url = TMH_make_order_status_url ( - god->sc.con, - god->order_id, - god->session_id, - god->hc->instance->settings.id, - &god->claim_token, - NULL); - if ( (NULL == taler_pay_uri) || - (NULL == order_status_url) ) - { - GNUNET_break_op (0); - GNUNET_free (taler_pay_uri); - GNUNET_free (order_status_url); - phase_fail (god, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, - "host"); - return false; - } - if (god->generate_html) - { - if (NULL != already_paid_order_id) - { - struct MHD_Response *reply; - - GNUNET_assert (NULL != god->contract_terms->fulfillment_url); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Redirecting to already paid order %s via fulfillment URL %s\n", - already_paid_order_id, - god->contract_terms->fulfillment_url); - reply = MHD_create_response_from_buffer (0, - NULL, - MHD_RESPMEM_PERSISTENT); - if (NULL == reply) - { - GNUNET_break (0); - phase_end (god, - MHD_NO); - return false; - } - GNUNET_break (MHD_YES == - MHD_add_response_header ( - reply, - MHD_HTTP_HEADER_LOCATION, - god->contract_terms->fulfillment_url)); - { - ret = MHD_queue_response (god->sc.con, - MHD_HTTP_FOUND, - reply); - MHD_destroy_response (reply); - phase_end (god, - ret); - return false; - } - } - - { - char *qr; - - qr = TMH_create_qrcode (taler_pay_uri); - if (NULL == qr) - { - GNUNET_break (0); - phase_end (god, - MHD_NO); - return false; - } - { - enum GNUNET_GenericReturnValue res; - json_t *context; - - context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("taler_pay_uri", - taler_pay_uri), - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - GNUNET_JSON_pack_string ("taler_pay_qrcode_svg", - qr), - GNUNET_JSON_pack_string ("order_summary", - get_order_summary (god))); - res = TALER_TEMPLATING_reply ( - god->sc.con, - MHD_HTTP_PAYMENT_REQUIRED, - "request_payment", - god->hc->instance->settings.id, - taler_pay_uri, - context); - if (GNUNET_SYSERR == res) - { - GNUNET_break (0); - ret = MHD_NO; - } - else - { - ret = MHD_YES; - } - json_decref (context); - } - GNUNET_free (qr); - } - } - else /* end of 'generate HTML' */ - { - ret = TALER_MHD_REPLY_JSON_PACK ( - god->sc.con, - MHD_HTTP_PAYMENT_REQUIRED, - GNUNET_JSON_pack_string ("taler_pay_uri", - taler_pay_uri), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - god->contract_terms->fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("already_paid_order_id", - already_paid_order_id))); - } - GNUNET_free (taler_pay_uri); - GNUNET_free (order_status_url); - phase_end (god, - ret); - return false; -} - - -/** - * Check if the order has been paid. - * - * @param[in,out] god request context - */ -static void -phase_check_paid (struct GetOrderData *god) -{ - enum GNUNET_DB_QueryStatus qs; - struct TALER_PrivateContractHashP h_contract; - - god->paid = false; - qs = TMH_db->lookup_order_status ( - TMH_db->cls, - god->hc->instance->settings.id, - god->order_id, - &h_contract, - &god->paid); - if (0 > qs) - { - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order_status"); - return; - } - god->phase++; -} - - -/** - * Check if the client already paid for an equivalent - * order under this session, and if so redirect to - * that order. - * - * @param[in,out] god request context - * @return true to exit due to suspension - */ -static bool -phase_redirect_to_paid_order (struct GetOrderData *god) -{ - if ( (NULL != god->session_id) && - (NULL != god->contract_terms->fulfillment_url) ) - { - /* Check if client paid for this fulfillment article - already within this session, but using a different - order ID. If so, redirect the client to the order - it already paid. Allows, for example, the case - where a mobile phone pays for a browser's session, - where the mobile phone has a different order - ID (because it purchased the article earlier) - than the one that the browser is waiting for. */ - char *already_paid_order_id = NULL; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Running re-purchase detection for %s/%s\n", - god->session_id, - god->contract_terms->fulfillment_url); - qs = TMH_db->lookup_order_by_fulfillment ( - TMH_db->cls, - god->hc->instance->settings.id, - god->contract_terms->fulfillment_url, - god->session_id, - TALER_EXCHANGE_YNA_NO != god->allow_refunded_for_repurchase, - &already_paid_order_id); - if (qs < 0) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "order by fulfillment"); - return false; - } - if ( (! god->paid) && - ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || - (0 != strcmp (god->order_id, - already_paid_order_id)) ) ) - { - bool ret; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Sending pay request for order %s (already paid: %s)\n", - god->order_id, - already_paid_order_id); - ret = send_pay_request (god, - already_paid_order_id); - GNUNET_free (already_paid_order_id); - return ret; - } - GNUNET_free (already_paid_order_id); - } - god->phase++; - return false; -} - - -/** - * Check if the order has been paid, and if not - * request payment. - * - * @param[in,out] god request context - * @return true to exit due to suspension - */ -static bool -phase_handle_unpaid (struct GetOrderData *god) -{ - if (god->paid) - { - god->phase++; - return false; - } - if (god->claimed) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order claimed but unpaid, sending pay request for order %s\n", - god->order_id); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order unclaimed, sending pay request for order %s\n", - god->order_id); - } - return send_pay_request (god, - NULL); -} - - -/** - * Function called with detailed information about a refund. - * It is responsible for packing up the data to return. - * - * @param cls closure - * @param refund_serial unique serial number of the refund - * @param timestamp time of the refund (for grouping of refunds in the wallet UI) - * @param coin_pub public coin from which the refund comes from - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param rtransaction_id identificator of the refund - * @param reason human-readable explanation of the refund - * @param refund_amount refund amount which is being taken from @a coin_pub - * @param pending true if the this refund was not yet processed by the wallet/exchange - */ -static void -process_refunds_cb (void *cls, - uint64_t refund_serial, - struct GNUNET_TIME_Timestamp timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - uint64_t rtransaction_id, - const char *reason, - const struct TALER_Amount *refund_amount, - bool pending) -{ - struct GetOrderData *god = cls; - - (void) refund_serial; - (void) timestamp; - (void) exchange_url; - (void) rtransaction_id; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Found refund of %s for coin %s with reason `%s' in database\n", - TALER_amount2s (refund_amount), - TALER_B2S (coin_pub), - reason); - god->refund_pending |= pending; - if ( (GNUNET_OK != - TALER_amount_cmp_currency (&god->refund_taken, - refund_amount)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&god->refund_amount, - refund_amount)) ) - { - god->bad_refund_currency_in_db = true; - return; - } - if (! pending) - { - GNUNET_assert (0 <= - TALER_amount_add (&god->refund_taken, - &god->refund_taken, - refund_amount)); - } - GNUNET_assert (0 <= - TALER_amount_add (&god->refund_amount, - &god->refund_amount, - refund_amount)); - god->refunded = true; -} - - -/** - * Check if the order has been refunded. - * - * @param[in,out] god request context - * @return true to exit due to suspension - */ -static bool -phase_check_refunded (struct GetOrderData *god) -{ - enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount refund_amount; - const char *refund_currency; - - switch (god->contract_terms->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - refund_amount = god->contract_terms->details.v0.brutto; - refund_currency = god->contract_terms->details.v0.brutto.currency; - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - if (god->choice_index < 0) - { - // order was not paid, no refund to be checked - god->phase++; - return false; - } - GNUNET_assert (god->choice_index < - god->contract_terms->details.v1.choices_len); - refund_currency = god->contract_terms->details.v1.choices[god->choice_index] - .amount.currency; - GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (refund_currency, - &refund_amount)); - break; - default: - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION, - NULL); - return false; - } - } - - if ( (god->sc.awaiting_refund) && - (GNUNET_OK != - TALER_amount_cmp_currency (&refund_amount, - &god->sc.refund_expected)) ) - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - refund_currency); - return false; - } - - /* At this point, we know the contract was paid. Let's check for - refunds. First, clear away refunds found from previous invocations. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (refund_currency, - &god->refund_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (refund_currency, - &god->refund_taken)); - qs = TMH_db->lookup_refunds_detailed ( - TMH_db->cls, - god->hc->instance->settings.id, - &god->h_contract_terms, - &process_refunds_cb, - god); - if (0 > qs) - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_refunds_detailed"); - return false; - } - if (god->bad_refund_currency_in_db) - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "currency mix-up between contract price and refunds in database"); - return false; - } - if ( ((god->sc.awaiting_refund) && - ( (! god->refunded) || - (1 != TALER_amount_cmp (&god->refund_amount, - &god->sc.refund_expected)) )) || - ( (god->sc.awaiting_refund_obtained) && - (god->refund_pending) ) ) - { - /* Client is waiting for a refund larger than what we have, suspend - until timeout */ - struct GNUNET_TIME_Relative remaining; - - remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); - if ( (! GNUNET_TIME_relative_is_zero (remaining)) && - (! god->generate_html) ) - { - /* yes, indeed suspend */ - if (god->sc.awaiting_refund) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting refund exceeding %s\n", - TALER_amount2s (&god->sc.refund_expected)); - if (god->sc.awaiting_refund_obtained) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting pending refunds\n"); - suspend_god (god); - return true; - } - } - god->phase++; - return false; -} - - -/** - * Create a taler://refund/ URI for the given @a con and @a order_id - * and @a instance_id. - * - * @param merchant_base_url URL to take host and path from; - * we cannot take it from the MHD connection as a browser - * may have changed 'http' to 'https' and we MUST be consistent - * with what the merchant's frontend used initially - * @param order_id the order id - * @return corresponding taler://refund/ URI, or NULL on missing "host" - */ -static char * -make_taler_refund_uri (const char *merchant_base_url, - const char *order_id) -{ - struct GNUNET_Buffer buf = { 0 }; - char *url; - struct GNUNET_Uri uri; - - url = GNUNET_strdup (merchant_base_url); - if (-1 == GNUNET_uri_parse (&uri, - url)) - { - GNUNET_break (0); - GNUNET_free (url); - return NULL; - } - GNUNET_assert (NULL != order_id); - GNUNET_buffer_write_str (&buf, - "taler"); - if (0 == strcasecmp ("http", - uri.scheme)) - GNUNET_buffer_write_str (&buf, - "+http"); - GNUNET_buffer_write_str (&buf, - "://refund/"); - GNUNET_buffer_write_str (&buf, - uri.host); - if (0 != uri.port) - GNUNET_buffer_write_fstr (&buf, - ":%u", - (unsigned int) uri.port); - if (NULL != uri.path) - GNUNET_buffer_write_path (&buf, - uri.path); - GNUNET_buffer_write_path (&buf, - order_id); - GNUNET_buffer_write_path (&buf, - ""); // Trailing slash - GNUNET_free (url); - return GNUNET_buffer_reap_str (&buf); -} - - -/** - * Generate the order status response. - * - * @param[in,out] god request context - */ -static void -phase_return_status (struct GetOrderData *god) -{ - /* All operations done, build final response */ - if (! god->generate_html) - { - phase_end (god, - TALER_MHD_REPLY_JSON_PACK ( - god->sc.con, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - god->contract_terms->fulfillment_url - )), - GNUNET_JSON_pack_bool ("refunded", - god->refunded), - GNUNET_JSON_pack_bool ("refund_pending", - god->refund_pending), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount))); - return; - } - - if (god->refund_pending) - { - char *qr; - char *uri; - - GNUNET_assert (NULL != god->contract_terms_json); - uri = make_taler_refund_uri (god->contract_terms->merchant_base_url, - god->order_id); - if (NULL == uri) - { - GNUNET_break (0); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "refund URI"); - return; - } - qr = TMH_create_qrcode (uri); - if (NULL == qr) - { - GNUNET_break (0); - GNUNET_free (uri); - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "qr code"); - return; - } - - { - enum GNUNET_GenericReturnValue res; - json_t *context; - - context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_summary", - get_order_summary (god)), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken), - GNUNET_JSON_pack_string ("taler_refund_uri", - uri), - GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", - qr)); - res = TALER_TEMPLATING_reply ( - god->sc.con, - MHD_HTTP_OK, - "offer_refund", - god->hc->instance->settings.id, - uri, - context); - GNUNET_break (GNUNET_OK == res); - json_decref (context); - phase_end (god, - (GNUNET_SYSERR == res) - ? MHD_NO - : MHD_YES); - } - GNUNET_free (uri); - GNUNET_free (qr); - return; - } - - { - enum GNUNET_GenericReturnValue res; - json_t *context; - - context = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_object_incref ("contract_terms", - god->contract_terms_json), - GNUNET_JSON_pack_string ("order_summary", - get_order_summary (god)), - TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount), - TALER_JSON_pack_amount ("refund_taken", - &god->refund_taken)); - res = TALER_TEMPLATING_reply ( - god->sc.con, - MHD_HTTP_OK, - "show_order_details", - god->hc->instance->settings.id, - NULL, - context); - GNUNET_break (GNUNET_OK == res); - json_decref (context); - phase_end (god, - (GNUNET_SYSERR == res) - ? MHD_NO - : MHD_YES); - } -} - - -MHD_RESULT -TMH_get_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct GetOrderData *god = hc->ctx; - - (void) rh; - if (NULL == god) - { - god = GNUNET_new (struct GetOrderData); - hc->ctx = god; - hc->cc = &god_cleanup; - god->sc.con = connection; - god->hc = hc; - god->order_id = hc->infix; - god->generate_html - = TMH_MHD_test_html_desired (connection); - - /* first-time initialization / sanity checks */ - TALER_MHD_parse_request_arg_auto (connection, - "h_contract", - &god->h_contract_terms, - god->h_contract_provided); - TALER_MHD_parse_request_arg_auto (connection, - "token", - &god->claim_token, - god->claim_token_provided); - if (! (TALER_MHD_arg_to_yna (connection, - "allow_refunded_for_repurchase", - TALER_EXCHANGE_YNA_NO, - &god->allow_refunded_for_repurchase)) ) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "allow_refunded_for_repurchase"); - god->session_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "session_id"); - - /* process await_refund_obtained argument */ - { - const char *await_refund_obtained_s; - - await_refund_obtained_s = - MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "await_refund_obtained"); - god->sc.awaiting_refund_obtained = - (NULL != await_refund_obtained_s) - ? 0 == strcasecmp (await_refund_obtained_s, - "yes") - : false; - if (god->sc.awaiting_refund_obtained) - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting refund obtained\n"); - } - - TALER_MHD_parse_request_amount (connection, - "refund", - &god->sc.refund_expected); - if (TALER_amount_is_valid (&god->sc.refund_expected)) - { - god->sc.awaiting_refund = true; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Awaiting minimum refund of %s\n", - TALER_amount2s (&god->sc.refund_expected)); - } - TALER_MHD_parse_request_timeout (connection, - &god->sc.long_poll_timeout); - } - - if (GNUNET_SYSERR == god->suspended) - return MHD_NO; /* we are in shutdown */ - if (GNUNET_YES == god->suspended) - { - god->suspended = GNUNET_NO; - GNUNET_CONTAINER_DLL_remove (god_head, - god_tail, - god); - } - - while (1) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling request in phase %d\n", - (int) god->phase); - switch (god->phase) - { - case GOP_INIT: - phase_init (god); - break; - case GOP_LOOKUP_TERMS: - phase_lookup_terms (god); - break; - case GOP_PARSE_CONTRACT: - phase_parse_contract (god); - break; - case GOP_CHECK_CLIENT_ACCESS: - phase_check_client_access (god); - break; - case GOP_CHECK_PAID: - phase_check_paid (god); - break; - case GOP_REDIRECT_TO_PAID_ORDER: - if (phase_redirect_to_paid_order (god)) - return MHD_YES; - break; - case GOP_HANDLE_UNPAID: - if (phase_handle_unpaid (god)) - return MHD_YES; - break; - case GOP_CHECK_REFUNDED: - if (phase_check_refunded (god)) - return MHD_YES; - break; - case GOP_RETURN_STATUS: - phase_return_status (god); - break; - case GOP_RETURN_MHD_YES: - return MHD_YES; - case GOP_RETURN_MHD_NO: - return MHD_NO; - } - } -} - - -/* end of taler-merchant-httpd_get-orders-ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.h b/src/backend/taler-merchant-httpd_get-orders-ID.h @@ -1,47 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2017, 2020 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-httpd_get-orders-ID.h - * @brief implementation of GET /orders/$ID - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_GET_ORDERS_ID_H -#define TALER_MERCHANT_HTTPD_GET_ORDERS_ID_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Force resuming all suspended order lookups, needed during shutdown. - */ -void -TMH_force_wallet_get_order_resume (void); - - -/** - * Handle a GET "/orders/$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_get_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_get-orders-ORDER_ID.c b/src/backend/taler-merchant-httpd_get-orders-ORDER_ID.c @@ -0,0 +1,1713 @@ +/* + This file is part of TALER + (C) 2014-2024 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-httpd_get-orders-ORDER_ID.c + * @brief implementation of GET /orders/$ID + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <gnunet/gnunet_uri_lib.h> +#include <gnunet/gnunet_common.h> +#include <taler/taler_signatures.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_templating_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_get-orders-ORDER_ID.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_qr.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_util.h" +#include "taler/taler_merchant_util.h" + +/** + * How often do we retry DB transactions on serialization failures? + */ +#define MAX_RETRIES 5 + + +/** + * The different phases in which we handle the request. + */ +enum Phase +{ + GOP_INIT = 0, + GOP_LOOKUP_TERMS = 1, + GOP_PARSE_CONTRACT = 2, + GOP_CHECK_CLIENT_ACCESS = 3, + GOP_CHECK_PAID = 4, + GOP_REDIRECT_TO_PAID_ORDER = 5, + GOP_HANDLE_UNPAID = 6, + GOP_CHECK_REFUNDED = 7, + GOP_RETURN_STATUS = 8, + GOP_RETURN_MHD_YES = 9, + GOP_RETURN_MHD_NO = 10 +}; + + +/** + * Context for the operation. + */ +struct GetOrderData +{ + + /** + * Hashed version of contract terms. All zeros if not provided. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Claim token used for access control. All zeros if not provided. + */ + struct TALER_ClaimTokenP claim_token; + + /** + * DLL of (suspended) requests. + */ + struct GetOrderData *next; + + /** + * DLL of (suspended) requests. + */ + struct GetOrderData *prev; + + /** + * Context of the request. + */ + struct TMH_HandlerContext *hc; + + /** + * Entry in the #resume_timeout_heap for this check payment, if we are + * suspended. + */ + struct TMH_SuspendedConnection sc; + + /** + * Database event we are waiting on to be resuming on payment. + */ + struct GNUNET_DB_EventHandler *pay_eh; + + /** + * Database event we are waiting on to be resuming for refunds. + */ + struct GNUNET_DB_EventHandler *refund_eh; + + /** + * Database event we are waiting on to be resuming for repurchase + * detection updating some equivalent order (same fulfillment URL) + * to our session. + */ + struct GNUNET_DB_EventHandler *session_eh; + + /** + * Which merchant instance is this for? + */ + struct MerchantInstance *mi; + + /** + * order ID for the payment + */ + const char *order_id; + + /** + * session of the client + */ + const char *session_id; + + /** + * choice index (contract v1) + */ + int16_t choice_index; + + /** + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. + */ + json_t *contract_terms_json; + + /** + * Parsed contract terms, NULL when parsing failed. + */ + struct TALER_MERCHANT_Contract *contract_terms; + + /** + * Total refunds granted for this payment. Only initialized + * if @e refunded is set to true. + */ + struct TALER_Amount refund_amount; + + /** + * Total refunds already collected. + * if @e refunded is set to true. + */ + struct TALER_Amount refund_taken; + + /** + * Phase in which we currently are handling this + * request. + */ + enum Phase phase; + + /** + * Return code: #TALER_EC_NONE if successful. + */ + enum TALER_ErrorCode ec; + + /** + * Did we suspend @a connection and are thus in + * the #god_head DLL (#GNUNET_YES). Set to + * #GNUNET_NO if we are not suspended, and to + * #GNUNET_SYSERR if we should close the connection + * without a response due to shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Set to YES if refunded orders should be included when + * doing repurchase detection. + */ + enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; + + /** + * Set to true if the client passed 'h_contract'. + */ + bool h_contract_provided; + + /** + * Set to true if the client passed a 'claim' token. + */ + bool claim_token_provided; + + /** + * Set to true if we are dealing with a claimed order + * (and thus @e h_contract_terms is set, otherwise certain + * DB queries will not work). + */ + bool claimed; + + /** + * Set to true if this order was paid. + */ + bool paid; + + /** + * Set to true if this order has been refunded and + * @e refund_amount is initialized. + */ + bool refunded; + + /** + * Set to true if a refund is still available for the + * wallet for this payment. + * @deprecated: true if refund_taken < refund_amount + */ + bool refund_pending; + + /** + * Set to true if the client requested HTML, otherwise we generate JSON. + */ + bool generate_html; + + /** + * Did we parse the contract terms? + */ + bool contract_parsed; + + /** + * Set to true if the refunds found in the DB have + * a different currency then the main contract. + */ + bool bad_refund_currency_in_db; + + /** + * Did the hash of the contract match the contract + * hash supplied by the client? + */ + bool contract_match; + + /** + * True if we had a claim token and the claim token + * provided by the client matched our claim token. + */ + bool token_match; + + /** + * True if we found a (claimed) contract for the order, + * false if we had an unclaimed order. + */ + bool contract_available; + +}; + + +/** + * Head of DLL of (suspended) requests. + */ +static struct GetOrderData *god_head; + +/** + * Tail of DLL of (suspended) requests. + */ +static struct GetOrderData *god_tail; + + +void +TMH_force_wallet_get_order_resume (void) +{ + struct GetOrderData *god; + + while (NULL != (god = god_head)) + { + GNUNET_CONTAINER_DLL_remove (god_head, + god_tail, + god); + GNUNET_assert (god->suspended); + god->suspended = GNUNET_SYSERR; + MHD_resume_connection (god->sc.con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + } +} + + +/** + * Suspend this @a god until the trigger is satisfied. + * + * @param god request to suspend + */ +static void +suspend_god (struct GetOrderData *god) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending GET /orders/%s\n", + god->order_id); + /* We reset the contract terms and start by looking them up + again, as while we are suspended fundamental things could + change (such as the contract being claimed) */ + if (NULL != god->contract_terms_json) + { + json_decref (god->contract_terms_json); + god->contract_terms_json = NULL; + god->contract_parsed = false; + } + if (NULL != god->contract_terms) + { + TALER_MERCHANT_contract_free (god->contract_terms); + god->contract_terms = NULL; + } + GNUNET_assert (! god->suspended); + god->contract_parsed = false; + god->contract_match = false; + god->token_match = false; + god->contract_available = false; + god->phase = GOP_LOOKUP_TERMS; + god->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (god_head, + god_tail, + god); + MHD_suspend_connection (god->sc.con); +} + + +/** + * Clean up the session state for a GET /orders/$ID request. + * + * @param cls must be a `struct GetOrderData *` + */ +static void +god_cleanup (void *cls) +{ + struct GetOrderData *god = cls; + + if (NULL != god->contract_terms_json) + { + json_decref (god->contract_terms_json); + god->contract_terms_json = NULL; + } + if (NULL != god->contract_terms) + { + TALER_MERCHANT_contract_free (god->contract_terms); + god->contract_terms = NULL; + } + if (NULL != god->session_eh) + { + TMH_db->event_listen_cancel (god->session_eh); + god->session_eh = NULL; + } + if (NULL != god->refund_eh) + { + TMH_db->event_listen_cancel (god->refund_eh); + god->refund_eh = NULL; + } + if (NULL != god->pay_eh) + { + TMH_db->event_listen_cancel (god->pay_eh); + god->pay_eh = NULL; + } + GNUNET_free (god); +} + + +/** + * Finish the request by returning @a mret as the + * final result. + * + * @param[in,out] god request we are processing + * @param mret MHD result to return + */ +static void +phase_end (struct GetOrderData *god, + MHD_RESULT mret) +{ + god->phase = (MHD_YES == mret) + ? GOP_RETURN_MHD_YES + : GOP_RETURN_MHD_NO; +} + + +/** + * Finish the request by returning an error @a ec + * with HTTP status @a http_status and @a message. + * + * @param[in,out] god request we are processing + * @param http_status HTTP status code to return + * @param ec error code to return + * @param message human readable hint to return, can be NULL + */ +static void +phase_fail (struct GetOrderData *god, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *message) +{ + phase_end (god, + TALER_MHD_reply_with_error (god->sc.con, + http_status, + ec, + message)); +} + + +/** + * We have received a trigger from the database + * that we should (possibly) resume the request. + * + * @param cls a `struct GetOrderData` to resume + * @param extra string encoding refund amount (or NULL) + * @param extra_size number of bytes in @a extra + */ +static void +resume_by_event (void *cls, + const void *extra, + size_t extra_size) +{ + struct GetOrderData *god = cls; + struct GNUNET_AsyncScopeSave old; + + GNUNET_async_scope_enter (&god->hc->async_scope_id, + &old); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received event for %s with argument `%.*s`\n", + god->order_id, + (int) extra_size, + (const char *) extra); + if (! god->suspended) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Not suspended, ignoring event\n"); + GNUNET_async_scope_restore (&old); + return; /* duplicate event is possible */ + } + if (GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout) && + god->sc.awaiting_refund) + { + char *as; + struct TALER_Amount a; + + if (0 == extra_size) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "No amount given, but need refund above threshold\n"); + GNUNET_async_scope_restore (&old); + return; /* not relevant */ + } + as = GNUNET_strndup (extra, + extra_size); + if (GNUNET_OK != + TALER_string_to_amount (as, + &a)) + { + GNUNET_break (0); + GNUNET_async_scope_restore (&old); + GNUNET_free (as); + return; + } + GNUNET_free (as); + if (GNUNET_OK != + TALER_amount_cmp_currency (&god->sc.refund_expected, + &a)) + { + GNUNET_break (0); + GNUNET_async_scope_restore (&old); + return; /* bad currency!? */ + } + if (1 == TALER_amount_cmp (&god->sc.refund_expected, + &a)) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Amount too small to trigger resuming\n"); + GNUNET_async_scope_restore (&old); + return; /* refund too small */ + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming (%s/%s) by event with argument `%.*s`\n", + GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout) + ? "future" + : "past", + god->sc.awaiting_refund + ? "awaiting refund" + : "not waiting for refund", + (int) extra_size, + (const char *) extra); + god->suspended = GNUNET_NO; + GNUNET_CONTAINER_DLL_remove (god_head, + god_tail, + god); + MHD_resume_connection (god->sc.con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + GNUNET_async_scope_restore (&old); +} + + +/** + * First phase (after request parsing). + * Set up long-polling. + * + * @param[in,out] god request context + */ +static void +phase_init (struct GetOrderData *god) +{ + god->phase++; + if (god->generate_html) + return; /* If HTML is requested, we never actually long poll. */ + if (! GNUNET_TIME_absolute_is_future (god->sc.long_poll_timeout)) + return; /* long polling not requested */ + + if (god->sc.awaiting_refund || + god->sc.awaiting_refund_obtained) + { + struct TMH_OrderPayEventP refund_eh = { + .header.size = htons (sizeof (refund_eh)), + .header.type = htons (god->sc.awaiting_refund_obtained + ? TALER_DBEVENT_MERCHANT_REFUND_OBTAINED + : TALER_DBEVENT_MERCHANT_ORDER_REFUND), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_CRYPTO_hash (god->order_id, + strlen (god->order_id), + &refund_eh.h_order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing %p to refunds on %s\n", + god, + god->order_id); + god->refund_eh + = TMH_db->event_listen ( + TMH_db->cls, + &refund_eh.header, + GNUNET_TIME_absolute_get_remaining ( + god->sc.long_poll_timeout), + &resume_by_event, + god); + } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing to payments on %s\n", + god->order_id); + GNUNET_CRYPTO_hash (god->order_id, + strlen (god->order_id), + &pay_eh.h_order_id); + god->pay_eh + = TMH_db->event_listen ( + TMH_db->cls, + &pay_eh.header, + GNUNET_TIME_absolute_get_remaining ( + god->sc.long_poll_timeout), + &resume_by_event, + god); + } +} + + +/** + * Lookup contract terms and check client has the + * right to access this order (by claim token or + * contract hash). + * + * @param[in,out] god request context + */ +static void +phase_lookup_terms (struct GetOrderData *god) +{ + uint64_t order_serial; + struct TALER_ClaimTokenP db_claim_token; + + /* Convert order_id to h_contract_terms */ + TMH_db->preflight (TMH_db->cls); + GNUNET_assert (NULL == god->contract_terms_json); + + { + enum GNUNET_DB_QueryStatus qs; + + bool paid; + bool wired; + bool session_matches; + qs = TMH_db->lookup_contract_terms3 ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + NULL, + &god->contract_terms_json, + &order_serial, + &paid, + &wired, + &session_matches, + &db_claim_token, + &god->choice_index); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_contract_terms"); + return; + } + /* Note: when "!ord.requireClaimToken" and the client does not provide + a claim token (all zeros!), then token_match==TRUE below: */ + god->token_match + = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + && (0 == GNUNET_memcmp (&db_claim_token, + &god->claim_token)); + } + + /* Check if client provided the right hash code of the contract terms */ + if (NULL != god->contract_terms_json) + { + god->contract_available = true; + if (GNUNET_YES == + GNUNET_is_zero (&god->h_contract_terms)) + { + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms_json, + &god->h_contract_terms)) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + return; + } + } + else + { + struct TALER_PrivateContractHashP h; + + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms_json, + &h)) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + return; + } + god->contract_match = (0 == + GNUNET_memcmp (&h, + &god->h_contract_terms)); + if (! god->contract_match) + { + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + return; + } + } + } + + if (god->contract_available) + { + god->claimed = true; + } + else + { + struct TALER_MerchantPostDataHashP unused; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_order ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &db_claim_token, + &unused, + (NULL == god->contract_terms_json) + ? &god->contract_terms_json + : NULL); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order"); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unknown order id given: `%s'\n", + god->order_id); + phase_fail (god, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + god->order_id); + return; + } + /* Note: when "!ord.requireClaimToken" and the client does not provide + a claim token (all zeros!), then token_match==TRUE below: */ + god->token_match + = (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && + (0 == GNUNET_memcmp (&db_claim_token, + &god->claim_token)); + } /* end unclaimed order logic */ + god->phase++; +} + + +/** + * Parse contract terms. + * + * @param[in,out] god request context + */ +static void +phase_parse_contract (struct GetOrderData *god) +{ + GNUNET_break (NULL == god->contract_terms); + god->contract_terms = TALER_MERCHANT_contract_parse ( + god->contract_terms_json, + true); + + if (NULL == god->contract_terms) + { + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + god->order_id); + return; + } + god->contract_parsed = true; + if ( (NULL != god->session_id) && + (NULL != god->contract_terms->fulfillment_url) && + (NULL == god->session_eh) ) + { + struct TMH_SessionEventP session_eh = { + .header.size = htons (sizeof (session_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), + .merchant_pub = god->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing to session triggers for %p\n", + god); + GNUNET_CRYPTO_hash (god->session_id, + strlen (god->session_id), + &session_eh.h_session_id); + GNUNET_CRYPTO_hash (god->contract_terms->fulfillment_url, + strlen (god->contract_terms->fulfillment_url), + &session_eh.h_fulfillment_url); + god->session_eh + = TMH_db->event_listen ( + TMH_db->cls, + &session_eh.header, + GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout), + &resume_by_event, + god); + } + god->phase++; +} + + +/** + * Check that this order is unclaimed or claimed by + * this client. + * + * @param[in,out] god request context + */ +static void +phase_check_client_access (struct GetOrderData *god) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Token match: %d, contract_available: %d, contract match: %d, claimed: %d\n", + god->token_match, + god->contract_available, + god->contract_match, + god->claimed); + + if (god->claim_token_provided && ! god->token_match) + { + /* Authentication provided but wrong. */ + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, + "authentication with claim token provided but wrong"); + return; + } + + if (god->h_contract_provided && ! god->contract_match) + { + /* Authentication provided but wrong. */ + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH, + NULL); + return; + } + + if (! (god->token_match || + god->contract_match) ) + { + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Neither claim token nor contract matched\n"); + /* Client has no rights to this order */ + if (NULL == god->contract_terms->public_reorder_url) + { + /* We cannot give the client a new order, just fail */ + if (! GNUNET_is_zero (&god->h_contract_terms)) + { + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + return; + } + GNUNET_break_op (0); + phase_fail (god, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_TOKEN, + "no 'public_reorder_url'"); + return; + } + /* We have a fulfillment URL, redirect the client there, maybe + the frontend can generate a fresh order for this new customer */ + if (god->generate_html) + { + /* Contract was claimed (maybe by another device), so this client + cannot get the status information. Redirect to fulfillment page, + where the client may be able to pickup a fresh order -- or might + be able authenticate via session ID */ + struct MHD_Response *reply; + MHD_RESULT ret; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Contract claimed, redirecting to fulfillment page for order %s\n", + god->order_id); + reply = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + if (NULL == reply) + { + GNUNET_break (0); + phase_end (god, + MHD_NO); + return; + } + GNUNET_break (MHD_YES == + MHD_add_response_header ( + reply, + MHD_HTTP_HEADER_LOCATION, + god->contract_terms->public_reorder_url)); + ret = MHD_queue_response (god->sc.con, + MHD_HTTP_FOUND, + reply); + MHD_destroy_response (reply); + phase_end (god, + ret); + return; + } + /* Need to generate JSON reply */ + phase_end (god, + TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_string ( + "public_reorder_url", + god->contract_terms->public_reorder_url))); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Claim token or contract matched\n"); + god->phase++; +} + + +/** + * Return the order summary of the contract of @a god in the + * preferred language of the HTTP client. + * + * @param god order to extract summary from + * @return dummy error message summary if no summary was provided in the contract + */ +static const char * +get_order_summary (const struct GetOrderData *god) +{ + const char *language_pattern; + const char *ret; + + language_pattern = MHD_lookup_connection_value (god->sc.con, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL == language_pattern) + language_pattern = "en"; + ret = json_string_value (TALER_JSON_extract_i18n (god->contract_terms_json, + language_pattern, + "summary")); + if (NULL == ret) + { + /* Upon order creation (and insertion into the database), the presence + of a summary should have been checked. So if we get here, someone + did something fishy to our database... */ + GNUNET_break (0); + ret = "<bug: no summary>"; + } + return ret; +} + + +/** + * The client did not yet pay, send it the payment request. + * + * @param god check pay request context + * @param already_paid_order_id if for the fulfillment URI there is + * already a paid order, this is the order ID to redirect + * the wallet to; NULL if not applicable + * @return true to exit due to suspension + */ +static bool +send_pay_request (struct GetOrderData *god, + const char *already_paid_order_id) +{ + MHD_RESULT ret; + char *taler_pay_uri; + char *order_status_url; + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); + if ( (! GNUNET_TIME_relative_is_zero (remaining)) && + (NULL == already_paid_order_id) ) + { + /* long polling: do not queue a response, suspend connection instead */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending request: long polling for payment\n"); + suspend_god (god); + return true; + } + + /* Check if resource_id has been paid for in the same session + * with another order_id. + */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending payment request\n"); + taler_pay_uri = TMH_make_taler_pay_uri ( + god->sc.con, + god->order_id, + god->session_id, + god->hc->instance->settings.id, + &god->claim_token); + order_status_url = TMH_make_order_status_url ( + god->sc.con, + god->order_id, + god->session_id, + god->hc->instance->settings.id, + &god->claim_token, + NULL); + if ( (NULL == taler_pay_uri) || + (NULL == order_status_url) ) + { + GNUNET_break_op (0); + GNUNET_free (taler_pay_uri); + GNUNET_free (order_status_url); + phase_fail (god, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + "host"); + return false; + } + if (god->generate_html) + { + if (NULL != already_paid_order_id) + { + struct MHD_Response *reply; + + GNUNET_assert (NULL != god->contract_terms->fulfillment_url); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Redirecting to already paid order %s via fulfillment URL %s\n", + already_paid_order_id, + god->contract_terms->fulfillment_url); + reply = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + if (NULL == reply) + { + GNUNET_break (0); + phase_end (god, + MHD_NO); + return false; + } + GNUNET_break (MHD_YES == + MHD_add_response_header ( + reply, + MHD_HTTP_HEADER_LOCATION, + god->contract_terms->fulfillment_url)); + { + ret = MHD_queue_response (god->sc.con, + MHD_HTTP_FOUND, + reply); + MHD_destroy_response (reply); + phase_end (god, + ret); + return false; + } + } + + { + char *qr; + + qr = TMH_create_qrcode (taler_pay_uri); + if (NULL == qr) + { + GNUNET_break (0); + phase_end (god, + MHD_NO); + return false; + } + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + context = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("taler_pay_uri", + taler_pay_uri), + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + GNUNET_JSON_pack_string ("taler_pay_qrcode_svg", + qr), + GNUNET_JSON_pack_string ("order_summary", + get_order_summary (god))); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_PAYMENT_REQUIRED, + "request_payment", + god->hc->instance->settings.id, + taler_pay_uri, + context); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + ret = MHD_NO; + } + else + { + ret = MHD_YES; + } + json_decref (context); + } + GNUNET_free (qr); + } + } + else /* end of 'generate HTML' */ + { + ret = TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_PAYMENT_REQUIRED, + GNUNET_JSON_pack_string ("taler_pay_uri", + taler_pay_uri), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + god->contract_terms->fulfillment_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("already_paid_order_id", + already_paid_order_id))); + } + GNUNET_free (taler_pay_uri); + GNUNET_free (order_status_url); + phase_end (god, + ret); + return false; +} + + +/** + * Check if the order has been paid. + * + * @param[in,out] god request context + */ +static void +phase_check_paid (struct GetOrderData *god) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_PrivateContractHashP h_contract; + + god->paid = false; + qs = TMH_db->lookup_order_status ( + TMH_db->cls, + god->hc->instance->settings.id, + god->order_id, + &h_contract, + &god->paid); + if (0 > qs) + { + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order_status"); + return; + } + god->phase++; +} + + +/** + * Check if the client already paid for an equivalent + * order under this session, and if so redirect to + * that order. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_redirect_to_paid_order (struct GetOrderData *god) +{ + if ( (NULL != god->session_id) && + (NULL != god->contract_terms->fulfillment_url) ) + { + /* Check if client paid for this fulfillment article + already within this session, but using a different + order ID. If so, redirect the client to the order + it already paid. Allows, for example, the case + where a mobile phone pays for a browser's session, + where the mobile phone has a different order + ID (because it purchased the article earlier) + than the one that the browser is waiting for. */ + char *already_paid_order_id = NULL; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Running re-purchase detection for %s/%s\n", + god->session_id, + god->contract_terms->fulfillment_url); + qs = TMH_db->lookup_order_by_fulfillment ( + TMH_db->cls, + god->hc->instance->settings.id, + god->contract_terms->fulfillment_url, + god->session_id, + TALER_EXCHANGE_YNA_NO != god->allow_refunded_for_repurchase, + &already_paid_order_id); + if (qs < 0) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order by fulfillment"); + return false; + } + if ( (! god->paid) && + ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) || + (0 != strcmp (god->order_id, + already_paid_order_id)) ) ) + { + bool ret; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending pay request for order %s (already paid: %s)\n", + god->order_id, + already_paid_order_id); + ret = send_pay_request (god, + already_paid_order_id); + GNUNET_free (already_paid_order_id); + return ret; + } + GNUNET_free (already_paid_order_id); + } + god->phase++; + return false; +} + + +/** + * Check if the order has been paid, and if not + * request payment. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_handle_unpaid (struct GetOrderData *god) +{ + if (god->paid) + { + god->phase++; + return false; + } + if (god->claimed) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order claimed but unpaid, sending pay request for order %s\n", + god->order_id); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order unclaimed, sending pay request for order %s\n", + god->order_id); + } + return send_pay_request (god, + NULL); +} + + +/** + * Function called with detailed information about a refund. + * It is responsible for packing up the data to return. + * + * @param cls closure + * @param refund_serial unique serial number of the refund + * @param timestamp time of the refund (for grouping of refunds in the wallet UI) + * @param coin_pub public coin from which the refund comes from + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param rtransaction_id identificator of the refund + * @param reason human-readable explanation of the refund + * @param refund_amount refund amount which is being taken from @a coin_pub + * @param pending true if the this refund was not yet processed by the wallet/exchange + */ +static void +process_refunds_cb (void *cls, + uint64_t refund_serial, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + uint64_t rtransaction_id, + const char *reason, + const struct TALER_Amount *refund_amount, + bool pending) +{ + struct GetOrderData *god = cls; + + (void) refund_serial; + (void) timestamp; + (void) exchange_url; + (void) rtransaction_id; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Found refund of %s for coin %s with reason `%s' in database\n", + TALER_amount2s (refund_amount), + TALER_B2S (coin_pub), + reason); + god->refund_pending |= pending; + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&god->refund_taken, + refund_amount)) || + (GNUNET_OK != + TALER_amount_cmp_currency (&god->refund_amount, + refund_amount)) ) + { + god->bad_refund_currency_in_db = true; + return; + } + if (! pending) + { + GNUNET_assert (0 <= + TALER_amount_add (&god->refund_taken, + &god->refund_taken, + refund_amount)); + } + GNUNET_assert (0 <= + TALER_amount_add (&god->refund_amount, + &god->refund_amount, + refund_amount)); + god->refunded = true; +} + + +/** + * Check if the order has been refunded. + * + * @param[in,out] god request context + * @return true to exit due to suspension + */ +static bool +phase_check_refunded (struct GetOrderData *god) +{ + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount refund_amount; + const char *refund_currency; + + switch (god->contract_terms->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + refund_amount = god->contract_terms->details.v0.brutto; + refund_currency = god->contract_terms->details.v0.brutto.currency; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + if (god->choice_index < 0) + { + // order was not paid, no refund to be checked + god->phase++; + return false; + } + GNUNET_assert (god->choice_index < + god->contract_terms->details.v1.choices_len); + refund_currency = god->contract_terms->details.v1.choices[god->choice_index] + .amount.currency; + GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (refund_currency, + &refund_amount)); + break; + default: + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION, + NULL); + return false; + } + } + + if ( (god->sc.awaiting_refund) && + (GNUNET_OK != + TALER_amount_cmp_currency (&refund_amount, + &god->sc.refund_expected)) ) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + refund_currency); + return false; + } + + /* At this point, we know the contract was paid. Let's check for + refunds. First, clear away refunds found from previous invocations. */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (refund_currency, + &god->refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (refund_currency, + &god->refund_taken)); + qs = TMH_db->lookup_refunds_detailed ( + TMH_db->cls, + god->hc->instance->settings.id, + &god->h_contract_terms, + &process_refunds_cb, + god); + if (0 > qs) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_refunds_detailed"); + return false; + } + if (god->bad_refund_currency_in_db) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "currency mix-up between contract price and refunds in database"); + return false; + } + if ( ((god->sc.awaiting_refund) && + ( (! god->refunded) || + (1 != TALER_amount_cmp (&god->refund_amount, + &god->sc.refund_expected)) )) || + ( (god->sc.awaiting_refund_obtained) && + (god->refund_pending) ) ) + { + /* Client is waiting for a refund larger than what we have, suspend + until timeout */ + struct GNUNET_TIME_Relative remaining; + + remaining = GNUNET_TIME_absolute_get_remaining (god->sc.long_poll_timeout); + if ( (! GNUNET_TIME_relative_is_zero (remaining)) && + (! god->generate_html) ) + { + /* yes, indeed suspend */ + if (god->sc.awaiting_refund) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting refund exceeding %s\n", + TALER_amount2s (&god->sc.refund_expected)); + if (god->sc.awaiting_refund_obtained) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting pending refunds\n"); + suspend_god (god); + return true; + } + } + god->phase++; + return false; +} + + +/** + * Create a taler://refund/ URI for the given @a con and @a order_id + * and @a instance_id. + * + * @param merchant_base_url URL to take host and path from; + * we cannot take it from the MHD connection as a browser + * may have changed 'http' to 'https' and we MUST be consistent + * with what the merchant's frontend used initially + * @param order_id the order id + * @return corresponding taler://refund/ URI, or NULL on missing "host" + */ +static char * +make_taler_refund_uri (const char *merchant_base_url, + const char *order_id) +{ + struct GNUNET_Buffer buf = { 0 }; + char *url; + struct GNUNET_Uri uri; + + url = GNUNET_strdup (merchant_base_url); + if (-1 == GNUNET_uri_parse (&uri, + url)) + { + GNUNET_break (0); + GNUNET_free (url); + return NULL; + } + GNUNET_assert (NULL != order_id); + GNUNET_buffer_write_str (&buf, + "taler"); + if (0 == strcasecmp ("http", + uri.scheme)) + GNUNET_buffer_write_str (&buf, + "+http"); + GNUNET_buffer_write_str (&buf, + "://refund/"); + GNUNET_buffer_write_str (&buf, + uri.host); + if (0 != uri.port) + GNUNET_buffer_write_fstr (&buf, + ":%u", + (unsigned int) uri.port); + if (NULL != uri.path) + GNUNET_buffer_write_path (&buf, + uri.path); + GNUNET_buffer_write_path (&buf, + order_id); + GNUNET_buffer_write_path (&buf, + ""); // Trailing slash + GNUNET_free (url); + return GNUNET_buffer_reap_str (&buf); +} + + +/** + * Generate the order status response. + * + * @param[in,out] god request context + */ +static void +phase_return_status (struct GetOrderData *god) +{ + /* All operations done, build final response */ + if (! god->generate_html) + { + phase_end (god, + TALER_MHD_REPLY_JSON_PACK ( + god->sc.con, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + god->contract_terms->fulfillment_url + )), + GNUNET_JSON_pack_bool ("refunded", + god->refunded), + GNUNET_JSON_pack_bool ("refund_pending", + god->refund_pending), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount))); + return; + } + + if (god->refund_pending) + { + char *qr; + char *uri; + + GNUNET_assert (NULL != god->contract_terms_json); + uri = make_taler_refund_uri (god->contract_terms->merchant_base_url, + god->order_id); + if (NULL == uri) + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "refund URI"); + return; + } + qr = TMH_create_qrcode (uri); + if (NULL == qr) + { + GNUNET_break (0); + GNUNET_free (uri); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "qr code"); + return; + } + + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + context = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_summary", + get_order_summary (god)), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), + GNUNET_JSON_pack_string ("taler_refund_uri", + uri), + GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", + qr)); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_OK, + "offer_refund", + god->hc->instance->settings.id, + uri, + context); + GNUNET_break (GNUNET_OK == res); + json_decref (context); + phase_end (god, + (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES); + } + GNUNET_free (uri); + GNUNET_free (qr); + return; + } + + { + enum GNUNET_GenericReturnValue res; + json_t *context; + + context = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("contract_terms", + god->contract_terms_json), + GNUNET_JSON_pack_string ("order_summary", + get_order_summary (god)), + TALER_JSON_pack_amount ("refund_amount", + &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken)); + res = TALER_TEMPLATING_reply ( + god->sc.con, + MHD_HTTP_OK, + "show_order_details", + god->hc->instance->settings.id, + NULL, + context); + GNUNET_break (GNUNET_OK == res); + json_decref (context); + phase_end (god, + (GNUNET_SYSERR == res) + ? MHD_NO + : MHD_YES); + } +} + + +MHD_RESULT +TMH_get_orders_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct GetOrderData *god = hc->ctx; + + (void) rh; + if (NULL == god) + { + god = GNUNET_new (struct GetOrderData); + hc->ctx = god; + hc->cc = &god_cleanup; + god->sc.con = connection; + god->hc = hc; + god->order_id = hc->infix; + god->generate_html + = TMH_MHD_test_html_desired (connection); + + /* first-time initialization / sanity checks */ + TALER_MHD_parse_request_arg_auto (connection, + "h_contract", + &god->h_contract_terms, + god->h_contract_provided); + TALER_MHD_parse_request_arg_auto (connection, + "token", + &god->claim_token, + god->claim_token_provided); + if (! (TALER_MHD_arg_to_yna (connection, + "allow_refunded_for_repurchase", + TALER_EXCHANGE_YNA_NO, + &god->allow_refunded_for_repurchase)) ) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "allow_refunded_for_repurchase"); + god->session_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + + /* process await_refund_obtained argument */ + { + const char *await_refund_obtained_s; + + await_refund_obtained_s = + MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "await_refund_obtained"); + god->sc.awaiting_refund_obtained = + (NULL != await_refund_obtained_s) + ? 0 == strcasecmp (await_refund_obtained_s, + "yes") + : false; + if (god->sc.awaiting_refund_obtained) + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting refund obtained\n"); + } + + TALER_MHD_parse_request_amount (connection, + "refund", + &god->sc.refund_expected); + if (TALER_amount_is_valid (&god->sc.refund_expected)) + { + god->sc.awaiting_refund = true; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Awaiting minimum refund of %s\n", + TALER_amount2s (&god->sc.refund_expected)); + } + TALER_MHD_parse_request_timeout (connection, + &god->sc.long_poll_timeout); + } + + if (GNUNET_SYSERR == god->suspended) + return MHD_NO; /* we are in shutdown */ + if (GNUNET_YES == god->suspended) + { + god->suspended = GNUNET_NO; + GNUNET_CONTAINER_DLL_remove (god_head, + god_tail, + god); + } + + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling request in phase %d\n", + (int) god->phase); + switch (god->phase) + { + case GOP_INIT: + phase_init (god); + break; + case GOP_LOOKUP_TERMS: + phase_lookup_terms (god); + break; + case GOP_PARSE_CONTRACT: + phase_parse_contract (god); + break; + case GOP_CHECK_CLIENT_ACCESS: + phase_check_client_access (god); + break; + case GOP_CHECK_PAID: + phase_check_paid (god); + break; + case GOP_REDIRECT_TO_PAID_ORDER: + if (phase_redirect_to_paid_order (god)) + return MHD_YES; + break; + case GOP_HANDLE_UNPAID: + if (phase_handle_unpaid (god)) + return MHD_YES; + break; + case GOP_CHECK_REFUNDED: + if (phase_check_refunded (god)) + return MHD_YES; + break; + case GOP_RETURN_STATUS: + phase_return_status (god); + break; + case GOP_RETURN_MHD_YES: + return MHD_YES; + case GOP_RETURN_MHD_NO: + return MHD_NO; + } + } +} + + +/* end of taler-merchant-httpd_get-orders-ORDER_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-orders-ORDER_ID.h b/src/backend/taler-merchant-httpd_get-orders-ORDER_ID.h @@ -0,0 +1,47 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2016, 2017, 2020 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-httpd_get-orders-ORDER_ID.h + * @brief implementation of GET /orders/$ID + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_GET_ORDERS_ID_H +#define TALER_MERCHANT_HTTPD_GET_ORDERS_ID_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Force resuming all suspended order lookups, needed during shutdown. + */ +void +TMH_force_wallet_get_order_resume (void); + + +/** + * Handle a GET "/orders/$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_get_orders_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-accounts-H_WIRE.c b/src/backend/taler-merchant-httpd_get-private-accounts-H_WIRE.c @@ -0,0 +1,109 @@ +/* + This file is part of TALER + (C) 2023, 2026 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-httpd_get-private-accounts-H_WIRE.c + * @brief implement GET /accounts/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-accounts-H_WIRE.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/accounts/$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_accounts_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *h_wire_s = hc->infix; + struct TALER_MerchantWireHashP h_wire; + struct TALER_MERCHANTDB_AccountDetails tp = { 0 }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != h_wire_s); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (h_wire_s, + strlen (h_wire_s), + &h_wire, + sizeof (h_wire))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_GENERIC_H_WIRE_MALFORMED, + h_wire_s); + } + qs = TMH_db->select_account (TMH_db->cls, + mi->settings.id, + &h_wire, + &tp); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_account"); + } + if (0 == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ACCOUNT_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_bool ("active", + tp.active), + TALER_JSON_pack_full_payto ("payto_uri", + tp.payto_uri), + GNUNET_JSON_pack_data_auto ("h_wire", + &tp.h_wire), + GNUNET_JSON_pack_data_auto ("salt", + &tp.salt), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("extra_wire_subject_metadata", + tp.extra_wire_subject_metadata)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("credit_facade_url", + tp.credit_facade_url))); + /* We do not return the credentials, as they may + be sensitive */ + json_decref (tp.credit_facade_credentials); + GNUNET_free (tp.extra_wire_subject_metadata); + GNUNET_free (tp.payto_uri.full_payto); + GNUNET_free (tp.credit_facade_url); + return ret; + } +} + + +/* end of taler-merchant-httpd_get-private-accounts-H_WIRE.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-accounts-H_WIRE.h b/src/backend/taler-merchant-httpd_get-private-accounts-H_WIRE.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-accounts-H_WIRE.h + * @brief implement GET /accounts/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/accounts/$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_accounts_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-accounts-H_WIRE.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-accounts.c b/src/backend/taler-merchant-httpd_get-private-accounts.c @@ -0,0 +1,84 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-accounts.c + * @brief implement GET /accounts + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-accounts.h" +#include <taler/taler_json_lib.h> + + +/** + * Add account details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param merchant_priv private key of the merchant instance + * @param ad details about the account + */ +static void +add_account (void *cls, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_AccountDetails *ad) +{ + json_t *pa = cls; + + (void) merchant_priv; + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_bool ("active", + ad->active), + TALER_JSON_pack_full_payto ("payto_uri", + ad->payto_uri), + GNUNET_JSON_pack_data_auto ("h_wire", + &ad->h_wire)))); +} + + +MHD_RESULT +TMH_private_get_accounts (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->select_accounts (TMH_db->cls, + hc->instance->settings.id, + &add_account, + pa); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("accounts", + pa)); +} + + +/* end of taler-merchant-httpd_get-private-accounts.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-accounts.h b/src/backend/taler-merchant-httpd_get-private-accounts.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-accounts.h + * @brief implement GET /accounts + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/accounts" 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_accounts (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-accounts.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-categories-CATEGORY_ID.c b/src/backend/taler-merchant-httpd_get-private-categories-CATEGORY_ID.c @@ -0,0 +1,121 @@ +/* + This file is part of TALER + (C) 2022-2024 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-httpd_get-private-categories-CATEGORY_ID.c + * @brief implement GET /private/categories/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-categories-CATEGORY_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/private/categories/$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_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long cnum; + char dummy; + struct TALER_MERCHANTDB_CategoryDetails cd; + size_t num_products = 0; + char *products = NULL; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &cnum, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "category_id must be a number"); + } + + qs = TMH_db->select_category (TMH_db->cls, + mi->settings.id, + cnum, + &cd, + &num_products, + &products); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_category"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + json_t *jproducts; + const char *pos = products; + + jproducts = json_array (); + GNUNET_assert (NULL != jproducts); + for (unsigned int i = 0; i<num_products; i++) + { + const char *product_id = pos; + json_t *jprod; + + pos = pos + strlen (product_id) + 1; + jprod = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + product_id)); + GNUNET_assert (0 == + json_array_append_new (jproducts, + jprod)); + } + GNUNET_free (products); + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("name", + cd.category_name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("name_i18n", + cd.category_name_i18n)), + GNUNET_JSON_pack_array_steal ("products", + jproducts)); + TALER_MERCHANTDB_category_details_free (&cd); + return ret; + } +} + + +/* end of taler-merchant-httpd_get-private-categories-CATEGORY_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-categories-CATEGORY_ID.h b/src/backend/taler-merchant-httpd_get-private-categories-CATEGORY_ID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_get-private-categories-CATEGORY_ID.h + * @brief implement GET /private/categories/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/private/categories/$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_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-categories-CATEGORY_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-categories.c b/src/backend/taler-merchant-httpd_get-private-categories.c @@ -0,0 +1,93 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_get-private-categories.c + * @brief implement GET /categories + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-categories.h" + + +/** + * Add category details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param category_id ID of the category + * @param category_name name of the category + * @param category_name_i18n translations of the @a category_name + * @param product_count number of products in the category + */ +static void +add_category (void *cls, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t product_count) +{ + json_t *pa = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ( + "category_id", + category_id), + GNUNET_JSON_pack_string ( + "name", + category_name), + GNUNET_JSON_pack_object_incref ( + "name_i18n", + (json_t *) category_name_i18n), + GNUNET_JSON_pack_uint64 ( + "product_count", + product_count)))); +} + + +MHD_RESULT +TMH_private_get_categories (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->lookup_categories (TMH_db->cls, + hc->instance->settings.id, + &add_category, + pa); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("categories", + pa)); +} + + +/* end of taler-merchant-httpd_get-private-categories.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-categories.h b/src/backend/taler-merchant-httpd_get-private-categories.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_get-private-categories.h + * @brief implement GET /private/categories + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/private/categories" 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_categories ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-categories.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-donau.c b/src/backend/taler-merchant-httpd_get-private-donau.c @@ -0,0 +1,122 @@ +/* + This file is part of TALER + Copyright (C) 2024 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-httpd_get-private-donau.c + * @brief implementation of GET /donau + * @author Bohdan Potuzhnyi + * @author Vlada Svirsh + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include "taler/taler_merchant_donau.h" +#include "taler/taler_merchant_service.h" +#include "taler-merchant-httpd_get-private-donau.h" + + +/** + * Add details about a Donau instance to the JSON array. + * + * @param cls json array to which the Donau instance details will be added + * @param donau_instance_serial the serial number of the Donau instance + * @param donau_url the URL of the Donau instance + * @param charity_name the name of the charity + * @param charity_pub_key the public key of the charity + * @param charity_id the ID of the charity + * @param charity_max_per_year the maximum donation amount per year + * @param charity_receipts_to_date the total donations received so far this year + * @param current_year the current year being tracked for donations + * @param donau_keys_json JSON object with key information specific to the Donau instance, NULL if not (yet) available. + */ +static void +add_donau_instance (void *cls, + uint64_t donau_instance_serial, + const char *donau_url, + const char *charity_name, + const struct DONAU_CharityPublicKeyP *charity_pub_key, + uint64_t charity_id, + const struct TALER_Amount *charity_max_per_year, + const struct TALER_Amount *charity_receipts_to_date, + int64_t current_year, + const json_t *donau_keys_json) +{ + json_t *json_instances = cls; + + GNUNET_assert ( + 0 == json_array_append_new ( + json_instances, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("donau_instance_serial", + donau_instance_serial), + GNUNET_JSON_pack_string ("donau_url", + donau_url), + GNUNET_JSON_pack_string ("charity_name", + charity_name), + GNUNET_JSON_pack_data_auto ("charity_pub_key", + charity_pub_key), + GNUNET_JSON_pack_uint64 ("charity_id", + charity_id), + TALER_JSON_pack_amount ("charity_max_per_year", + charity_max_per_year), + TALER_JSON_pack_amount ("charity_receipts_to_date", + charity_receipts_to_date), + GNUNET_JSON_pack_int64 ("current_year", + current_year), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("donau_keys_json", + (json_t *) donau_keys_json)) + ))); +} + + +/** + * Handle a GET "/donau" 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_donau_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *json_donau_instances = json_array (); + enum GNUNET_DB_QueryStatus qs; + + TMH_db->preflight (TMH_db->cls); + qs = TMH_db->select_donau_instances (TMH_db->cls, + hc->instance->settings.id, + &add_donau_instance, + json_donau_instances); + if (0 > qs) + { + GNUNET_break (0); + json_decref (json_donau_instances); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ( + "donau_instances", + json_donau_instances)); +} diff --git a/src/backend/taler-merchant-httpd_get-private-donau.h b/src/backend/taler-merchant-httpd_get-private-donau.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + Copyright (C) 2024 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-httpd_get-private-donau.h + * @brief implementation of GET /donau + * @author Bohdan Potuzhnyi + * @author Vlada Svirsh + */ + +#ifndef TALER_MERCHANT_HTTPD_GET_DONAU_INSTANCES_H +#define TALER_MERCHANT_HTTPD_GET_DONAU_INSTANCES_H + +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + +/** + * Handle a GET "/donau" 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_donau_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-groups.c b/src/backend/taler-merchant-httpd_get-private-groups.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 taler-merchant-httpd_get-private-groups.c + * @brief implementation of GET /private/groups + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-groups.h" +#include <taler/taler_json_lib.h> + +/** + * Sensible bound on the number of results to return + */ +#define MAX_DELTA 1024 + + +/** + * Callback for listing product groups. + * + * @param cls closure with a `json_t *` + * @param product_group_id unique identifier of the group + * @param group_name name of the group + * @param group_description human-readable description + */ +static void +add_group (void *cls, + uint64_t product_group_id, + const char *group_name, + const char *group_description) +{ + json_t *groups = cls; + json_t *entry; + + entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("group_serial", + product_group_id), + GNUNET_JSON_pack_string ("group_name", + group_name), + GNUNET_JSON_pack_string ("description", + group_description)); + GNUNET_assert (NULL != entry); + GNUNET_assert (0 == + json_array_append_new (groups, + entry)); +} + + +MHD_RESULT +TMH_private_get_groups (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + int64_t limit = -20; + uint64_t offset; + json_t *groups; + + (void) rh; + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + 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"); + } + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + + groups = json_array (); + GNUNET_assert (NULL != groups); + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->select_product_groups (TMH_db->cls, + hc->instance->settings.id, + limit, + offset, + &add_group, + groups); + if (qs < 0) + { + GNUNET_break (0); + json_decref (groups); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_product_groups"); + } + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("groups", + groups)); +} diff --git a/src/backend/taler-merchant-httpd_get-private-groups.h b/src/backend/taler-merchant-httpd_get-private-groups.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 taler-merchant-httpd_get-private-groups.h + * @brief HTTP serving layer for listing product groups + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_GROUPS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_GROUPS_H + +#include "taler-merchant-httpd.h" + +/** + * Handle GET /private/groups 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_groups (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-incoming-ID.c b/src/backend/taler-merchant-httpd_get-private-incoming-ID.c @@ -0,0 +1,236 @@ +/* + This file is part of TALER + (C) 2026 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-httpd_get-private-incoming-ID.c + * @brief implement API for obtaining details about an expected incoming wire transfer + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_get-private-incoming-ID.h" + + +/** + * Function called with information about orders aggregated into + * a wire transfer. + * Generate a response (array entry) based on the given arguments. + * + * @param cls closure with a `json_t *` array to build up the response + * @param order_id ID of the order that was paid and aggregated + * @param remaining_deposit deposited amount minus any refunds + * @param deposit_fee deposit fees paid to the exchange for the order + */ +static void +reconciliation_cb (void *cls, + const char *order_id, + const struct TALER_Amount *remaining_deposit, + const struct TALER_Amount *deposit_fee) +{ + json_t *rd = cls; + json_t *r; + + r = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_id", + order_id), + TALER_JSON_pack_amount ("remaining_deposit", + remaining_deposit), + TALER_JSON_pack_amount ("deposit_fee", + deposit_fee)); + GNUNET_assert (0 == + json_array_append_new (rd, + r)); +} + + +/** + * Manages a GET /private/incoming call. + * + * @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_incoming_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + unsigned long long serial_id; + struct TALER_Amount wire_fee; + bool no_fee; + struct GNUNET_TIME_Timestamp expected_time; + struct TALER_Amount expected_credit_amount; + struct TALER_WireTransferIdentifierRawP wtid; + struct TALER_FullPayto payto_uri; + char *exchange_url = NULL; + struct GNUNET_TIME_Timestamp execution_time; + bool confirmed; + + { + char dummy; + + if (1 != + sscanf (hc->infix, + "%llu%c", + &serial_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "transfer ID must be a number"); + } + } + + TMH_db->preflight (TMH_db->cls); + { + struct TALER_MasterPublicKeyP master_pub; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_expected_transfer (TMH_db->cls, + hc->instance->settings.id, + serial_id, + &expected_time, + &expected_credit_amount, + &wtid, + &payto_uri, + &exchange_url, + &execution_time, + &confirmed, + &master_pub); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_expected_transfer"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_EXPECTED_TRANSFER_UNKNOWN, + hc->infix); + } + + { + char *method; + struct GNUNET_TIME_Timestamp start_date; + struct GNUNET_TIME_Timestamp end_date; + struct TALER_MasterSignatureP master_sig; + struct TALER_WireFeeSet fees; + + method = TALER_payto_get_method (payto_uri.full_payto); + qs = TMH_db->lookup_wire_fee ( + TMH_db->cls, + &master_pub, + method, + expected_time, + &fees, + &start_date, + &end_date, + &master_sig); + GNUNET_free (method); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + GNUNET_free (exchange_url); + GNUNET_free (payto_uri.full_payto); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_wire_fee"); + } + no_fee = (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs); + if (! no_fee) + wire_fee = fees.wire; + } + + } + + { + enum GNUNET_DB_QueryStatus qs; + json_t *rd; + MHD_RESULT mret; + + rd = json_array (); + GNUNET_assert (NULL != rd); + qs = TMH_db->lookup_reconciliation_details (TMH_db->cls, + hc->instance->settings.id, + serial_id, + &reconciliation_cb, + rd); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + GNUNET_free (exchange_url); + GNUNET_free (payto_uri.full_payto); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_reconciliation_details"); + } + + mret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ( + "expected_credit_amount", + TALER_amount_is_valid (&expected_credit_amount) + ? &expected_credit_amount + : NULL)), + GNUNET_JSON_pack_data_auto ("wtid", + &wtid), + TALER_JSON_pack_full_payto ("payto_uri", + payto_uri), + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + GNUNET_JSON_pack_bool ("confirmed", + confirmed), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("execution_time", + execution_time)), + GNUNET_JSON_pack_timestamp ("expected_time", + expected_time), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("wire_fee", + no_fee ? NULL : &wire_fee)), + GNUNET_JSON_pack_array_steal ("reconciliation_details", + rd)); + GNUNET_free (exchange_url); + GNUNET_free (payto_uri.full_payto); + return mret; + } +} + + +/* end of taler-merchant-httpd_get-private-incoming-ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-incoming-ID.h b/src/backend/taler-merchant-httpd_get-private-incoming-ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2026 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/> +*/ +/** + * @file taler-merchant-httpd_get-private-incoming-ID.h + * @brief headers for GET /incoming/$ID handler + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_ID_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Manages a GET /private/incoming/$ID call. + * + * @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_incoming_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-incoming.c b/src/backend/taler-merchant-httpd_get-private-incoming.c @@ -0,0 +1,194 @@ +/* + 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-httpd_get-private-incoming.c + * @brief implement API for obtaining a list of expected incoming wire transfers + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_get-private-incoming.h" + + +/** + * Function called with information about a wire transfer. + * Generate a response (array entry) based on the given arguments. + * + * @param cls closure with a `json_t *` array to build up the response + * @param expected_credit_amount amount expected to be wired to the merchant (minus fees), NULL if unknown + * @param wtid wire transfer identifier + * @param payto_uri target account that received the wire transfer + * @param exchange_url base URL of the exchange that made the wire transfer + * @param expected_transfer_serial_id serial number identifying the transfer in the backend + * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS + * if it did not yet happen + * @param confirmed true if the merchant acknowledged the wire transfer reception + * @param validated true if the reconciliation succeeded + * @param last_http_status HTTP status of our last request to the exchange for this transfer + * @param last_ec last error code we got back (otherwise #TALER_EC_NONE) + * @param last_error_detail last detail we got back (or NULL for none) + */ +static void +incoming_cb (void *cls, + const struct TALER_Amount *expected_credit_amount, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct TALER_FullPayto payto_uri, + const char *exchange_url, + uint64_t expected_transfer_serial_id, + struct GNUNET_TIME_Timestamp execution_time, + bool confirmed, + bool validated, + unsigned int last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_error_detail) +{ + json_t *ja = cls; + json_t *r; + + r = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("expected_credit_amount", + expected_credit_amount)), + GNUNET_JSON_pack_data_auto ("wtid", + wtid), + TALER_JSON_pack_full_payto ("payto_uri", + payto_uri), + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", + expected_transfer_serial_id), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("execution_time", + execution_time)), + GNUNET_JSON_pack_bool ("validated", + validated), + GNUNET_JSON_pack_bool ("confirmed", + confirmed), + GNUNET_JSON_pack_uint64 ("last_http_status", + last_http_status), + GNUNET_JSON_pack_uint64 ("last_ec", + last_ec), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("last_error_detail", + last_error_detail))); + GNUNET_assert (0 == + json_array_append_new (ja, + r)); +} + + +/** + * Manages a GET /private/incoming call. + * + * @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_incoming (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_FullPayto payto_uri = { + .full_payto = NULL + }; + struct GNUNET_TIME_Timestamp before = GNUNET_TIME_UNIT_FOREVER_TS; + struct GNUNET_TIME_Timestamp after = GNUNET_TIME_UNIT_ZERO_TS; + int64_t limit = -20; + uint64_t offset; + enum TALER_EXCHANGE_YesNoAll confirmed; + enum TALER_EXCHANGE_YesNoAll verified; + + (void) rh; + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit < 0) + offset = INT64_MAX; + else + offset = 0; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + TALER_MHD_parse_request_yna (connection, + "verified", + TALER_EXCHANGE_YNA_ALL, + &verified); + TALER_MHD_parse_request_yna (connection, + "confirmed", + TALER_EXCHANGE_YNA_ALL, + &confirmed); + TALER_MHD_parse_request_timestamp (connection, + "before", + &before); + TALER_MHD_parse_request_timestamp (connection, + "after", + &after); + { + const char *esc_payto; + + esc_payto = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "payto_uri"); + if (NULL != esc_payto) + { + payto_uri.full_payto + = GNUNET_strdup (esc_payto); + (void) MHD_http_unescape (payto_uri.full_payto); + } + } + TMH_db->preflight (TMH_db->cls); + { + json_t *ja; + enum GNUNET_DB_QueryStatus qs; + + ja = json_array (); + GNUNET_assert (NULL != ja); + qs = TMH_db->lookup_expected_transfers (TMH_db->cls, + hc->instance->settings.id, + payto_uri, + before, + after, + limit, + offset, + confirmed, + verified, + &incoming_cb, + ja); + GNUNET_free (payto_uri.full_payto); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_expected_transfers"); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("incoming", + ja)); + } +} + + +/* end of taler-merchant-httpd_get-private-incoming.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-incoming.h b/src/backend/taler-merchant-httpd_get-private-incoming.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 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-httpd_get-private-incoming.h + * @brief headers for GET /incoming handler + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Manages a GET /private/incoming call. + * + * @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_incoming (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-kyc.c b/src/backend/taler-merchant-httpd_get-private-kyc.c @@ -0,0 +1,1488 @@ +/* + This file is part of GNU Taler + (C) 2021-2026 Taler Systems SA + + GNU 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. + + GNU 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-httpd_get-private-kyc.c + * @brief implementing GET /instances/$ID/kyc request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_get-private-kyc.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_get-exchanges.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include <regex.h> + +/** + * Information we keep per /kyc request. + */ +struct KycContext; + + +/** + * Structure for tracking requests to the exchange's + * ``/kyc-check`` API. + */ +struct ExchangeKycRequest +{ + /** + * Kept in a DLL. + */ + struct ExchangeKycRequest *next; + + /** + * Kept in a DLL. + */ + struct ExchangeKycRequest *prev; + + /** + * Find operation where we connect to the respective exchange. + */ + struct TMH_EXCHANGES_KeysOperation *fo; + + /** + * JSON array of payto-URIs with KYC auth wire transfer + * instructions. Provided if @e auth_ok is false and + * @e kyc_auth_conflict is false. + */ + json_t *pkaa; + + /** + * The keys of the exchange. + */ + struct TALER_EXCHANGE_Keys *keys; + + /** + * KYC request this exchange request is made for. + */ + struct KycContext *kc; + + /** + * JSON array of AccountLimits that apply, NULL if + * unknown (and likely defaults apply). + */ + json_t *jlimits; + + /** + * Our account's payto URI. + */ + struct TALER_FullPayto payto_uri; + + /** + * Base URL of the exchange. + */ + char *exchange_url; + + /** + * Hash of the wire account (with salt) we are checking. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Current access token for the KYC SPA. Only set + * if @e auth_ok is true. + */ + struct TALER_AccountAccessTokenP access_token; + + /** + * Timestamp when we last got a reply from the exchange. + */ + struct GNUNET_TIME_Timestamp last_check; + + /** + * Last HTTP status code obtained via /kyc-check from the exchange. + */ + unsigned int last_http_status; + + /** + * Last Taler error code returned from /kyc-check. + */ + enum TALER_ErrorCode last_ec; + + /** + * True if this account cannot work at this exchange because KYC auth is + * impossible. + */ + bool kyc_auth_conflict; + + /** + * We could not get /keys from the exchange. + */ + bool no_keys; + + /** + * True if @e access_token is available. + */ + bool auth_ok; + + /** + * True if we believe no KYC is currently required + * for this account at this exchange. + */ + bool kyc_ok; + + /** + * True if the exchange exposed to us that the account + * is currently under AML review. + */ + bool in_aml_review; + +}; + + +/** + * Information we keep per /kyc request. + */ +struct KycContext +{ + /** + * Stored in a DLL. + */ + struct KycContext *next; + + /** + * Stored in a DLL. + */ + struct KycContext *prev; + + /** + * Connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Instance we are serving. + */ + struct TMH_MerchantInstance *mi; + + /** + * Our handler context. + */ + struct TMH_HandlerContext *hc; + + /** + * Response to return, NULL if we don't have one yet. + */ + struct MHD_Response *response; + + /** + * JSON array where we are building up the array with + * pending KYC operations. + */ + json_t *kycs_data; + + /** + * Head of DLL of requests we are making to an + * exchange to inquire about the latest KYC status. + */ + struct ExchangeKycRequest *exchange_pending_head; + + /** + * Tail of DLL of requests we are making to an + * exchange to inquire about the latest KYC status. + */ + struct ExchangeKycRequest *exchange_pending_tail; + + /** + * Notification handler from database on changes + * to the KYC status. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Set to the exchange URL, or NULL to not filter by + * exchange. "exchange_url" query parameter. + */ + const char *exchange_url; + + /** + * How long are we willing to wait for the exchange(s)? + * Based on "timeout_ms" query parameter. + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Set to the h_wire of the merchant account if + * @a have_h_wire is true, used to filter by account. + * Set from "h_wire" query parameter. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Set to the Etag of a response already known to the + * client. We should only return from long-polling + * on timeout (with "Not Modified") or when the Etag + * of the response differs from what is given here. + * Only set if @a have_lp_not_etag is true. + * Set from "lp_etag" query parameter. + */ + struct GNUNET_ShortHashCode lp_not_etag; + + /** + * Specifies what status change we are long-polling for. If specified, the + * endpoint will only return once the status *matches* the given value. If + * multiple accounts or exchanges match the query, any account reaching the + * STATUS will cause the response to be returned. + */ + const char *lp_status; + + /** + * Specifies what status change we are long-polling for. If specified, the + * endpoint will only return once the status no longer matches the given + * value. If multiple accounts or exchanges *no longer matches* the given + * STATUS will cause the response to be returned. + */ + const char *lp_not_status; + + /** + * #GNUNET_NO if the @e connection was not suspended, + * #GNUNET_YES if the @e connection was suspended, + * #GNUNET_SYSERR if @e connection was resumed to as + * part of #MH_force_pc_resume during shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * What state are we long-polling for? "lpt" argument. + */ + enum TALER_EXCHANGE_KycLongPollTarget lpt; + + /** + * HTTP status code to use for the reply, i.e 200 for "OK". + * Special value UINT_MAX is used to indicate hard errors + * (no reply, return #MHD_NO). + */ + unsigned int response_code; + + /** + * True if @e h_wire was given. + */ + bool have_h_wire; + + /** + * True if @e lp_not_etag was given. + */ + bool have_lp_not_etag; + + /** + * We're still waiting on the exchange to determine + * the KYC status of our deposit(s). + */ + bool return_immediately; + +}; + + +/** + * Head of DLL. + */ +static struct KycContext *kc_head; + +/** + * Tail of DLL. + */ +static struct KycContext *kc_tail; + + +void +TMH_force_kyc_resume () +{ + for (struct KycContext *kc = kc_head; + NULL != kc; + kc = kc->next) + { + if (GNUNET_YES == kc->suspended) + { + kc->suspended = GNUNET_SYSERR; + MHD_resume_connection (kc->connection); + } + } +} + + +/** + * Custom cleanup routine for a `struct KycContext`. + * + * @param cls the `struct KycContext` to clean up. + */ +static void +kyc_context_cleanup (void *cls) +{ + struct KycContext *kc = cls; + struct ExchangeKycRequest *ekr; + + while (NULL != (ekr = kc->exchange_pending_head)) + { + GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, + kc->exchange_pending_tail, + ekr); + if (NULL != ekr->fo) + { + TMH_EXCHANGES_keys4exchange_cancel (ekr->fo); + ekr->fo = NULL; + } + json_decref (ekr->pkaa); + json_decref (ekr->jlimits); + if (NULL != ekr->keys) + TALER_EXCHANGE_keys_decref (ekr->keys); + GNUNET_free (ekr->exchange_url); + GNUNET_free (ekr->payto_uri.full_payto); + GNUNET_free (ekr); + } + if (NULL != kc->eh) + { + TMH_db->event_listen_cancel (kc->eh); + kc->eh = NULL; + } + if (NULL != kc->response) + { + MHD_destroy_response (kc->response); + kc->response = NULL; + } + GNUNET_CONTAINER_DLL_remove (kc_head, + kc_tail, + kc); + json_decref (kc->kycs_data); + GNUNET_free (kc); +} + + +/** + * We have found an exchange in status @a status. Clear any + * long-pollers that wait for us having (or not having) this + * status. + * + * @param[in,out] kc context + * @param status the status we encountered + */ +static void +clear_status (struct KycContext *kc, + const char *status) +{ + if ( (NULL != kc->lp_status) && + (0 == strcmp (kc->lp_status, + status)) ) + kc->lp_status = NULL; /* satisfied! */ + if ( (NULL != kc->lp_not_status) && + (0 != strcmp (kc->lp_not_status, + status) ) ) + kc->lp_not_status = NULL; /* satisfied! */ +} + + +/** + * Resume the given KYC context and send the final response. Stores the + * response in the @a kc and signals MHD to resume the connection. Also + * ensures MHD runs immediately. + * + * @param kc KYC context + */ +static void +resume_kyc_with_response (struct KycContext *kc) +{ + struct GNUNET_ShortHashCode sh; + bool not_modified; + char *can; + + if ( (! GNUNET_TIME_absolute_is_past (kc->timeout)) && + ( (NULL != kc->lp_not_status) || + (NULL != kc->lp_status) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Long-poll target status not reached, not returning response yet\n"); + if (GNUNET_NO == kc->suspended) + { + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + } + return; + } + can = TALER_JSON_canonicalize (kc->kycs_data); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (&sh, + sizeof (sh), + "KYC-SALT", + strlen ("KYC-SALT"), + can, + strlen (can), + NULL, + 0)); + not_modified = kc->have_lp_not_etag && + (0 == GNUNET_memcmp (&sh, + &kc->lp_not_etag)); + if (not_modified && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Status unchanged, not returning response yet\n"); + if (GNUNET_NO == kc->suspended) + { + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + } + GNUNET_free (can); + return; + } + { + const char *inm; + + inm = MHD_lookup_connection_value (kc->connection, + MHD_GET_ARGUMENT_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL == inm) || + ('"' != inm[0]) || + ('"' != inm[strlen (inm) - 1]) || + (0 != strncmp (inm + 1, + can, + strlen (can))) ) + not_modified = false; /* must return full response */ + } + GNUNET_free (can); + kc->response_code = not_modified + ? MHD_HTTP_NOT_MODIFIED + : MHD_HTTP_OK; + kc->response = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_array_incref ("kyc_data", + kc->kycs_data)); + { + char *etag; + char *qetag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&sh, + sizeof (sh)); + GNUNET_asprintf (&qetag, + "\"%s\"", + etag); + GNUNET_break (MHD_YES == + MHD_add_response_header (kc->response, + MHD_HTTP_HEADER_ETAG, + qetag)); + GNUNET_free (qetag); + GNUNET_free (etag); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming /kyc handling as exchange interaction is done (%u)\n", + MHD_HTTP_OK); + if (GNUNET_YES == kc->suspended) + { + kc->suspended = GNUNET_NO; + MHD_resume_connection (kc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + } +} + + +/** + * Handle a DB event about an update relevant + * for the processing of the kyc request. + * + * @param cls our `struct KycContext` + * @param extra additional event data provided + * @param extra_size number of bytes in @a extra + */ +static void +kyc_change_cb (void *cls, + const void *extra, + size_t extra_size) +{ + struct KycContext *kc = cls; + + if (GNUNET_YES == kc->suspended) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming KYC with gateway timeout\n"); + kc->suspended = GNUNET_NO; + MHD_resume_connection (kc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + } +} + + +/** + * Pack the given @a limit into the JSON @a limits array. + * + * @param limit account limit to pack + * @param[in,out] limits JSON array to extend + */ +static void +pack_limit (const struct TALER_EXCHANGE_AccountLimit *limit, + json_t *limits) +{ + json_t *jl; + + jl = GNUNET_JSON_PACK ( + TALER_JSON_pack_kycte ("operation_type", + limit->operation_type), + GNUNET_JSON_pack_time_rel ("timeframe", + limit->timeframe), + TALER_JSON_pack_amount ("threshold", + &limit->threshold), + GNUNET_JSON_pack_bool ("soft_limit", + limit->soft_limit) + ); + GNUNET_assert (0 == + json_array_append_new (limits, + jl)); +} + + +/** + * Return JSON array with AccountLimit objects giving + * the current limits for this exchange. + * + * @param[in,out] ekr overall request context + */ +static json_t * +get_exchange_limits ( + struct ExchangeKycRequest *ekr) +{ + const struct TALER_EXCHANGE_Keys *keys = ekr->keys; + json_t *limits; + + if (NULL != ekr->jlimits) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning custom KYC limits\n"); + return json_incref (ekr->jlimits); + } + if (NULL == keys) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No keys, thus no default KYC limits known\n"); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning default KYC limits (%u/%u)\n", + keys->hard_limits_length, + keys->zero_limits_length); + limits = json_array (); + GNUNET_assert (NULL != limits); + for (unsigned int i = 0; i<keys->hard_limits_length; i++) + { + const struct TALER_EXCHANGE_AccountLimit *limit + = &keys->hard_limits[i]; + + pack_limit (limit, + limits); + } + for (unsigned int i = 0; i<keys->zero_limits_length; i++) + { + const struct TALER_EXCHANGE_ZeroLimitedOperation *zlimit + = &keys->zero_limits[i]; + json_t *jl; + struct TALER_Amount zero; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (keys->currency, + &zero)); + jl = GNUNET_JSON_PACK ( + TALER_JSON_pack_kycte ("operation_type", + zlimit->operation_type), + GNUNET_JSON_pack_time_rel ("timeframe", + GNUNET_TIME_UNIT_ZERO), + TALER_JSON_pack_amount ("threshold", + &zero), + GNUNET_JSON_pack_bool ("soft_limit", + true) + ); + GNUNET_assert (0 == + json_array_append_new (limits, + jl)); + } + return limits; +} + + +/** + * Maps @a ekr to a status code for clients to interpret the + * overall result. + * + * @param ekr request summary + * @return status of the KYC state as a string + */ +static const char * +map_to_status (const struct ExchangeKycRequest *ekr) +{ + if (ekr->no_keys) + { + return "no-exchange-keys"; + } + if (TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE == + ekr->last_ec) + return "unsupported-account"; + if (ekr->kyc_ok) + { + if (NULL != ekr->jlimits) + { + size_t off; + json_t *limit; + json_array_foreach (ekr->jlimits, off, limit) + { + struct TALER_Amount threshold; + enum TALER_KYCLOGIC_KycTriggerEvent operation_type; + bool soft = false; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_kycte ("operation_type", + &operation_type), + TALER_JSON_spec_amount_any ("threshold", + &threshold), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("soft_limit", + &soft), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (limit, + spec, + NULL, NULL)) + { + GNUNET_break (0); + return "merchant-internal-error"; + } + if (! TALER_amount_is_zero (&threshold)) + continue; /* only care about zero-limits */ + if (! soft) + continue; /* only care about soft limits */ + if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || + (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) || + (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) ) + { + if (! ekr->auth_ok) + { + if (ekr->kyc_auth_conflict) + return "kyc-wire-impossible"; + return "kyc-wire-required"; + } + return "kyc-required"; + } + } + } + if (NULL == ekr->jlimits) + { + /* check default limits */ + const struct TALER_EXCHANGE_Keys *keys = ekr->keys; + + for (unsigned int i = 0; i < keys->zero_limits_length; i++) + { + enum TALER_KYCLOGIC_KycTriggerEvent operation_type + = keys->zero_limits[i].operation_type; + + if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || + (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) || + (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) ) + { + if (! ekr->auth_ok) + { + if (ekr->kyc_auth_conflict) + return "kyc-wire-impossible"; + return "kyc-wire-required"; + } + return "kyc-required"; + } + } + } + return "ready"; + } + if (! ekr->auth_ok) + { + if (ekr->kyc_auth_conflict) + return "kyc-wire-impossible"; + return "kyc-wire-required"; + } + if (ekr->in_aml_review) + return "awaiting-aml-review"; + switch (ekr->last_http_status) + { + case 0: + return "exchange-unreachable"; + case MHD_HTTP_OK: + /* then we should have kyc_ok */ + GNUNET_break (0); + return NULL; + case MHD_HTTP_ACCEPTED: + /* Then KYC is really what is needed */ + return "kyc-required"; + case MHD_HTTP_NO_CONTENT: + /* then we should have had kyc_ok! */ + GNUNET_break (0); + return NULL; + case MHD_HTTP_FORBIDDEN: + /* then we should have had ! auth_ok */ + GNUNET_break (0); + return NULL; + case MHD_HTTP_NOT_FOUND: + /* then we should have had ! auth_ok */ + GNUNET_break (0); + return NULL; + case MHD_HTTP_CONFLICT: + /* then we should have had ! auth_ok */ + GNUNET_break (0); + return NULL; + case MHD_HTTP_INTERNAL_SERVER_ERROR: + return "exchange-internal-error"; + case MHD_HTTP_GATEWAY_TIMEOUT: + return "exchange-gateway-timeout"; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange responded with unexpected HTTP status %u to /kyc-check request!\n", + ekr->last_http_status); + break; + } + return "exchange-status-invalid"; +} + + +/** + * Take data from @a ekr to expand our response. + * + * @param ekr exchange we are done inspecting + */ +static void +ekr_expand_response (struct ExchangeKycRequest *ekr) +{ + struct TMH_Exchange *e = TMH_EXCHANGES_lookup_exchange (ekr->exchange_url); + const char *status; + + GNUNET_assert (NULL != e); + status = map_to_status (ekr); + if (NULL == status) + { + GNUNET_break (0); + status = "logic-bug"; + } + clear_status (ekr->kc, + status); + GNUNET_assert ( + 0 == + json_array_append_new ( + ekr->kc->kycs_data, + GNUNET_JSON_PACK ( + TALER_JSON_pack_full_payto ( + "payto_uri", + ekr->payto_uri), + GNUNET_JSON_pack_data_auto ( + "h_wire", + &ekr->h_wire), + GNUNET_JSON_pack_string ( + "status", + status), + GNUNET_JSON_pack_string ( + "exchange_url", + ekr->exchange_url), + GNUNET_JSON_pack_string ( + "exchange_currency", + TMH_EXCHANGES_get_currency (e)), + GNUNET_JSON_pack_bool ("no_keys", + ekr->no_keys), + GNUNET_JSON_pack_bool ("auth_conflict", + ekr->kyc_auth_conflict), + GNUNET_JSON_pack_uint64 ("exchange_http_status", + ekr->last_http_status), + (TALER_EC_NONE == ekr->last_ec) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "dummy", + NULL)) + : GNUNET_JSON_pack_uint64 ("exchange_code", + ekr->last_ec), + ekr->auth_ok + ? GNUNET_JSON_pack_data_auto ( + "access_token", + &ekr->access_token) + : GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ( + "dummy", + NULL)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ( + "limits", + get_exchange_limits (ekr))), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("payto_kycauths", + ekr->pkaa)) + ))); +} + + +/** + * We are done with asynchronous processing, generate the + * response for the @e kc. + * + * @param[in,out] kc KYC context to respond for + */ +static void +kc_respond (struct KycContext *kc) +{ + if ( (! kc->return_immediately) && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + if (GNUNET_NO == kc->suspended) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending: long poll target %d not reached\n", + kc->lpt); + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Remaining suspended: long poll target %d not reached\n", + kc->lpt); + } + return; + } + /* All exchange requests done, create final + big response from cumulated replies */ + resume_kyc_with_response (kc); +} + + +/** + * We are done with the KYC request @a ekr. Remove it from the work list and + * check if we are done overall. + * + * @param[in] ekr key request that is done (and will be freed) + */ +static void +ekr_finished (struct ExchangeKycRequest *ekr) +{ + struct KycContext *kc = ekr->kc; + + ekr_expand_response (ekr); + GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, + kc->exchange_pending_tail, + ekr); + json_decref (ekr->jlimits); + json_decref (ekr->pkaa); + if (NULL != ekr->keys) + TALER_EXCHANGE_keys_decref (ekr->keys); + GNUNET_free (ekr->exchange_url); + GNUNET_free (ekr->payto_uri.full_payto); + GNUNET_free (ekr); + + if (NULL != kc->exchange_pending_head) + return; /* wait for more */ + kc_respond (kc); +} + + +/** + * Figure out which exchange accounts from @a keys could + * be used for a KYC auth wire transfer from the account + * that @a ekr is checking. Will set the "pkaa" array + * in @a ekr. + * + * @param[in,out] ekr request we are processing + */ +static void +determine_eligible_accounts ( + struct ExchangeKycRequest *ekr) +{ + struct KycContext *kc = ekr->kc; + const struct TALER_EXCHANGE_Keys *keys = ekr->keys; + struct TALER_Amount kyc_amount; + char *merchant_pub_str; + struct TALER_NormalizedPayto np; + + ekr->pkaa = json_array (); + GNUNET_assert (NULL != ekr->pkaa); + { + const struct TALER_EXCHANGE_GlobalFee *gf; + + gf = TALER_EXCHANGE_get_global_fee (keys, + GNUNET_TIME_timestamp_get ()); + if (NULL == gf) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (keys->currency, + &kyc_amount)); + } + else + { + /* FIXME-#9427: history fee should be globally renamed to KYC fee... */ + kyc_amount = gf->fees.history; + } + } + + merchant_pub_str + = GNUNET_STRINGS_data_to_string_alloc ( + &kc->mi->merchant_pub, + sizeof (kc->mi->merchant_pub)); + /* For all accounts of the exchange */ + np = TALER_payto_normalize (ekr->payto_uri); + for (unsigned int i = 0; i<keys->accounts_len; i++) + { + const struct TALER_EXCHANGE_WireAccount *account + = &keys->accounts[i]; + + /* KYC auth transfers are never supported with conversion */ + if (NULL != account->conversion_url) + continue; + /* filter by source account by credit_restrictions */ + if (GNUNET_YES != + TALER_EXCHANGE_test_account_allowed (account, + true, /* credit */ + np)) + continue; + /* exchange account is allowed, add it */ + { + const char *exchange_account_payto + = account->fpayto_uri.full_payto; + char *payto_kycauth; + + if (TALER_amount_is_zero (&kyc_amount)) + GNUNET_asprintf (&payto_kycauth, + "%s%cmessage=KYC:%s", + exchange_account_payto, + (NULL == strchr (exchange_account_payto, + '?')) + ? '?' + : '&', + merchant_pub_str); + else + GNUNET_asprintf (&payto_kycauth, + "%s%camount=%s&message=KYC:%s", + exchange_account_payto, + (NULL == strchr (exchange_account_payto, + '?')) + ? '?' + : '&', + TALER_amount2s (&kyc_amount), + merchant_pub_str); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found account %s where KYC auth is possible\n", + payto_kycauth); + GNUNET_assert (0 == + json_array_append_new (ekr->pkaa, + json_string (payto_kycauth))); + GNUNET_free (payto_kycauth); + } + } + GNUNET_free (np.normalized_payto); + GNUNET_free (merchant_pub_str); +} + + +/** + * Function called with the result of a #TMH_EXCHANGES_keys4exchange() + * operation. Runs the KYC check against the exchange. + * + * @param cls closure with our `struct ExchangeKycRequest *` + * @param keys keys of the exchange context + * @param exchange representation of the exchange + */ +static void +kyc_with_exchange (void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct ExchangeKycRequest *ekr = cls; + + (void) exchange; + ekr->fo = NULL; + if (NULL == keys) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to download `%skeys`\n", + ekr->exchange_url); + ekr->no_keys = true; + ekr_finished (ekr); + return; + } + ekr->keys = TALER_EXCHANGE_keys_incref (keys); + if (! ekr->auth_ok) + { + determine_eligible_accounts (ekr); + if (0 == json_array_size (ekr->pkaa)) + { + /* No KYC auth wire transfers are possible to this exchange from + our merchant bank account, so we cannot use this account with + this exchange if it has any KYC requirements! */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC auth to `%s' impossible for merchant account `%s'\n", + ekr->exchange_url, + ekr->payto_uri.full_payto); + ekr->kyc_auth_conflict = true; + } + } + ekr_finished (ekr); +} + + +/** + * Closure for add_unreachable_status(). + */ +struct UnreachableContext +{ + /** + * Where we are building the response. + */ + struct KycContext *kc; + + /** + * Pointer to our account hash. + */ + const struct TALER_MerchantWireHashP *h_wire; + + /** + * Bank account for which we have no status from any exchange. + */ + struct TALER_FullPayto payto_uri; + +}; + +/** + * Add all trusted exchanges with "unknown" status for the + * bank account given in the context. + * + * @param cls a `struct UnreachableContext` + * @param url base URL of the exchange + * @param exchange internal handle for the exchange + */ +static void +add_unreachable_status (void *cls, + const char *url, + const struct TMH_Exchange *exchange) +{ + struct UnreachableContext *uc = cls; + struct KycContext *kc = uc->kc; + + clear_status (kc, + "exchange-unreachable"); + GNUNET_assert ( + 0 == + json_array_append_new ( + kc->kycs_data, + GNUNET_JSON_PACK ( + TALER_JSON_pack_full_payto ( + "payto_uri", + uc->payto_uri), + GNUNET_JSON_pack_data_auto ( + "h_wire", + uc->h_wire), + GNUNET_JSON_pack_string ( + "exchange_currency", + TMH_EXCHANGES_get_currency (exchange)), + GNUNET_JSON_pack_string ( + "status", + "exchange-unreachable"), + GNUNET_JSON_pack_string ( + "exchange_url", + url), + GNUNET_JSON_pack_bool ("no_keys", + true), + GNUNET_JSON_pack_bool ("auth_conflict", + false), + GNUNET_JSON_pack_uint64 ("exchange_http_status", + 0) + ))); + +} + + +/** + * Function called from account_kyc_get_status() with KYC status information + * for this merchant. + * + * @param cls our `struct KycContext *` + * @param h_wire hash of the wire account + * @param payto_uri payto:// URI of the merchant's bank account + * @param exchange_url base URL of the exchange for which this is a status + * @param last_check when did we last get an update on our KYC status from the exchange + * @param kyc_ok true if we satisfied the KYC requirements + * @param access_token access token for the KYC SPA, NULL if we cannot access it yet (need KYC auth wire transfer) + * @param last_http_status last HTTP status from /kyc-check + * @param last_ec last Taler error code from /kyc-check + * @param in_aml_review true if the account is pending review + * @param jlimits JSON array of applicable AccountLimits, or NULL if unknown (like defaults apply) + */ +static void +kyc_status_cb ( + void *cls, + const struct TALER_MerchantWireHashP *h_wire, + struct TALER_FullPayto payto_uri, + const char *exchange_url, + struct GNUNET_TIME_Timestamp last_check, + bool kyc_ok, + const struct TALER_AccountAccessTokenP *access_token, + unsigned int last_http_status, + enum TALER_ErrorCode last_ec, + bool in_aml_review, + const json_t *jlimits) +{ + struct KycContext *kc = cls; + struct ExchangeKycRequest *ekr; + + if (NULL == exchange_url) + { + struct UnreachableContext uc = { + .kc = kc, + .h_wire = h_wire, + .payto_uri = payto_uri + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Account has unknown KYC status for all exchanges.\n"); + TMH_exchange_get_trusted (&add_unreachable_status, + &uc); + kc_respond (kc); + return; + } + if (! TMH_EXCHANGES_check_trusted (exchange_url)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Skipping exchange `%s': not trusted\n", + exchange_url); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYC status for `%s' at `%s' is %u/%s/%s/%s\n", + payto_uri.full_payto, + exchange_url, + last_http_status, + kyc_ok ? "KYC OK" : "KYC NEEDED", + in_aml_review ? "IN AML REVIEW" : "NO AML REVIEW", + NULL == jlimits ? "DEFAULT LIMITS" : "CUSTOM LIMITS"); + switch (kc->lpt) + { + case TALER_EXCHANGE_KLPT_NONE: + break; + case TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER: + if (NULL != access_token) + kc->return_immediately = true; + break; + case TALER_EXCHANGE_KLPT_INVESTIGATION_DONE: + if (! in_aml_review) + kc->return_immediately = true; + break; + case TALER_EXCHANGE_KLPT_KYC_OK: + if (kyc_ok) + kc->return_immediately = true; + break; + } + ekr = GNUNET_new (struct ExchangeKycRequest); + GNUNET_CONTAINER_DLL_insert (kc->exchange_pending_head, + kc->exchange_pending_tail, + ekr); + ekr->last_http_status = last_http_status; + ekr->last_ec = last_ec; + if (NULL != jlimits) + ekr->jlimits = json_incref ((json_t *) jlimits); + ekr->h_wire = *h_wire; + ekr->exchange_url = GNUNET_strdup (exchange_url); + ekr->payto_uri.full_payto + = GNUNET_strdup (payto_uri.full_payto); + ekr->last_check = last_check; + ekr->kyc_ok = kyc_ok; + ekr->kc = kc; + ekr->in_aml_review = in_aml_review; + ekr->auth_ok = (NULL != access_token); + if ( (! ekr->auth_ok) || + (NULL == ekr->jlimits) ) + { + /* Figure out wire transfer instructions */ + if (GNUNET_NO == kc->suspended) + { + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + } + ekr->fo = TMH_EXCHANGES_keys4exchange ( + exchange_url, + false, + &kyc_with_exchange, + ekr); + if (NULL == ekr->fo) + { + GNUNET_break (0); + ekr_finished (ekr); + return; + } + return; + } + ekr->access_token = *access_token; + ekr_finished (ekr); +} + + +/** + * Check the KYC status of an instance. + * + * @param mi instance to check KYC status of + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +static MHD_RESULT +get_instances_ID_kyc ( + struct TMH_MerchantInstance *mi, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct KycContext *kc = hc->ctx; + + if (NULL == kc) + { + kc = GNUNET_new (struct KycContext); + kc->mi = mi; + hc->ctx = kc; + hc->cc = &kyc_context_cleanup; + GNUNET_CONTAINER_DLL_insert (kc_head, + kc_tail, + kc); + kc->connection = connection; + kc->hc = hc; + kc->kycs_data = json_array (); + GNUNET_assert (NULL != kc->kycs_data); + TALER_MHD_parse_request_timeout (connection, + &kc->timeout); + { + uint64_t num = 0; + int val; + + TALER_MHD_parse_request_number (connection, + "lpt", + &num); + val = (int) num; + if ( (val < 0) || + (val > TALER_EXCHANGE_KLPT_MAX) ) + { + /* Protocol violation, but we can be graceful and + just ignore the long polling! */ + GNUNET_break_op (0); + val = TALER_EXCHANGE_KLPT_NONE; + } + kc->lpt = (enum TALER_EXCHANGE_KycLongPollTarget) val; + } + kc->return_immediately + = (TALER_EXCHANGE_KLPT_NONE == kc->lpt); + /* process 'exchange_url' argument */ + kc->exchange_url = MHD_lookup_connection_value ( + connection, + MHD_GET_ARGUMENT_KIND, + "exchange_url"); + if ( (NULL != kc->exchange_url) && + ( (! TALER_url_valid_charset (kc->exchange_url)) || + (! TALER_is_web_url (kc->exchange_url)) ) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "exchange_url must be a valid HTTP(s) URL"); + } + kc->lp_status = MHD_lookup_connection_value ( + connection, + MHD_GET_ARGUMENT_KIND, + "lp_status"); + kc->lp_not_status = MHD_lookup_connection_value ( + connection, + MHD_GET_ARGUMENT_KIND, + "lp_not_status"); + TALER_MHD_parse_request_arg_auto (connection, + "h_wire", + &kc->h_wire, + kc->have_h_wire); + TALER_MHD_parse_request_arg_auto (connection, + "lp_not_etag", + &kc->lp_not_etag, + kc->have_lp_not_etag); + + if (! GNUNET_TIME_absolute_is_past (kc->timeout)) + { + if (kc->have_h_wire) + { + struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = { + .header.size = htons (sizeof (ev)), + .header.type = htons ( + TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_STATUS_CHANGED + ), + .h_wire = kc->h_wire + }; + + kc->eh = TMH_db->event_listen ( + TMH_db->cls, + &ev.header, + GNUNET_TIME_absolute_get_remaining (kc->timeout), + &kyc_change_cb, + kc); + } + else + { + struct GNUNET_DB_EventHeaderP hdr = { + .size = htons (sizeof (hdr)), + .type = htons (TALER_DBEVENT_MERCHANT_KYC_STATUS_CHANGED) + }; + + kc->eh = TMH_db->event_listen ( + TMH_db->cls, + &hdr, + GNUNET_TIME_absolute_get_remaining (kc->timeout), + &kyc_change_cb, + kc); + } + } /* end register LISTEN hooks */ + } /* end 1st time initialization */ + + if (GNUNET_SYSERR == kc->suspended) + return MHD_NO; /* during shutdown, we don't generate any more replies */ + GNUNET_assert (GNUNET_NO == kc->suspended); + + if (NULL != kc->response) + return MHD_queue_response (connection, + kc->response_code, + kc->response); + + /* Check our database */ + { + enum GNUNET_DB_QueryStatus qs; + + GNUNET_break (0 == + json_array_clear (kc->kycs_data)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking KYC status for %s (%d/%s)\n", + mi->settings.id, + kc->have_h_wire, + kc->exchange_url); + /* We may run repeatedly due to long-polling; clear data + from previous runs first */ + GNUNET_break (0 == json_array_clear (kc->kycs_data)); + qs = TMH_db->account_kyc_get_status ( + TMH_db->cls, + mi->settings.id, + kc->have_h_wire + ? &kc->h_wire + : NULL, + kc->exchange_url, + &kyc_status_cb, + kc); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "account_kyc_get_status returned %d records\n", + (int) qs); + if (qs < 0) + { + /* Database error */ + GNUNET_break (0); + if (GNUNET_YES == kc->suspended) + { + /* must have suspended before DB error, resume! */ + MHD_resume_connection (connection); + kc->suspended = GNUNET_NO; + } + return TALER_MHD_reply_with_ec ( + connection, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "account_kyc_get_status"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* We use an Etag of all zeros for the 204 status code */ + static struct GNUNET_ShortHashCode zero_etag; + + /* no matching accounts, could not have suspended */ + GNUNET_assert (GNUNET_NO == kc->suspended); + if (kc->have_lp_not_etag && + (0 == GNUNET_memcmp (&zero_etag, + &kc->lp_not_etag)) && + (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No matching accounts, suspending to wait for this to change\n"); + MHD_suspend_connection (kc->connection); + kc->suspended = GNUNET_YES; + return MHD_YES; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No matching accounts, returning empty response\n"); + kc->response_code = MHD_HTTP_NO_CONTENT; + kc->response = MHD_create_response_from_buffer_static (0, + NULL); + TALER_MHD_add_global_headers (kc->response, + false); + { + char *etag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&zero_etag, + sizeof (zero_etag)); + GNUNET_break (MHD_YES == + MHD_add_response_header (kc->response, + MHD_HTTP_HEADER_ETAG, + etag)); + GNUNET_free (etag); + } + return MHD_queue_response (connection, + kc->response_code, + kc->response); + } + } + if (GNUNET_YES == kc->suspended) + return MHD_YES; + /* Should have generated a response */ + GNUNET_break (NULL != kc->response); + return MHD_queue_response (connection, + kc->response_code, + kc->response); +} + + +MHD_RESULT +TMH_private_get_instances_ID_kyc ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + (void) rh; + return get_instances_ID_kyc (mi, + connection, + hc); +} + + +MHD_RESULT +TMH_private_get_instances_default_ID_kyc ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi; + + (void) rh; + mi = TMH_lookup_instance (hc->infix); + if (NULL == mi) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); + } + return get_instances_ID_kyc (mi, + connection, + hc); +} + + +/* end of taler-merchant-httpd_get-private-kyc.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-kyc.h b/src/backend/taler-merchant-httpd_get-private-kyc.h @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems SA + + GNU 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. + + GNU 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-httpd_get-private-kyc.h + * @brief implements GET /instances/$ID/kyc request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_KYC_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_KYC_H +#include "taler-merchant-httpd.h" + + +/** + * Force all KYC contexts to be resumed as we are about + * to shut down MHD. + */ +void +TMH_force_kyc_resume (void); + + +/** + * Change the instance's kyc settings. + * This is the handler called using the instance's own kycentication. + * + * @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_instances_ID_kyc (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Change the instance's kyc settings. + * This is the handler called using the default instance's kycentication. + * + * @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_instances_default_ID_kyc (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.c b/src/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.c @@ -0,0 +1,1795 @@ +/* + This file is part of TALER + (C) 2017-2024, 2026 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/> +*/ +/** + * @file taler-merchant-httpd_get-private-orders-ORDER_ID.c + * @brief implementation of GET /private/orders/ID handler + * @author Florian Dold + * @author Christian Grothoff + * @author Bohdan Potuzhnyi + * @author Iván Ávalos + */ +#include "taler/platform.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_util.h> +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include "taler/taler_merchant_util.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler-merchant-httpd_get-private-orders-ORDER_ID.h" + +/** + * Data structure we keep for a check payment request. + */ +struct GetOrderRequestContext; + + +/** + * Request to an exchange for details about wire transfers + * in response to a coin's deposit operation. + */ +struct TransferQuery +{ + + /** + * Kept in a DLL. + */ + struct TransferQuery *next; + + /** + * Kept in a DLL. + */ + struct TransferQuery *prev; + + /** + * Base URL of the exchange. + */ + char *exchange_url; + + /** + * Overall request this TQ belongs with. + */ + struct GetOrderRequestContext *gorc; + + /** + * Hash of the merchant's bank account the transfer (presumably) went to. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Value deposited (including deposit fee). + */ + struct TALER_Amount amount_with_fee; + + /** + * Deposit fee paid for this coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Public key of the coin this is about. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Which deposit operation is this about? + */ + uint64_t deposit_serial; + +}; + + +/** + * Phases of order processing. + */ +enum GetOrderPhase +{ + /** + * Initialization. + */ + GOP_INIT = 0, + + /** + * Obtain contract terms from database. + */ + GOP_FETCH_CONTRACT = 1, + + /** + * Parse the contract terms. + */ + GOP_PARSE_CONTRACT = 2, + + /** + * Check if the contract was fully paid. + */ + GOP_CHECK_PAID = 3, + + /** + * Check if the wallet may have purchased an equivalent + * order before and we need to redirect the wallet to + * an existing paid order. + */ + GOP_CHECK_REPURCHASE = 4, + + /** + * Terminate processing of unpaid orders, either by + * suspending until payment or by returning the + * unpaid order status. + */ + GOP_UNPAID_FINISH = 5, + + /** + * Check if the (paid) order was refunded. + */ + GOP_CHECK_REFUNDS = 6, + + /** + * Load all deposits associated with the order. + */ + GOP_CHECK_DEPOSITS = 7, + + /** + * Check local records for transfers of funds to + * the merchant. + */ + GOP_CHECK_LOCAL_TRANSFERS = 8, + + /** + * Generate final comprehensive result. + */ + GOP_REPLY_RESULT = 9, + + /** + * End with the HTTP status and error code in + * wire_hc and wire_ec. + */ + GOP_ERROR = 10, + + /** + * We are suspended awaiting payment. + */ + GOP_SUSPENDED_ON_UNPAID = 11, + + /** + * Processing is done, return #MHD_YES. + */ + GOP_END_YES = 12, + + /** + * Processing is done, return #MHD_NO. + */ + GOP_END_NO = 13 + +}; + + +/** + * Data structure we keep for a check payment request. + */ +struct GetOrderRequestContext +{ + + /** + * Processing phase we are in. + */ + enum GetOrderPhase phase; + + /** + * Entry in the #resume_timeout_heap for this check payment, if we are + * suspended. + */ + struct TMH_SuspendedConnection sc; + + /** + * Which merchant instance is this for? + */ + struct TMH_HandlerContext *hc; + + /** + * session of the client + */ + const char *session_id; + + /** + * Kept in a DLL while suspended on exchange. + */ + struct GetOrderRequestContext *next; + + /** + * Kept in a DLL while suspended on exchange. + */ + struct GetOrderRequestContext *prev; + + /** + * Head of DLL of individual queries for transfer data. + */ + struct TransferQuery *tq_head; + + /** + * Tail of DLL of individual queries for transfer data. + */ + struct TransferQuery *tq_tail; + + /** + * Timeout task while waiting on exchange. + */ + struct GNUNET_SCHEDULER_Task *tt; + + /** + * Database event we are waiting on to be resuming + * for payment or refunds. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Database event we are waiting on to be resuming + * for session capture. + */ + struct GNUNET_DB_EventHandler *session_eh; + + /** + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. + */ + json_t *contract_terms_json; + + /** + * Parsed contract terms, NULL when parsing failed + */ + struct TALER_MERCHANT_Contract *contract_terms; + + /** + * Claim token of the order. + */ + struct TALER_ClaimTokenP claim_token; + + /** + * Timestamp of the last payment. + */ + struct GNUNET_TIME_Timestamp last_payment; + + /** + * Wire details for the payment, to be returned in the reply. NULL + * if not available. + */ + json_t *wire_details; + + /** + * Details about refunds, NULL if there are no refunds. + */ + json_t *refund_details; + + /** + * Amount of the order, unset for unpaid v1 orders. + */ + struct TALER_Amount contract_amount; + + /** + * Hash over the @e contract_terms. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Set to the Etag of a response already known to the + * client. We should only return from long-polling + * on timeout (with "Not Modified") or when the Etag + * of the response differs from what is given here. + * Only set if @a have_lp_not_etag is true. + * Set from "lp_etag" query parameter. + */ + struct GNUNET_ShortHashCode lp_not_etag; + + /** + * Total amount the exchange deposited into our bank account + * (confirmed or unconfirmed), excluding fees. + */ + struct TALER_Amount deposits_total; + + /** + * Total amount in deposit fees we paid for all coins. + */ + struct TALER_Amount deposit_fees_total; + + /** + * Total amount in deposit fees cancelled due to refunds for all coins. + */ + struct TALER_Amount deposit_fees_refunded_total; + + /** + * Total value of the coins that the exchange deposited into our bank + * account (confirmed or unconfirmed), including deposit fees. + */ + struct TALER_Amount value_total; + + /** + * Serial ID of the order. + */ + uint64_t order_serial; + + /** + * Index of selected choice from ``choices`` array in the contract_terms. + * Is -1 for orders without choices. + */ + int16_t choice_index; + + /** + * Total refunds granted for this payment. Only initialized + * if @e refunded is set to true. + */ + struct TALER_Amount refund_amount; + + /** + * Exchange HTTP error code encountered while trying to determine wire transfer + * details. #TALER_EC_NONE for no error encountered. + */ + unsigned int exchange_hc; + + /** + * Exchange error code encountered while trying to determine wire transfer + * details. #TALER_EC_NONE for no error encountered. + */ + enum TALER_ErrorCode exchange_ec; + + /** + * Error code encountered while trying to determine wire transfer + * details. #TALER_EC_NONE for no error encountered. + */ + enum TALER_ErrorCode wire_ec; + + /** + * Set to YES if refunded orders should be included when + * doing repurchase detection. + */ + enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; + + /** + * HTTP status to return with @e wire_ec, 0 if @e wire_ec is #TALER_EC_NONE. + */ + unsigned int wire_hc; + + /** + * Did we suspend @a connection and are thus in + * the #gorc_head DLL (#GNUNET_YES). Set to + * #GNUNET_NO if we are not suspended, and to + * #GNUNET_SYSERR if we should close the connection + * without a response due to shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Set to true if this payment has been refunded and + * @e refund_amount is initialized. + */ + bool refunded; + + /** + * True if @e lp_not_etag was given. + */ + bool have_lp_not_etag; + + /** + * True if the order was paid. + */ + bool paid; + + /** + * True if the paid session in the database matches + * our @e session_id. + */ + bool paid_session_matches; + + /** + * True if the exchange wired the money to the merchant. + */ + bool wired; + + /** + * True if the order remains unclaimed. + */ + bool order_only; + + /** + * Set to true if this payment has been refunded and + * some refunds remain to be picked up by the wallet. + */ + bool refund_pending; + + /** + * Set to true if our database (incorrectly) has refunds + * in a different currency than the currency of the + * original payment for the order. + */ + bool refund_currency_mismatch; + + /** + * Set to true if our database (incorrectly) has deposits + * in a different currency than the currency of the + * original payment for the order. + */ + bool deposit_currency_mismatch; +}; + + +/** + * Head of list of suspended requests waiting on the exchange. + */ +static struct GetOrderRequestContext *gorc_head; + +/** + * Tail of list of suspended requests waiting on the exchange. + */ +static struct GetOrderRequestContext *gorc_tail; + + +void +TMH_force_gorc_resume (void) +{ + struct GetOrderRequestContext *gorc; + + while (NULL != (gorc = gorc_head)) + { + GNUNET_CONTAINER_DLL_remove (gorc_head, + gorc_tail, + gorc); + GNUNET_assert (GNUNET_YES == gorc->suspended); + gorc->suspended = GNUNET_SYSERR; + MHD_resume_connection (gorc->sc.con); + } +} + + +/** + * We have received a trigger from the database + * that we should (possibly) resume the request. + * + * @param cls a `struct GetOrderRequestContext` to resume + * @param extra string encoding refund amount (or NULL) + * @param extra_size number of bytes in @a extra + */ +static void +resume_by_event (void *cls, + const void *extra, + size_t extra_size) +{ + struct GetOrderRequestContext *gorc = cls; + + (void) extra; + (void) extra_size; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming request for order %s by trigger\n", + gorc->hc->infix); + if (GNUNET_NO == gorc->suspended) + return; /* duplicate event is possible */ + gorc->suspended = GNUNET_NO; + gorc->phase = GOP_FETCH_CONTRACT; + GNUNET_CONTAINER_DLL_remove (gorc_head, + gorc_tail, + gorc); + MHD_resume_connection (gorc->sc.con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Clean up the session state for a GET /private/order/ID request. + * + * @param cls closure, must be a `struct GetOrderRequestContext *` + */ +static void +gorc_cleanup (void *cls) +{ + struct GetOrderRequestContext *gorc = cls; + struct TransferQuery *tq; + + while (NULL != (tq = gorc->tq_head)) + { + GNUNET_CONTAINER_DLL_remove (gorc->tq_head, + gorc->tq_tail, + tq); + GNUNET_free (tq->exchange_url); + GNUNET_free (tq); + } + + if (NULL != gorc->contract_terms_json) + json_decref (gorc->contract_terms_json); + if (NULL != gorc->contract_terms) + { + TALER_MERCHANT_contract_free (gorc->contract_terms); + gorc->contract_terms = NULL; + } + if (NULL != gorc->wire_details) + json_decref (gorc->wire_details); + if (NULL != gorc->refund_details) + json_decref (gorc->refund_details); + if (NULL != gorc->tt) + { + GNUNET_SCHEDULER_cancel (gorc->tt); + gorc->tt = NULL; + } + if (NULL != gorc->eh) + { + TMH_db->event_listen_cancel (gorc->eh); + gorc->eh = NULL; + } + if (NULL != gorc->session_eh) + { + TMH_db->event_listen_cancel (gorc->session_eh); + gorc->session_eh = NULL; + } + GNUNET_free (gorc); +} + + +/** + * Processing the request @a gorc is finished, set the + * final return value in phase based on @a mret. + * + * @param[in,out] gorc order context to initialize + * @param mret MHD HTTP response status to return + */ +static void +phase_end (struct GetOrderRequestContext *gorc, + MHD_RESULT mret) +{ + gorc->phase = (MHD_YES == mret) + ? GOP_END_YES + : GOP_END_NO; +} + + +/** + * Initialize event callbacks for the order processing. + * + * @param[in,out] gorc order context to initialize + */ +static void +phase_init (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = hc->instance->merchant_pub + }; + + if (! GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout)) + { + gorc->phase++; + return; + } + + GNUNET_CRYPTO_hash (hc->infix, + strlen (hc->infix), + &pay_eh.h_order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing to payment triggers for %p\n", + gorc); + gorc->eh = TMH_db->event_listen ( + TMH_db->cls, + &pay_eh.header, + GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_timeout), + &resume_by_event, + gorc); + if ( (NULL != gorc->session_id) && + (NULL != gorc->contract_terms->fulfillment_url) ) + { + struct TMH_SessionEventP session_eh = { + .header.size = htons (sizeof (session_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), + .merchant_pub = hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subscribing to session triggers for %p\n", + gorc); + GNUNET_CRYPTO_hash (gorc->session_id, + strlen (gorc->session_id), + &session_eh.h_session_id); + GNUNET_CRYPTO_hash (gorc->contract_terms->fulfillment_url, + strlen (gorc->contract_terms->fulfillment_url), + &session_eh.h_fulfillment_url); + gorc->session_eh + = TMH_db->event_listen ( + TMH_db->cls, + &session_eh.header, + GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_timeout), + &resume_by_event, + gorc); + } + gorc->phase++; +} + + +/** + * Obtain latest contract terms from the database. + * + * @param[in,out] gorc order context to update + */ +static void +phase_fetch_contract (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + enum GNUNET_DB_QueryStatus qs; + + if (NULL != gorc->contract_terms_json) + { + /* Free memory filled with old contract terms before fetching the latest + ones from the DB. Note that we cannot simply skip the database + interaction as the contract terms loaded previously might be from an + earlier *unclaimed* order state (which we loaded in a previous + invocation of this function and we are back here due to long polling) + and thus the contract terms could have changed during claiming. Thus, + we need to fetch the latest contract terms from the DB again. */ + json_decref (gorc->contract_terms_json); + gorc->contract_terms_json = NULL; + gorc->order_only = false; + } + TMH_db->preflight (TMH_db->cls); + qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + gorc->session_id, + &gorc->contract_terms_json, + &gorc->order_serial, + &gorc->paid, + &gorc->wired, + &gorc->paid_session_matches, + &gorc->claim_token, + &gorc->choice_index); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "lookup_contract_terms (%s) returned %d\n", + hc->infix, + (int) qs); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract terms")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order %s is %s (%s) according to database, choice %d\n", + hc->infix, + gorc->paid ? "paid" : "unpaid", + gorc->wired ? "wired" : "unwired", + (int) gorc->choice_index); + gorc->phase++; + return; + } + GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs); + GNUNET_assert (! gorc->paid); + /* No contract, only order, fetch from orders table */ + gorc->order_only = true; + { + struct TALER_MerchantPostDataHashP unused; + + /* We need the order for two cases: Either when the contract doesn't exist yet, + * or when the order is claimed but unpaid, and we need the claim token. */ + qs = TMH_db->lookup_order (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &gorc->claim_token, + &unused, + &gorc->contract_terms_json); + } + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + hc->infix)); + return; + } + gorc->phase++; +} + + +/** + * Obtain parse contract terms of the order. Extracts the fulfillment URL, + * total amount, summary and timestamp from the contract terms! + * + * @param[in,out] gorc order context to update + */ +static void +phase_parse_contract (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + + if (NULL == gorc->contract_terms) + { + gorc->contract_terms = TALER_MERCHANT_contract_parse ( + gorc->contract_terms_json, + true); + + if (NULL == gorc->contract_terms) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + hc->infix)); + return; + } + } + + switch (gorc->contract_terms->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + gorc->contract_amount = gorc->contract_terms->details.v0.brutto; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + if (gorc->choice_index >= 0) + { + if (gorc->choice_index >= + gorc->contract_terms->details.v1.choices_len) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL)); + return; + } + + gorc->contract_amount = + gorc->contract_terms->details.v1.choices[gorc->choice_index].amount; + } + else + { + GNUNET_break (gorc->order_only); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Choice index %i for order %s is invalid or not yet available", + gorc->choice_index, + gorc->contract_terms->order_id); + } + break; + default: + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION, + NULL)); + return; + } + } + + if ( (! gorc->order_only) && + (GNUNET_OK != + TALER_JSON_contract_hash (gorc->contract_terms_json, + &gorc->h_contract_terms)) ) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + NULL)); + return; + } + GNUNET_assert (NULL != gorc->contract_terms_json); + GNUNET_assert (NULL != gorc->contract_terms); + gorc->phase++; +} + + +/** + * Check payment status of the order. + * + * @param[in,out] gorc order context to update + */ +static void +phase_check_paid (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + + if (gorc->order_only) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order %s unclaimed, no need to lookup payment status\n", + hc->infix); + GNUNET_assert (! gorc->paid); + GNUNET_assert (! gorc->wired); + gorc->phase++; + return; + } + if (NULL == gorc->session_id) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No session ID, do not need to lookup session-ID specific payment status (%s/%s)\n", + gorc->paid ? "paid" : "unpaid", + gorc->wired ? "wired" : "unwired"); + gorc->phase++; + return; + } + if (! gorc->paid_session_matches) + { + gorc->paid = false; + gorc->wired = false; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order %s %s for session %s (%s)\n", + hc->infix, + gorc->paid ? "paid" : "unpaid", + gorc->session_id, + gorc->wired ? "wired" : "unwired"); + gorc->phase++; +} + + +/** + * Check if the @a reply satisfies the long-poll not_etag + * constraint. If so, return it as a response for @a gorc, + * otherwise suspend and wait for a change. + * + * @param[in,out] gorc request to handle + * @param reply body for JSON response (#MHD_HTTP_OK) + */ +static void +check_reply (struct GetOrderRequestContext *gorc, + const json_t *reply) +{ + struct GNUNET_ShortHashCode sh; + unsigned int http_response_code; + bool not_modified; + struct MHD_Response *response; + char *can; + + can = TALER_JSON_canonicalize (reply); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_kdf (&sh, + sizeof (sh), + "GOR-SALT", + strlen ("GOR-SALT"), + can, + strlen (can), + NULL, + 0)); + not_modified = gorc->have_lp_not_etag && + (0 == GNUNET_memcmp (&sh, + &gorc->lp_not_etag)); + + if (not_modified && + (! GNUNET_TIME_absolute_is_past (gorc->sc.long_poll_timeout)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Status unchanged, not returning response yet\n"); + GNUNET_assert (GNUNET_NO == gorc->suspended); + /* note: not necessarily actually unpaid ... */ + GNUNET_CONTAINER_DLL_insert (gorc_head, + gorc_tail, + gorc); + gorc->phase = GOP_SUSPENDED_ON_UNPAID; + gorc->suspended = GNUNET_YES; + MHD_suspend_connection (gorc->sc.con); + GNUNET_free (can); + return; + } + { + const char *inm; + + inm = MHD_lookup_connection_value (gorc->sc.con, + MHD_GET_ARGUMENT_KIND, + MHD_HTTP_HEADER_IF_NONE_MATCH); + if ( (NULL == inm) || + ('"' != inm[0]) || + ('"' != inm[strlen (inm) - 1]) || + (0 != strncmp (inm + 1, + can, + strlen (can))) ) + not_modified = false; /* must return full response */ + } + GNUNET_free (can); + http_response_code = not_modified + ? MHD_HTTP_NOT_MODIFIED + : MHD_HTTP_OK; + response = TALER_MHD_make_json (reply); + { + char *etag; + char *qetag; + + etag = GNUNET_STRINGS_data_to_string_alloc (&sh, + sizeof (sh)); + GNUNET_asprintf (&qetag, + "\"%s\"", + etag); + GNUNET_break (MHD_YES == + MHD_add_response_header (response, + MHD_HTTP_HEADER_ETAG, + qetag)); + GNUNET_free (qetag); + GNUNET_free (etag); + } + + { + MHD_RESULT ret; + + ret = MHD_queue_response (gorc->sc.con, + http_response_code, + response); + MHD_destroy_response (response); + phase_end (gorc, + ret); + } +} + + +/** + * Check if re-purchase detection applies to the order. + * + * @param[in,out] gorc order context to update + */ +static void +phase_check_repurchase (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + char *already_paid_order_id = NULL; + enum GNUNET_DB_QueryStatus qs; + char *taler_pay_uri; + char *order_status_url; + json_t *reply; + + if ( (gorc->paid) || + (NULL == gorc->contract_terms->fulfillment_url) || + (NULL == gorc->session_id) ) + { + /* Repurchase cannot apply */ + gorc->phase++; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Running re-purchase detection for %s/%s\n", + gorc->session_id, + gorc->contract_terms->fulfillment_url); + qs = TMH_db->lookup_order_by_fulfillment ( + TMH_db->cls, + hc->instance->settings.id, + gorc->contract_terms->fulfillment_url, + gorc->session_id, + TALER_EXCHANGE_YNA_NO != + gorc->allow_refunded_for_repurchase, + &already_paid_order_id); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems, and the entry should exist as per above */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order by fulfillment")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "No already paid order for %s/%s\n", + gorc->session_id, + gorc->contract_terms->fulfillment_url); + gorc->phase++; + return; + } + + /* User did pay for this order, but under a different session; ask wallet to + switch order ID */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Found already paid order %s\n", + already_paid_order_id); + taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, + hc->infix, + gorc->session_id, + hc->instance->settings.id, + &gorc->claim_token); + order_status_url = TMH_make_order_status_url (gorc->sc.con, + hc->infix, + gorc->session_id, + hc->instance->settings.id, + &gorc->claim_token, + NULL); + if ( (NULL == taler_pay_uri) || + (NULL == order_status_url) ) + { + GNUNET_break_op (0); + GNUNET_free (taler_pay_uri); + GNUNET_free (order_status_url); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + "host")); + return; + } + reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("taler_pay_uri", + taler_pay_uri), + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + GNUNET_JSON_pack_string ("order_status", + "unpaid"), + GNUNET_JSON_pack_string ("already_paid_order_id", + already_paid_order_id), + GNUNET_JSON_pack_string ("already_paid_fulfillment_url", + gorc->contract_terms->fulfillment_url), + /* undefined for unpaid v1 contracts */ + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("total_amount", + TALER_amount_is_valid (&gorc->contract_amount) + ? &gorc->contract_amount + : NULL)), + GNUNET_JSON_pack_object_incref ("proto_contract_terms", + gorc->contract_terms_json), + GNUNET_JSON_pack_string ("summary", + gorc->contract_terms->summary), + GNUNET_JSON_pack_timestamp ("pay_deadline", + gorc->contract_terms->pay_deadline), + GNUNET_JSON_pack_timestamp ("creation_time", + gorc->contract_terms->timestamp)); + + GNUNET_free (order_status_url); + GNUNET_free (taler_pay_uri); + GNUNET_free (already_paid_order_id); + check_reply (gorc, + reply); + json_decref (reply); +} + + +/** + * Check if we should suspend until the order is paid. + * + * @param[in,out] gorc order context to update + */ +static void +phase_unpaid_finish (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + char *order_status_url; + + if (gorc->paid) + { + gorc->phase++; + return; + } + /* User never paid for this order, suspend waiting + on payment or return details. */ + if (GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout) && + (! gorc->have_lp_not_etag) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending GET /private/orders/%s\n", + hc->infix); + GNUNET_CONTAINER_DLL_insert (gorc_head, + gorc_tail, + gorc); + gorc->phase = GOP_SUSPENDED_ON_UNPAID; + gorc->suspended = GNUNET_YES; + MHD_suspend_connection (gorc->sc.con); + return; + } + order_status_url = TMH_make_order_status_url (gorc->sc.con, + hc->infix, + gorc->session_id, + hc->instance->settings.id, + &gorc->claim_token, + NULL); + if (! gorc->order_only) + { + json_t *reply; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order %s claimed but not paid yet\n", + hc->infix); + reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + GNUNET_JSON_pack_object_incref ("contract_terms", + gorc->contract_terms_json), + GNUNET_JSON_pack_string ("order_status", + "claimed")); + GNUNET_free (order_status_url); + check_reply (gorc, + reply); + json_decref (reply); + return; + } + { + char *taler_pay_uri; + json_t *reply; + + taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, + hc->infix, + gorc->session_id, + hc->instance->settings.id, + &gorc->claim_token); + reply = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("taler_pay_uri", + taler_pay_uri), + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + GNUNET_JSON_pack_string ("order_status", + "unpaid"), + GNUNET_JSON_pack_object_incref ("proto_contract_terms", + gorc->contract_terms_json), + /* undefined for unpaid v1 contracts */ + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("total_amount", + &gorc->contract_amount)), + GNUNET_JSON_pack_string ("summary", + gorc->contract_terms->summary), + GNUNET_JSON_pack_timestamp ("creation_time", + gorc->contract_terms->timestamp)); + check_reply (gorc, + reply); + GNUNET_free (taler_pay_uri); + } + GNUNET_free (order_status_url); +} + + +/** + * Function called with information about a refund. + * It is responsible for summing up the refund amount. + * + * @param cls closure + * @param refund_serial unique serial number of the refund + * @param timestamp time of the refund (for grouping of refunds in the wallet UI) + * @param coin_pub public coin from which the refund comes from + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param rtransaction_id identificator of the refund + * @param reason human-readable explanation of the refund + * @param refund_amount refund amount which is being taken from @a coin_pub + * @param pending true if the this refund was not yet processed by the wallet/exchange + */ +static void +process_refunds_cb ( + void *cls, + uint64_t refund_serial, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + uint64_t rtransaction_id, + const char *reason, + const struct TALER_Amount *refund_amount, + bool pending) +{ + struct GetOrderRequestContext *gorc = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found refund %llu over %s for reason %s\n", + (unsigned long long) rtransaction_id, + TALER_amount2s (refund_amount), + reason); + GNUNET_assert ( + 0 == + json_array_append_new ( + gorc->refund_details, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + refund_amount), + GNUNET_JSON_pack_bool ("pending", + pending), + GNUNET_JSON_pack_timestamp ("timestamp", + timestamp), + GNUNET_JSON_pack_string ("reason", + reason)))); + /* For refunded coins, we are not charged deposit fees, so subtract those + again */ + for (struct TransferQuery *tq = gorc->tq_head; + NULL != tq; + tq = tq->next) + { + if (0 != + strcmp (exchange_url, + tq->exchange_url)) + continue; + if (0 != + GNUNET_memcmp (&tq->coin_pub, + coin_pub)) + continue; + if (GNUNET_OK != + TALER_amount_cmp_currency ( + &gorc->deposit_fees_total, + &tq->deposit_fee)) + { + gorc->refund_currency_mismatch = true; + return; + } + GNUNET_assert ( + 0 <= + TALER_amount_add (&gorc->deposit_fees_refunded_total, + &gorc->deposit_fees_refunded_total, + &tq->deposit_fee)); + } + if (GNUNET_OK != + TALER_amount_cmp_currency ( + &gorc->refund_amount, + refund_amount)) + { + gorc->refund_currency_mismatch = true; + return; + } + GNUNET_assert (0 <= + TALER_amount_add (&gorc->refund_amount, + &gorc->refund_amount, + refund_amount)); + gorc->refunded = true; + gorc->refund_pending |= pending; +} + + +/** + * Check refund status for the order. + * + * @param[in,out] gorc order context to update + */ +static void +phase_check_refunds (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (! gorc->order_only); + GNUNET_assert (gorc->paid); + + /* Accumulate refunds, if any. */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (gorc->contract_amount.currency, + &gorc->refund_amount)); + json_array_clear (gorc->refund_details); + qs = TMH_db->lookup_refunds_detailed ( + TMH_db->cls, + hc->instance->settings.id, + &gorc->h_contract_terms, + &process_refunds_cb, + gorc); + if (0 > qs) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "detailed refunds")); + return; + } + if (gorc->refund_currency_mismatch) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "refunds in different currency than original order price")); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Total refunds are %s\n", + TALER_amount2s (&gorc->refund_amount)); + gorc->phase++; +} + + +/** + * Function called with each @a coin_pub that was deposited into the + * @a h_wire account of the merchant for the @a deposit_serial as part + * of the payment for the order identified by @a cls. + * + * Queries the exchange for the payment status associated with the + * given coin. + * + * @param cls a `struct GetOrderRequestContext` + * @param deposit_serial identifies the deposit operation + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param h_wire hash of the merchant's wire account into which the deposit was made + * @param deposit_timestamp when was the deposit made + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param coin_pub public key of the deposited coin + */ +static void +deposit_cb ( + void *cls, + uint64_t deposit_serial, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + struct GetOrderRequestContext *gorc = cls; + struct TransferQuery *tq; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Checking deposit status for coin %s (over %s)\n", + TALER_B2S (coin_pub), + TALER_amount2s (amount_with_fee)); + gorc->last_payment + = GNUNET_TIME_timestamp_max (gorc->last_payment, + deposit_timestamp); + tq = GNUNET_new (struct TransferQuery); + tq->gorc = gorc; + tq->exchange_url = GNUNET_strdup (exchange_url); + tq->deposit_serial = deposit_serial; + GNUNET_CONTAINER_DLL_insert (gorc->tq_head, + gorc->tq_tail, + tq); + tq->coin_pub = *coin_pub; + tq->h_wire = *h_wire; + tq->amount_with_fee = *amount_with_fee; + tq->deposit_fee = *deposit_fee; +} + + +/** + * Check wire transfer status for the order at the exchange. + * + * @param[in,out] gorc order context to update + */ +static void +phase_check_deposits (struct GetOrderRequestContext *gorc) +{ + GNUNET_assert (! gorc->order_only); + GNUNET_assert (gorc->paid); + + /* amount must be always valid for paid orders */ + GNUNET_assert (GNUNET_OK == + TALER_amount_is_valid (&gorc->contract_amount)); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (gorc->contract_amount.currency, + &gorc->deposits_total)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (gorc->contract_amount.currency, + &gorc->deposit_fees_total)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (gorc->contract_amount.currency, + &gorc->deposit_fees_refunded_total)); + TMH_db->lookup_deposits_by_order (TMH_db->cls, + gorc->order_serial, + &deposit_cb, + gorc); + gorc->phase++; +} + + +/** + * Function called with available wire details, to be added to + * the response. + * + * @param cls a `struct GetOrderRequestContext` + * @param wtid wire transfer subject of the wire transfer for the coin + * @param exchange_url base URL of the exchange that made the payment + * @param execution_time when was the payment made + * @param deposit_value contribution of the coin to the total wire transfer value + * @param deposit_fee deposit fee charged by the exchange for the coin + * @param transfer_confirmed did the merchant confirm that a wire transfer with + * @a wtid over the total amount happened? + * @param expected_credit_serial row for the expected wire transfer this + * entry references + */ +static void +process_transfer_details ( + void *cls, + const struct TALER_WireTransferIdentifierRawP *wtid, + const char *exchange_url, + struct GNUNET_TIME_Timestamp execution_time, + const struct TALER_Amount *deposit_value, + const struct TALER_Amount *deposit_fee, + bool transfer_confirmed, + uint64_t expected_credit_serial) +{ + struct GetOrderRequestContext *gorc = cls; + json_t *wire_details = gorc->wire_details; + struct TALER_Amount wired; + + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&gorc->deposits_total, + deposit_value)) || + (GNUNET_OK != + TALER_amount_cmp_currency (&gorc->deposit_fees_total, + deposit_fee)) ) + { + GNUNET_break (0); + gorc->deposit_currency_mismatch = true; + return; + } + + /* Compute total amount *wired* */ + GNUNET_assert (0 <= + TALER_amount_add (&gorc->deposits_total, + &gorc->deposits_total, + deposit_value)); + GNUNET_assert (0 <= + TALER_amount_add (&gorc->deposit_fees_total, + &gorc->deposit_fees_total, + deposit_fee)); + GNUNET_assert (0 <= TALER_amount_subtract (&wired, + deposit_value, + deposit_fee)); + GNUNET_assert (0 == + json_array_append_new ( + wire_details, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("wtid", + wtid), + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + TALER_JSON_pack_amount ("amount", + &wired), + TALER_JSON_pack_amount ("deposit_fee", + deposit_fee), + GNUNET_JSON_pack_timestamp ("execution_time", + execution_time), + GNUNET_JSON_pack_bool ("confirmed", + transfer_confirmed), + GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", + expected_credit_serial)))); +} + + +/** + * Check transfer status in local database. + * + * @param[in,out] gorc order context to update + */ +static void +phase_check_local_transfers (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (gorc->paid); + GNUNET_assert (! gorc->order_only); + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (gorc->contract_amount.currency, + &gorc->deposits_total)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (gorc->contract_amount.currency, + &gorc->deposit_fees_total)); + GNUNET_assert (NULL != gorc->wire_details); + /* We may be running again due to long-polling, clear state first */ + json_array_clear (gorc->wire_details); + qs = TMH_db->lookup_transfer_details_by_order (TMH_db->cls, + gorc->order_serial, + &process_transfer_details, + gorc); + if (0 > qs) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "transfer details")); + return; + } + if (gorc->deposit_currency_mismatch) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "deposits in different currency than original order price")); + return; + } + + if (! gorc->wired) + { + /* we believe(d) the wire transfer did not happen yet, check if maybe + in light of new evidence it did */ + struct TALER_Amount expect_total; + + if (0 > + TALER_amount_subtract (&expect_total, + &gorc->contract_amount, + &gorc->refund_amount)) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "refund exceeds contract value")); + return; + } + GNUNET_assert ( + 0 <= + TALER_amount_add (&expect_total, + &expect_total, + &gorc->deposit_fees_refunded_total)); + + if (0 > + TALER_amount_subtract (&expect_total, + &expect_total, + &gorc->deposit_fees_total)) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "deposit fees exceed total minus refunds")); + return; + } + if (0 >= + TALER_amount_cmp (&expect_total, + &gorc->deposits_total)) + { + /* expect_total <= gorc->deposits_total: good: we got the wire transfer */ + gorc->wired = true; + qs = TMH_db->mark_order_wired (TMH_db->cls, + gorc->order_serial); + GNUNET_break (qs >= 0); /* just warn if transaction failed */ + TMH_notify_order_change (hc->instance, + TMH_OSF_PAID + | TMH_OSF_WIRED, + gorc->contract_terms->timestamp, + gorc->order_serial); + } + } + gorc->phase++; +} + + +/** + * Generate final result for the status request. + * + * @param[in,out] gorc order context to update + */ +static void +phase_reply_result (struct GetOrderRequestContext *gorc) +{ + struct TMH_HandlerContext *hc = gorc->hc; + char *order_status_url; + + GNUNET_assert (gorc->paid); + GNUNET_assert (! gorc->order_only); + + { + struct TALER_PrivateContractHashP *h_contract = NULL; + + /* In a session-bound payment, allow the browser to check the order + * status page (e.g. to get a refund). + * + * Note that we don't allow this outside of session-based payment, as + * otherwise this becomes an oracle to convert order_id to h_contract. + */ + if (NULL != gorc->session_id) + h_contract = &gorc->h_contract_terms; + + order_status_url = + TMH_make_order_status_url (gorc->sc.con, + hc->infix, + gorc->session_id, + hc->instance->settings.id, + &gorc->claim_token, + h_contract); + } + if (GNUNET_TIME_absolute_is_zero (gorc->last_payment.abs_time)) + { + GNUNET_break (GNUNET_YES == + TALER_amount_is_zero (&gorc->contract_amount)); + gorc->last_payment = gorc->contract_terms->timestamp; + } + { + json_t *reply; + + reply = GNUNET_JSON_PACK ( + // Deprecated in protocol v6! + GNUNET_JSON_pack_array_steal ("wire_reports", + json_array ()), + GNUNET_JSON_pack_uint64 ("exchange_code", + gorc->exchange_ec), + GNUNET_JSON_pack_uint64 ("exchange_http_status", + gorc->exchange_hc), + /* legacy: */ + GNUNET_JSON_pack_uint64 ("exchange_ec", + gorc->exchange_ec), + /* legacy: */ + GNUNET_JSON_pack_uint64 ("exchange_hc", + gorc->exchange_hc), + TALER_JSON_pack_amount ("deposit_total", + &gorc->deposits_total), + GNUNET_JSON_pack_object_incref ("contract_terms", + gorc->contract_terms_json), + GNUNET_JSON_pack_string ("order_status", + "paid"), + GNUNET_JSON_pack_timestamp ("last_payment", + gorc->last_payment), + GNUNET_JSON_pack_bool ("refunded", + gorc->refunded), + GNUNET_JSON_pack_bool ("wired", + gorc->wired), + GNUNET_JSON_pack_bool ("refund_pending", + gorc->refund_pending), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("refund_amount", + &gorc->refund_amount)), + GNUNET_JSON_pack_array_incref ("wire_details", + gorc->wire_details), + GNUNET_JSON_pack_array_incref ("refund_details", + gorc->refund_details), + GNUNET_JSON_pack_string ("order_status_url", + order_status_url), + (gorc->choice_index >= 0) + ? GNUNET_JSON_pack_int64 ("choice_index", + gorc->choice_index) + : GNUNET_JSON_pack_end_ ()); + check_reply (gorc, + reply); + json_decref (reply); + } + GNUNET_free (order_status_url); +} + + +/** + * End with error status in wire_hc and wire_ec. + * + * @param[in,out] gorc order context to update + */ +static void +phase_error (struct GetOrderRequestContext *gorc) +{ + GNUNET_assert (TALER_EC_NONE != gorc->wire_ec); + phase_end (gorc, + TALER_MHD_reply_with_error (gorc->sc.con, + gorc->wire_hc, + gorc->wire_ec, + NULL)); +} + + +MHD_RESULT +TMH_private_get_orders_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct GetOrderRequestContext *gorc = hc->ctx; + + if (NULL == gorc) + { + /* First time here, parse request and check order is known */ + GNUNET_assert (NULL != hc->infix); + gorc = GNUNET_new (struct GetOrderRequestContext); + hc->cc = &gorc_cleanup; + hc->ctx = gorc; + gorc->sc.con = connection; + gorc->hc = hc; + gorc->wire_details = json_array (); + GNUNET_assert (NULL != gorc->wire_details); + gorc->refund_details = json_array (); + GNUNET_assert (NULL != gorc->refund_details); + gorc->session_id = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + if (! (TALER_MHD_arg_to_yna (connection, + "allow_refunded_for_repurchase", + TALER_EXCHANGE_YNA_NO, + &gorc->allow_refunded_for_repurchase)) ) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "allow_refunded_for_repurchase"); + TALER_MHD_parse_request_timeout (connection, + &gorc->sc.long_poll_timeout); + TALER_MHD_parse_request_arg_auto (connection, + "lp_not_etag", + &gorc->lp_not_etag, + gorc->have_lp_not_etag); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Starting GET /private/orders/%s processing with timeout %s\n", + hc->infix, + GNUNET_STRINGS_absolute_time_to_string ( + gorc->sc.long_poll_timeout)); + } + if (GNUNET_SYSERR == gorc->suspended) + return MHD_NO; /* we are in shutdown */ + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing order %s in phase %d\n", + hc->infix, + (int) gorc->phase); + switch (gorc->phase) + { + case GOP_INIT: + phase_init (gorc); + break; + case GOP_FETCH_CONTRACT: + phase_fetch_contract (gorc); + break; + case GOP_PARSE_CONTRACT: + phase_parse_contract (gorc); + break; + case GOP_CHECK_PAID: + phase_check_paid (gorc); + break; + case GOP_CHECK_REPURCHASE: + phase_check_repurchase (gorc); + break; + case GOP_UNPAID_FINISH: + phase_unpaid_finish (gorc); + break; + case GOP_CHECK_REFUNDS: + phase_check_refunds (gorc); + break; + case GOP_CHECK_DEPOSITS: + phase_check_deposits (gorc); + break; + case GOP_CHECK_LOCAL_TRANSFERS: + phase_check_local_transfers (gorc); + break; + case GOP_REPLY_RESULT: + phase_reply_result (gorc); + break; + case GOP_ERROR: + phase_error (gorc); + break; + case GOP_SUSPENDED_ON_UNPAID: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending order request awaiting payment\n"); + return MHD_YES; + case GOP_END_YES: + return MHD_YES; + case GOP_END_NO: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Closing connection, no response generated\n"); + return MHD_NO; + } + } /* end first-time per-request initialization */ +} diff --git a/src/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.h b/src/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + (C) 2017, 2020 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/> +*/ +/** + * @file taler-merchant-httpd_get-private-orders-ORDER_ID.h + * @brief headers for GET /private/orders/ID handler + * @author Christian Grothoff + * @author Florian Dold + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_ID_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + +/** + * Manages a GET /private/orders/ID call, checking the status of a payment and + * refunds and, if necessary, constructing the URL for a payment redirect URL. + * + * @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_orders_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Force resuming all long polling GET orders ID requests, we are shutting + * down. + */ +void +TMH_force_gorc_resume (void); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-orders.c b/src/backend/taler-merchant-httpd_get-private-orders.c @@ -0,0 +1,1533 @@ +/* + This file is part of TALER + (C) 2019--2026 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-httpd_get-private-orders.c + * @brief implement GET /orders + * @author Christian Grothoff + * + * FIXME-cleanup: consider introducing phases / state machine + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-orders.h" +#include <taler/taler_merchant_util.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> + + +/** + * Sensible bound on TALER_MERCHANTDB_OrderFilter.delta + */ +#define MAX_DELTA 1024 + +#define CSV_HEADER \ + "Order ID,Row,Timestamp,Amount,Refund amount,Pending refund amount,Summary,Refundable,Paid\r\n" +#define CSV_FOOTER "\r\n" + +#define XML_HEADER "<?xml version=\"1.0\"?>" \ + "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \ + " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \ + " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \ + " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \ + " xmlns:o=\"urn:schemas-microsoft-com:office:office\"" \ + " xmlns:x=\"urn:schemas-microsoft-com:office:excel\"" \ + " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \ + "<Styles>" \ + "<Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd hh:mm:ss\"/></Style>" \ + "<Style ss:ID=\"Total\"><Font ss:Bold=\"1\"/></Style>" \ + "</Styles>\n" \ + "<Worksheet ss:Name=\"Orders\">\n" \ + "<Table>\n" \ + "<Row>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Order ID</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Timestamp</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Price</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Refunded</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Summary</Data></Cell>\n" \ + "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Paid</Data></Cell>\n" \ + "</Row>\n" +#define XML_FOOTER "</Table></Worksheet></Workbook>" + + +/** + * A pending GET /orders request. + */ +struct TMH_PendingOrder +{ + + /** + * Kept in a DLL. + */ + struct TMH_PendingOrder *prev; + + /** + * Kept in a DLL. + */ + struct TMH_PendingOrder *next; + + /** + * Which connection was suspended. + */ + struct MHD_Connection *con; + + /** + * Which instance is this client polling? This also defines + * which DLL this struct is part of. + */ + struct TMH_MerchantInstance *mi; + + /** + * At what time does this request expire? If set in the future, we + * may wait this long for a payment to arrive before responding. + */ + struct GNUNET_TIME_Absolute long_poll_timeout; + + /** + * Filter to apply. + */ + struct TALER_MERCHANTDB_OrderFilter of; + + /** + * The array of orders (used for JSON and PDF/Typst). + */ + json_t *pa; + + /** + * Running total of order amounts, for totals row in CSV/XML/PDF. + * Initialised to zero on first order seen. + */ + struct TALER_Amount total_amount; + + /** + * Running total of granted refund amounts. + * Initialised to zero on first paid order seen. + */ + struct TALER_Amount total_refund_amount; + + /** + * Running total of pending refund amounts. + * Initialised to zero on first paid order seen. + */ + struct TALER_Amount total_pending_refund_amount; + + /** + * True once @e total_amount has been initialised with a currency. + */ + bool total_amount_initialized; + + /** + * True once @e total_refund_amount / @e total_pending_refund_amount + * have been initialised with a currency. + */ + bool total_refund_initialized; + + /** + * The name of the instance we are querying for. + */ + const char *instance_id; + + /** + * Alias of @a of.summary_filter, but with memory to be released (owner). + */ + char *summary_filter; + + /** + * The result after adding the orders (#TALER_EC_NONE for okay, anything else for an error). + */ + enum TALER_ErrorCode result; + + /** + * Is the structure in the DLL + */ + bool in_dll; + + /** + * Output format requested by the client. + */ + enum + { + POF_JSON, + POF_CSV, + POF_XML, + POF_PDF + } format; + + /** + * Buffer used when format is #POF_CSV. + */ + struct GNUNET_Buffer csv; + + /** + * Buffer used when format is #POF_XML. + */ + struct GNUNET_Buffer xml; + + /** + * Async context used to run Typst (for #POF_PDF). + */ + struct TALER_MHD_TypstContext *tc; + + /** + * Pre-built MHD response (used when #POF_PDF Typst is done). + */ + struct MHD_Response *response; + + /** + * Task to timeout pending order. + */ + struct GNUNET_SCHEDULER_Task *order_timeout_task; + + /** + * HTTP status to return with @e response. + */ + unsigned int http_status; +}; + + +/** + * DLL head for requests suspended waiting for Typst. + */ +static struct TMH_PendingOrder *pdf_head; + +/** + * DLL tail for requests suspended waiting for Typst. + */ +static struct TMH_PendingOrder *pdf_tail; + + +void +TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi) +{ + struct TMH_PendingOrder *po; + + while (NULL != (po = mi->po_head)) + { + GNUNET_assert (po->in_dll); + GNUNET_CONTAINER_DLL_remove (mi->po_head, + mi->po_tail, + po); + MHD_resume_connection (po->con); + po->in_dll = false; + } + if (NULL != mi->po_eh) + { + TMH_db->event_listen_cancel (mi->po_eh); + mi->po_eh = NULL; + } +} + + +void +TMH_force_get_orders_resume_typst () +{ + struct TMH_PendingOrder *po; + + while (NULL != (po = pdf_head)) + { + GNUNET_CONTAINER_DLL_remove (pdf_head, + pdf_tail, + po); + MHD_resume_connection (po->con); + } +} + + +/** + * Task run to trigger timeouts on GET /orders requests with long polling. + * + * @param cls a `struct TMH_PendingOrder *` + */ +static void +order_timeout (void *cls) +{ + struct TMH_PendingOrder *po = cls; + struct TMH_MerchantInstance *mi = po->mi; + + po->order_timeout_task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming long polled job due to timeout\n"); + GNUNET_assert (po->in_dll); + GNUNET_CONTAINER_DLL_remove (mi->po_head, + mi->po_tail, + po); + po->in_dll = false; + MHD_resume_connection (po->con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Cleanup our "context", where we stored the data + * we are building for the response. + * + * @param ctx context to clean up, must be a `struct TMH_PendingOrder *` + */ +static void +cleanup (void *ctx) +{ + struct TMH_PendingOrder *po = ctx; + + if (po->in_dll) + { + struct TMH_MerchantInstance *mi = po->mi; + + GNUNET_CONTAINER_DLL_remove (mi->po_head, + mi->po_tail, + po); + MHD_resume_connection (po->con); + } + if (NULL != po->order_timeout_task) + { + GNUNET_SCHEDULER_cancel (po->order_timeout_task); + po->order_timeout_task = NULL; + } + json_decref (po->pa); + GNUNET_free (po->summary_filter); + switch (po->format) + { + case POF_JSON: + break; + case POF_CSV: + GNUNET_buffer_clear (&po->csv); + break; + case POF_XML: + GNUNET_buffer_clear (&po->xml); + break; + case POF_PDF: + if (NULL != po->tc) + { + TALER_MHD_typst_cancel (po->tc); + po->tc = NULL; + } + break; + } + if (NULL != po->response) + { + MHD_destroy_response (po->response); + po->response = NULL; + } + GNUNET_free (po); +} + + +/** + * Closure for #process_refunds_cb(). + */ +struct ProcessRefundsClosure +{ + /** + * Place where we accumulate the granted refunds. + */ + struct TALER_Amount total_refund_amount; + + /** + * Place where we accumulate the pending refunds. + */ + struct TALER_Amount pending_refund_amount; + + /** + * Set to an error code if something goes wrong. + */ + enum TALER_ErrorCode ec; +}; + + +/** + * Function called with information about a refund. + * It is responsible for summing up the refund amount. + * + * @param cls closure + * @param refund_serial unique serial number of the refund + * @param timestamp time of the refund (for grouping of refunds in the wallet UI) + * @param coin_pub public coin from which the refund comes from + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param rtransaction_id identificator of the refund + * @param reason human-readable explanation of the refund + * @param refund_amount refund amount which is being taken from @a coin_pub + * @param pending true if the this refund was not yet processed by the wallet/exchange + */ +static void +process_refunds_cb (void *cls, + uint64_t refund_serial, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + uint64_t rtransaction_id, + const char *reason, + const struct TALER_Amount *refund_amount, + bool pending) +{ + struct ProcessRefundsClosure *prc = cls; + + if (GNUNET_OK != + TALER_amount_cmp_currency (&prc->total_refund_amount, + refund_amount)) + { + /* Database error, refunds in mixed currency in DB. Not OK! */ + prc->ec = TALER_EC_GENERIC_DB_INVARIANT_FAILURE; + GNUNET_break (0); + return; + } + GNUNET_assert (0 <= + TALER_amount_add (&prc->total_refund_amount, + &prc->total_refund_amount, + refund_amount)); + if (pending) + GNUNET_assert (0 <= + TALER_amount_add (&prc->pending_refund_amount, + &prc->pending_refund_amount, + refund_amount)); +} + + +/** + * Add one order entry to the running order-amount total in @a po. + * Sets po->result to #TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow. + * + * @param[in,out] po pending order accumulator + * @param amount the order amount to add + */ +static void +accumulate_total (struct TMH_PendingOrder *po, + const struct TALER_Amount *amount) +{ + if (! po->total_amount_initialized) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (amount->currency, + &po->total_amount)); + po->total_amount_initialized = true; + } + if (0 > TALER_amount_add (&po->total_amount, + &po->total_amount, + amount)) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT; + } +} + + +/** + * Add refund amounts to the running refund totals in @a po. + * Sets po->result to #TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow. + * Only called for paid orders (where refund tracking is meaningful). + * + * @param[in,out] po pending order accumulator + * @param refund granted refund amount for this order + * @param pending pending (not-yet-processed) refund amount for this order + */ +static void +accumulate_refund_totals (struct TMH_PendingOrder *po, + const struct TALER_Amount *refund, + const struct TALER_Amount *pending) +{ + if (TALER_EC_NONE != po->result) + return; + if (! po->total_refund_initialized) + { + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (refund->currency, + &po->total_refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pending->currency, + &po->total_pending_refund_amount)); + po->total_refund_initialized = true; + } + if (0 > TALER_amount_add (&po->total_refund_amount, + &po->total_refund_amount, + refund)) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT; + return; + } + if (0 > TALER_amount_add (&po->total_pending_refund_amount, + &po->total_pending_refund_amount, + pending)) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT; + } +} + + +/** + * Add order details to our response accumulator. + * + * @param cls some closure + * @param orig_order_id the order this is about + * @param order_serial serial ID of the order + * @param creation_time when was the order created + */ +static void +add_order (void *cls, + const char *orig_order_id, + uint64_t order_serial, + struct GNUNET_TIME_Timestamp creation_time) +{ + struct TMH_PendingOrder *po = cls; + json_t *contract_terms = NULL; + struct TALER_PrivateContractHashP h_contract_terms; + enum GNUNET_DB_QueryStatus qs; + char *order_id = NULL; + bool refundable = false; + bool paid; + bool wired; + struct TALER_MERCHANT_Contract *contract = NULL; + int16_t choice_index = -1; + struct ProcessRefundsClosure prc = { + .ec = TALER_EC_NONE + }; + const struct TALER_Amount *amount; + char amount_buf[128]; + char refund_buf[128]; + char pending_buf[128]; + + /* Bail early if we already have an error */ + if (TALER_EC_NONE != po->result) + return; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Adding order `%s' (%llu) to result set at instance `%s'\n", + orig_order_id, + (unsigned long long) order_serial, + po->instance_id); + qs = TMH_db->lookup_order_status_by_serial (TMH_db->cls, + po->instance_id, + order_serial, + &order_id, + &h_contract_terms, + &paid); + if (qs < 0) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Contract terms don't exist, so the order cannot be paid. */ + paid = false; + if (NULL == orig_order_id) + { + /* Got a DB trigger about a new proposal, but it + was already deleted again. Just ignore the event. */ + return; + } + order_id = GNUNET_strdup (orig_order_id); + } + + { + /* First try to find the order in the contracts */ + uint64_t os; + bool session_matches; + + qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, + po->instance_id, + order_id, + NULL, + &contract_terms, + &os, + &paid, + &wired, + &session_matches, + NULL, + &choice_index); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + GNUNET_break (os == order_serial); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* Might still be unclaimed, so try order table */ + struct TALER_MerchantPostDataHashP unused; + + paid = false; + wired = false; + qs = TMH_db->lookup_order (TMH_db->cls, + po->instance_id, + order_id, + NULL, + &unused, + &contract_terms); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Order %llu disappeared during iteration. Skipping.\n", + (unsigned long long) order_serial); + goto cleanup; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; + goto cleanup; + } + + contract = TALER_MERCHANT_contract_parse (contract_terms, + true); + if (NULL == contract) + { + GNUNET_break (0); + po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; + goto cleanup; + } + + if (paid) + { + const struct TALER_Amount *brutto; + + switch (contract->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + brutto = &contract->details.v0.brutto; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + { + struct TALER_MERCHANT_ContractChoice *choice + = &contract->details.v1.choices[choice_index]; + + GNUNET_assert (choice_index < contract->details.v1.choices_len); + brutto = &choice->amount; + } + break; + default: + GNUNET_break (0); + goto cleanup; + } + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (brutto->currency, + &prc.total_refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (brutto->currency, + &prc.pending_refund_amount)); + + qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, + po->instance_id, + &h_contract_terms, + &process_refunds_cb, + &prc); + if (0 > qs) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; + goto cleanup; + } + if (TALER_EC_NONE != prc.ec) + { + GNUNET_break (0); + po->result = prc.ec; + goto cleanup; + } + if (0 > TALER_amount_cmp (&prc.total_refund_amount, + brutto) && + GNUNET_TIME_absolute_is_future (contract->refund_deadline.abs_time)) + refundable = true; + } + + /* compute amount totals */ + amount = NULL; + switch (contract->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + { + amount = &contract->details.v0.brutto; + + if (TALER_amount_is_zero (amount) && + (po->of.wired != TALER_EXCHANGE_YNA_ALL) ) + { + /* If we are actually filtering by wire status, + and the order was over an amount of zero, + do not return it as wire status is not + exactly meaningful for orders over zero. */ + goto cleanup; + } + + /* Accumulate order total */ + if (paid) + accumulate_total (po, + amount); + if (TALER_EC_NONE != po->result) + goto cleanup; + /* Accumulate refund totals (only meaningful for paid orders) */ + if (paid) + { + accumulate_refund_totals (po, + &prc.total_refund_amount, + &prc.pending_refund_amount); + if (TALER_EC_NONE != po->result) + goto cleanup; + } + } + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + if (-1 == choice_index) + choice_index = 0; /* default choice */ + GNUNET_assert (choice_index < contract->details.v1.choices_len); + { + struct TALER_MERCHANT_ContractChoice *choice + = &contract->details.v1.choices[choice_index]; + + amount = &choice->amount; + /* Accumulate order total */ + accumulate_total (po, + amount); + if (TALER_EC_NONE != po->result) + goto cleanup; + /* Accumulate refund totals (only meaningful for paid orders) */ + if (paid) + { + accumulate_refund_totals (po, + &prc.total_refund_amount, + &prc.pending_refund_amount); + if (TALER_EC_NONE != po->result) + goto cleanup; + } + } + default: + GNUNET_break (0); + po->result = TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION; + goto cleanup; + } + + /* convert amounts to strings (needed for some formats) */ + /* FIXME: use currency formatting rules in the future + instead of TALER_amount2s for human readability... */ + strcpy (amount_buf, + TALER_amount2s (amount)); + if (paid) + strcpy (refund_buf, + TALER_amount2s (&prc.total_refund_amount)); + if (paid) + strcpy (pending_buf, + TALER_amount2s (&prc.pending_refund_amount)); + + switch (po->format) + { + case POF_JSON: + case POF_PDF: + GNUNET_assert ( + 0 == + json_array_append_new ( + po->pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_id", + contract->order_id), + GNUNET_JSON_pack_uint64 ("row_id", + order_serial), + GNUNET_JSON_pack_timestamp ("timestamp", + creation_time), + TALER_JSON_pack_amount ("amount", + amount), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ( + "refund_amount", + paid + ? &prc.total_refund_amount + : NULL)), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ( + "pending_refund_amount", + paid + ? &prc.pending_refund_amount + : NULL)), + GNUNET_JSON_pack_string ("summary", + contract->summary), + GNUNET_JSON_pack_bool ("refundable", + refundable), + GNUNET_JSON_pack_bool ("paid", + paid)))); + break; + case POF_CSV: + { + size_t len = strlen (contract->summary); + size_t wpos = 0; + char *esummary; + + /* Escape 'summary' to double '"' as per RFC 4180, 2.7. */ + esummary = GNUNET_malloc (2 * len + 1); + for (size_t off = 0; off<len; off++) + { + if ('"' == contract->summary[off]) + esummary[wpos++] = '"'; + esummary[wpos++] = contract->summary[off]; + } + + GNUNET_buffer_write_fstr ( + &po->csv, + "%s,%llu,%llu,%s,%s,%s,\"%s\",%s,%s\r\n", + contract->order_id, + (unsigned long long) order_serial, + (unsigned long long) GNUNET_TIME_timestamp_to_s (creation_time), + amount_buf, + paid ? refund_buf : "", + paid ? pending_buf : "", + esummary, + refundable ? "yes" : "no", + paid ? "yes" : "no"); + GNUNET_free (esummary); + break; + } + case POF_XML: + { + char *esummary = TALER_escape_xml (contract->summary); + char creation_time_s[128]; + const struct tm *tm; + time_t tt; + + tt = (time_t) GNUNET_TIME_timestamp_to_s (creation_time); + tm = gmtime (&tt); + strftime (creation_time_s, + sizeof (creation_time_s), + "%Y-%m-%dT%H:%M:%S", + tm); + GNUNET_buffer_write_fstr ( + &po->xml, + "<Row>" + "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"DateTime\">%s</Data></Cell>" + "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>" + "</Row>\n", + contract->order_id, + creation_time_s, + amount_buf, + paid ? refund_buf : "", + NULL != esummary ? esummary : "", + paid ? "TRUE" : "FALSE", + paid ? "1" : "0"); + GNUNET_free (esummary); + } + break; + } /* end switch po->format */ + +cleanup: + json_decref (contract_terms); + GNUNET_free (order_id); + if (NULL != contract) + { + TALER_MERCHANT_contract_free (contract); + contract = NULL; + } +} + + +/** + * We have received a trigger from the database + * that we should (possibly) resume some requests. + * + * @param cls a `struct TMH_MerchantInstance` + * @param extra a `struct TMH_OrderChangeEventP` + * @param extra_size number of bytes in @a extra + */ +static void +resume_by_event (void *cls, + const void *extra, + size_t extra_size) +{ + struct TMH_MerchantInstance *mi = cls; + const struct TMH_OrderChangeEventDetailsP *oce = extra; + struct TMH_PendingOrder *pn; + enum TMH_OrderStateFlags osf; + uint64_t order_serial_id; + struct GNUNET_TIME_Timestamp date; + + if (sizeof (*oce) != extra_size) + { + GNUNET_break (0); + return; + } + osf = (enum TMH_OrderStateFlags) (int) ntohl (oce->order_state); + order_serial_id = GNUNET_ntohll (oce->order_serial_id); + date = GNUNET_TIME_timestamp_ntoh (oce->execution_date); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received notification about order %llu\n", + (unsigned long long) order_serial_id); + for (struct TMH_PendingOrder *po = mi->po_head; + NULL != po; + po = pn) + { + pn = po->next; + if (! ( ( ((TALER_EXCHANGE_YNA_YES == po->of.paid) == + (0 != (osf & TMH_OSF_PAID))) || + (TALER_EXCHANGE_YNA_ALL == po->of.paid) ) && + ( ((TALER_EXCHANGE_YNA_YES == po->of.refunded) == + (0 != (osf & TMH_OSF_REFUNDED))) || + (TALER_EXCHANGE_YNA_ALL == po->of.refunded) ) && + ( ((TALER_EXCHANGE_YNA_YES == po->of.wired) == + (0 != (osf & TMH_OSF_WIRED))) || + (TALER_EXCHANGE_YNA_ALL == po->of.wired) ) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client %p waits on different order type\n", + po); + continue; + } + if (po->of.delta > 0) + { + if (order_serial_id < po->of.start_row) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client %p waits on different order row\n", + po); + continue; + } + if (GNUNET_TIME_timestamp_cmp (date, + <, + po->of.date)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client %p waits on different order date\n", + po); + continue; + } + po->of.delta--; + } + else + { + if (order_serial_id > po->of.start_row) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client %p waits on different order row\n", + po); + continue; + } + if (GNUNET_TIME_timestamp_cmp (date, + >, + po->of.date)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client %p waits on different order date\n", + po); + continue; + } + po->of.delta++; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Waking up client %p!\n", + po); + add_order (po, + NULL, + order_serial_id, + date); + GNUNET_assert (po->in_dll); + GNUNET_CONTAINER_DLL_remove (mi->po_head, + mi->po_tail, + po); + po->in_dll = false; + MHD_resume_connection (po->con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + } + if (NULL == mi->po_head) + { + TMH_db->event_listen_cancel (mi->po_eh); + mi->po_eh = NULL; + } +} + + +/** + * There has been a change or addition of a new @a order_id. Wake up + * long-polling clients that may have been waiting for this event. + * + * @param mi the instance where the order changed + * @param osf order state flags + * @param date execution date of the order + * @param order_serial_id serial ID of the order in the database + */ +void +TMH_notify_order_change (struct TMH_MerchantInstance *mi, + enum TMH_OrderStateFlags osf, + struct GNUNET_TIME_Timestamp date, + uint64_t order_serial_id) +{ + struct TMH_OrderChangeEventDetailsP oce = { + .order_serial_id = GNUNET_htonll (order_serial_id), + .execution_date = GNUNET_TIME_timestamp_hton (date), + .order_state = htonl (osf) + }; + struct TMH_OrderChangeEventP eh = { + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE), + .header.size = htons (sizeof (eh)), + .merchant_pub = mi->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients of new order %llu at %s\n", + (unsigned long long) order_serial_id, + TALER_B2S (&mi->merchant_pub)); + TMH_db->event_notify (TMH_db->cls, + &eh.header, + &oce, + sizeof (oce)); +} + + +/** + * Transforms an (untrusted) input filter into a Postgresql LIKE filter. + * Escapes "%" and "_" in the @a input and adds "%" at the beginning + * and the end to turn the @a input into a suitable Postgresql argument. + * + * @param input text to turn into a substring match expression, or NULL + * @return NULL if @a input was NULL, otherwise transformed @a input + */ +static char * +tr (const char *input) +{ + char *out; + size_t slen; + size_t wpos; + + if (NULL == input) + return NULL; + slen = strlen (input); + out = GNUNET_malloc (slen * 2 + 3); + wpos = 0; + out[wpos++] = '%'; + for (size_t i = 0; i<slen; i++) + { + char c = input[i]; + + if ( (c == '%') || + (c == '_') ) + out[wpos++] = '\\'; + out[wpos++] = c; + } + out[wpos++] = '%'; + GNUNET_assert (wpos < slen * 2 + 3); + return out; +} + + +/** + * Function called with the result of a #TALER_MHD_typst() operation. + * + * @param cls closure, a `struct TMH_PendingOrder *` + * @param tr result of the operation + */ +static void +pdf_cb (void *cls, + const struct TALER_MHD_TypstResponse *tr) +{ + struct TMH_PendingOrder *po = cls; + + po->tc = NULL; + GNUNET_CONTAINER_DLL_remove (pdf_head, + pdf_tail, + po); + if (TALER_EC_NONE != tr->ec) + { + po->http_status + = TALER_ErrorCode_get_http_status (tr->ec); + po->response + = TALER_MHD_make_error (tr->ec, + tr->details.hint); + } + else + { + po->http_status = MHD_HTTP_OK; + po->response = TALER_MHD_response_from_pdf_file (tr->details.filename); + } + MHD_resume_connection (po->con); + TALER_MHD_daemon_trigger (); +} + + +/** + * Build the final response for a completed (non-long-poll) request and + * queue it on @a connection. + * + * Handles all formats (JSON, CSV, XML, PDF). For PDF this may suspend + * the connection while Typst runs asynchronously; in that case the caller + * must return #MHD_YES immediately. + * + * @param po the pending order state (already fully populated) + * @param connection the MHD connection + * @param mi the merchant instance + * @return MHD result code + */ +static MHD_RESULT +reply_orders (struct TMH_PendingOrder *po, + struct MHD_Connection *connection, + struct TMH_MerchantInstance *mi) +{ + char total_buf[128]; + char refund_buf[128]; + char pending_buf[128]; + + if (po->total_amount_initialized) + strcpy (total_buf, + TALER_amount2s (&po->total_amount)); + if (po->total_refund_initialized) + strcpy (refund_buf, + TALER_amount2s (&po->total_refund_amount)); + if (po->total_refund_initialized) + strcpy (pending_buf, + TALER_amount2s (&po->total_pending_refund_amount)); + + switch (po->format) + { + case POF_JSON: + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_incref ("orders", + po->pa)); + case POF_CSV: + { + struct MHD_Response *resp; + MHD_RESULT mret; + + GNUNET_buffer_write_fstr ( + &po->csv, + "Total (paid only),,,%s,%s,%s,,,\r\n", + po->total_amount_initialized + ? total_buf + : "-", + po->total_refund_initialized + ? refund_buf + : "-", + po->total_refund_initialized + ? pending_buf + : "-"); + GNUNET_buffer_write_str (&po->csv, + CSV_FOOTER); + resp = MHD_create_response_from_buffer (po->csv.position, + po->csv.mem, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp, + false); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "text/csv")); + mret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return mret; + } + case POF_XML: + { + struct MHD_Response *resp; + MHD_RESULT mret; + + /* Append totals row with paid and refunded amount columns */ + GNUNET_buffer_write_fstr ( + &po->xml, + "<Row>" + "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">Total (paid only)</Data></Cell>" + "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>" + "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>" + "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>" + "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>" + "</Row>\n", + po->total_amount_initialized + ? total_buf + : "-", + po->total_refund_initialized + ? refund_buf + : "-"); + GNUNET_buffer_write_str (&po->xml, + XML_FOOTER); + resp = MHD_create_response_from_buffer (po->xml.position, + po->xml.mem, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp, + false); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_CONTENT_TYPE, + "application/vnd.ms-excel")); + mret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return mret; + } + case POF_PDF: + { + /* Build the JSON document for Typst, passing all totals */ + json_t *root; + struct TALER_MHD_TypstDocument doc; + + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("business_name", + mi->settings.name), + GNUNET_JSON_pack_array_incref ("orders", + po->pa), + po->total_amount_initialized + ? TALER_JSON_pack_amount ("total_amount", + &po->total_amount) + : GNUNET_JSON_pack_string ("total_amount", + "-"), + po->total_refund_initialized + ? TALER_JSON_pack_amount ("total_refund_amount", + &po->total_refund_amount) + : GNUNET_JSON_pack_string ("total_refund_amount", + "-"), + po->total_refund_initialized + ? TALER_JSON_pack_amount ("total_pending_refund_amount", + &po->total_pending_refund_amount) + : GNUNET_JSON_pack_string ("total_pending_refund_amount", + "-")); + doc.form_name = "orders"; + doc.form_version = "0.0.0"; + doc.data = root; + + po->tc = TALER_MHD_typst (TMH_cfg, + false, /* remove on exit */ + "merchant", + 1, /* one document */ + &doc, + &pdf_cb, + po); + json_decref (root); + if (NULL == po->tc) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client requested PDF, but Typst is unavailable\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, + NULL); + } + GNUNET_CONTAINER_DLL_insert (pdf_head, + pdf_tail, + po); + MHD_suspend_connection (connection); + return MHD_YES; + } + } /* end switch */ + GNUNET_assert (0); + return MHD_NO; +} + + +/** + * Handle a GET "/orders" 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_orders (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_PendingOrder *po = hc->ctx; + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + + if (NULL != po) + { + if (TALER_EC_NONE != po->result) + { + /* Resumed from long-polling with error */ + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + po->result, + NULL); + } + if (POF_PDF == po->format) + { + /* resumed from long-polling or from Typst PDF generation */ + /* We really must have a response in this case */ + if (NULL == po->response) + { + GNUNET_break (0); + return MHD_NO; + } + return MHD_queue_response (connection, + po->http_status, + po->response); + } + return reply_orders (po, + connection, + mi); + } + po = GNUNET_new (struct TMH_PendingOrder); + hc->ctx = po; + hc->cc = &cleanup; + po->con = connection; + po->pa = json_array (); + GNUNET_assert (NULL != po->pa); + po->instance_id = mi->settings.id; + po->mi = mi; + + /* Determine desired output format from Accept header */ + { + const char *mime; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "application/json"; + if (0 == strcmp (mime, + "*/*")) + mime = "application/json"; + if (0 == strcmp (mime, + "application/json")) + { + po->format = POF_JSON; + } + else if (0 == strcmp (mime, + "text/csv")) + { + po->format = POF_CSV; + GNUNET_buffer_write_str (&po->csv, + CSV_HEADER); + } + else if (0 == strcmp (mime, + "application/vnd.ms-excel")) + { + po->format = POF_XML; + GNUNET_buffer_write_str (&po->xml, + XML_HEADER); + } + else if (0 == strcmp (mime, + "application/pdf")) + { + po->format = POF_PDF; + } + else + { + GNUNET_break_op (0); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_NOT_ACCEPTABLE, + GNUNET_JSON_pack_string ("hint", + mime)); + } + } + + if (! (TALER_MHD_arg_to_yna (connection, + "paid", + TALER_EXCHANGE_YNA_ALL, + &po->of.paid)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paid"); + } + if (! (TALER_MHD_arg_to_yna (connection, + "refunded", + TALER_EXCHANGE_YNA_ALL, + &po->of.refunded)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "refunded"); + } + if (! (TALER_MHD_arg_to_yna (connection, + "wired", + TALER_EXCHANGE_YNA_ALL, + &po->of.wired)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "wired"); + } + po->of.delta = -20; + /* deprecated in protocol v12 */ + TALER_MHD_parse_request_snumber (connection, + "delta", + &po->of.delta); + /* since protocol v12 */ + TALER_MHD_parse_request_snumber (connection, + "limit", + &po->of.delta); + if ( (-MAX_DELTA > po->of.delta) || + (po->of.delta > MAX_DELTA) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "limit"); + } + { + const char *date_s_str; + + date_s_str = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "date_s"); + if (NULL == date_s_str) + { + if (po->of.delta > 0) + po->of.date = GNUNET_TIME_UNIT_ZERO_TS; + else + po->of.date = GNUNET_TIME_UNIT_FOREVER_TS; + } + else + { + char dummy; + unsigned long long ll; + + if (1 != + sscanf (date_s_str, + "%llu%c", + &ll, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "date_s"); + } + + po->of.date = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_from_s (ll)); + if (GNUNET_TIME_absolute_is_never (po->of.date.abs_time)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "date_s"); + } + } + } + if (po->of.delta > 0) + { + struct GNUNET_TIME_Relative duration + = GNUNET_TIME_UNIT_FOREVER_REL; + struct GNUNET_TIME_Absolute cut_off; + + TALER_MHD_parse_request_rel_time (connection, + "max_age", + &duration); + cut_off = GNUNET_TIME_absolute_subtract (GNUNET_TIME_absolute_get (), + duration); + po->of.date = GNUNET_TIME_timestamp_max ( + po->of.date, + GNUNET_TIME_absolute_to_timestamp (cut_off)); + } + if (po->of.delta > 0) + po->of.start_row = 0; + else + po->of.start_row = INT64_MAX; + /* deprecated in protocol v12 */ + TALER_MHD_parse_request_number (connection, + "start", + &po->of.start_row); + /* since protocol v12 */ + TALER_MHD_parse_request_number (connection, + "offset", + &po->of.start_row); + if (INT64_MAX < po->of.start_row) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "offset"); + } + po->summary_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "summary_filter")); + po->of.summary_filter = po->summary_filter; /* just an (read-only) alias! */ + po->of.session_id + = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "session_id"); + po->of.fulfillment_url + = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "fulfillment_url"); + TALER_MHD_parse_request_timeout (connection, + &po->long_poll_timeout); + if (GNUNET_TIME_absolute_is_never (po->long_poll_timeout)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "timeout_ms"); + } + if ( (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) && + (NULL == mi->po_eh) ) + { + struct TMH_OrderChangeEventP change_eh = { + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE), + .header.size = htons (sizeof (change_eh)), + .merchant_pub = mi->merchant_pub + }; + + mi->po_eh = TMH_db->event_listen (TMH_db->cls, + &change_eh.header, + GNUNET_TIME_UNIT_FOREVER_REL, + &resume_by_event, + mi); + } + + po->of.timeout = GNUNET_TIME_absolute_get_remaining (po->long_poll_timeout); + + qs = TMH_db->lookup_orders (TMH_db->cls, + po->instance_id, + &po->of, + &add_order, + po); + if (0 > qs) + { + GNUNET_break (0); + po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; + } + if (TALER_EC_NONE != po->result) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + po->result, + NULL); + } + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) && + (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) ) + { + GNUNET_assert (NULL == po->order_timeout_task); + po->order_timeout_task + = GNUNET_SCHEDULER_add_at (po->long_poll_timeout, + &order_timeout, + po); + GNUNET_CONTAINER_DLL_insert (mi->po_head, + mi->po_tail, + po); + po->in_dll = true; + MHD_suspend_connection (connection); + return MHD_YES; + } + return reply_orders (po, + connection, + mi); +} + + +/* end of taler-merchant-httpd_get-private-orders.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-orders.h b/src/backend/taler-merchant-httpd_get-private-orders.h @@ -0,0 +1,76 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_get-private-orders.h + * @brief implement GET /orders + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/orders" 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_orders (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * There has been a change or addition of a new @a order_id. Wake up + * long-polling clients that may have been waiting for this event. + * + * @param mi the instance where the order changed + * @param osf order state flags + * @param date execution date of the order + * @param order_serial_id serial ID of the order in the database + */ +void +TMH_notify_order_change (struct TMH_MerchantInstance *mi, + enum TMH_OrderStateFlags osf, + struct GNUNET_TIME_Timestamp date, + uint64_t order_serial_id); + + +/** + * We are shutting down (or an instance is being deleted), force resume of all + * GET /orders requests. + * + * @param mi instance to force resuming for + */ +void +TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi); + + +/** + * We are shutting down (or an instance is being deleted), force resume of all + * GET /orders requests. + */ +void +TMH_force_get_orders_resume_typst (void); + + +/* end of taler-merchant-httpd_get-private-orders.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.c b/src/backend/taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.c @@ -0,0 +1,110 @@ +/* + This file is part of TALER + (C) 2022-2024 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-httpd_get-private-otp-devices-DEVICE_ID.c + * @brief implement GET /otp-devices/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_OtpDeviceDetails tp = { 0 }; + enum GNUNET_DB_QueryStatus qs; + uint64_t faketime_s + = GNUNET_TIME_timestamp_to_s (GNUNET_TIME_timestamp_get ()); + struct GNUNET_TIME_Timestamp my_time; + struct TALER_Amount price; + + TALER_MHD_parse_request_number (connection, + "faketime", + &faketime_s); + memset (&price, + 0, + sizeof (price)); + TALER_MHD_parse_request_amount (connection, + "price", + &price); + my_time = GNUNET_TIME_timestamp_from_s (faketime_s); + GNUNET_assert (NULL != mi); + qs = TMH_db->select_otp (TMH_db->cls, + mi->settings.id, + hc->infix, + &tp); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_otp"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + char *pos_confirmation; + + pos_confirmation = (NULL == tp.otp_key) + ? NULL + : TALER_build_pos_confirmation (tp.otp_key, + tp.otp_algorithm, + &price, + my_time); + /* Note: we deliberately (by design) do not return the otp_key */ + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("device_description", + tp.otp_description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_code", + pos_confirmation)), + GNUNET_JSON_pack_uint64 ("otp_timestamp", + faketime_s), + GNUNET_JSON_pack_uint64 ("otp_algorithm", + tp.otp_algorithm), + GNUNET_JSON_pack_uint64 ("otp_ctr", + tp.otp_ctr)); + GNUNET_free (pos_confirmation); + GNUNET_free (tp.otp_description); + GNUNET_free (tp.otp_key); + return ret; + } +} + + +/* end of taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h b/src/backend/taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-otp-devices-DEVICE_ID.h + * @brief implement GET /otp-devices/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-otp-devices-DEVICE_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-otp-devices.c b/src/backend/taler-merchant-httpd_get-private-otp-devices.c @@ -0,0 +1,80 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-otp-devices.c + * @brief implement GET /otp-devices + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-otp-devices.h" + + +/** + * Add OTP device details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param otp_id ID of the OTP device + * @param otp_description human-readable description for the OTP device + */ +static void +add_otp (void *cls, + const char *otp_id, + const char *otp_description) +{ + json_t *pa = cls; + + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("otp_device_id", + otp_id), + GNUNET_JSON_pack_string ("device_description", + otp_description)))); +} + + +MHD_RESULT +TMH_private_get_otp_devices (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->lookup_otp_devices (TMH_db->cls, + hc->instance->settings.id, + &add_otp, + pa); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("otp_devices", + pa)); +} + + +/* end of taler-merchant-httpd_get-private-otp-devices.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-otp-devices.h b/src/backend/taler-merchant-httpd_get-private-otp-devices.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-otp-devices.h + * @brief implement GET /otp-devices + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/otp-devices" 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_otp_devices (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-otp-devices.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-pos.c b/src/backend/taler-merchant-httpd_get-private-pos.c @@ -0,0 +1,234 @@ +/* + This file is part of TALER + (C) 2019, 2020, 2021, 2024 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-httpd_get-private-pos.c + * @brief implement GET /private/pos + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-pos.h" +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_helper.h" + +/** + * Closure for add_product(). + */ +struct Context +{ + /** + * JSON array of products we are building. + */ + json_t *pa; + + /** + * JSON array of categories we are building. + */ + json_t *ca; + +}; + + +/** + * Add category to the @e ca array. + * + * @param cls a `struct Context` with JSON arrays to build + * @param category_id ID of the category + * @param category_name name of the category + * @param category_name_i18n translations of the @a category_name + * @param product_count number of products in the category + */ +static void +add_category ( + void *cls, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t product_count) +{ + struct Context *ctx = cls; + + (void) product_count; + GNUNET_assert ( + 0 == + json_array_append_new ( + ctx->ca, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("id", + category_id), + GNUNET_JSON_pack_object_incref ("name_i18n", + (json_t *) category_name_i18n), + GNUNET_JSON_pack_string ("name", + category_name)))); +} + + +/** + * Add product details to our JSON array. + * + * @param cls a `struct Context` with JSON arrays to build + * @param product_serial row ID of the product + * @param product_id ID of the product + * @param pd full product details + * @param num_categories length of @a categories array + * @param categories array of categories the + * product is in + */ +static void +add_product (void *cls, + uint64_t product_serial, + const char *product_id, + const struct TALER_MERCHANTDB_ProductDetails *pd, + size_t num_categories, + const uint64_t *categories) +{ + struct Context *ctx = cls; + json_t *pa = ctx->pa; + json_t *cata; + int64_t total_stock_api; + char unit_total_stock_buf[64]; + + cata = json_array (); + GNUNET_assert (NULL != cata); + for (size_t i = 0; i<num_categories; i++) + GNUNET_assert ( + 0 == json_array_append_new ( + cata, + json_integer (categories[i]))); + if (0 == num_categories) + { + // If there is no category, we return the default category + GNUNET_assert ( + 0 == json_array_append_new ( + cata, + json_integer (0))); + } + if (INT64_MAX == pd->total_stock) + total_stock_api = -1; + else + total_stock_api = (int64_t) pd->total_stock; + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_STOCK, + pd->total_stock, + pd->total_stock_frac, + sizeof (unit_total_stock_buf), + unit_total_stock_buf); + + GNUNET_assert ( + 0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_name", + pd->product_name), + GNUNET_JSON_pack_string ("description", + pd->description), + GNUNET_JSON_pack_object_incref ("description_i18n", + (json_t *) pd->description_i18n), + GNUNET_JSON_pack_string ("unit", + pd->unit), + // Note: deprecated field + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("price", + (0 == pd->price_array_length) + ? NULL + : &pd->price_array[0])), + TALER_JSON_pack_amount_array ("unit_price", + pd->price_array_length, + pd->price_array), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image", + pd->image)), + GNUNET_JSON_pack_array_steal ("categories", + cata), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("taxes", + (json_t *) pd->taxes)), + GNUNET_JSON_pack_int64 ("total_stock", + total_stock_api), + GNUNET_JSON_pack_string ("unit_total_stock", + unit_total_stock_buf), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + pd->allow_fractional_quantity), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + pd->fractional_precision_level), + GNUNET_JSON_pack_uint64 ("minimum_age", + pd->minimum_age), + GNUNET_JSON_pack_uint64 ("product_serial", + product_serial), + GNUNET_JSON_pack_string ("product_id", + product_id)))); +} + + +MHD_RESULT +TMH_private_get_pos (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct Context ctx; + enum GNUNET_DB_QueryStatus qs; + + ctx.pa = json_array (); + GNUNET_assert (NULL != ctx.pa); + ctx.ca = json_array (); + GNUNET_assert (NULL != ctx.ca); + GNUNET_assert ( + 0 == json_array_append_new ( + ctx.ca, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("id", + 0), + GNUNET_JSON_pack_string ("name", + "default")))); + qs = TMH_db->lookup_categories (TMH_db->cls, + hc->instance->settings.id, + &add_category, + &ctx); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ctx.pa); + json_decref (ctx.ca); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + qs = TMH_db->lookup_all_products (TMH_db->cls, + hc->instance->settings.id, + &add_product, + &ctx); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ctx.pa); + json_decref (ctx.ca); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("categories", + ctx.ca), + GNUNET_JSON_pack_array_steal ("products", + ctx.pa)); +} + + +/* end of taler-merchant-httpd_get-private-pos.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-pos.h b/src/backend/taler-merchant-httpd_get-private-pos.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_get-private-pos.h + * @brief implement GET /pos + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_POS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_POS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/pos" 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_pos (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-pos.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-pots-POT_ID.c b/src/backend/taler-merchant-httpd_get-private-pots-POT_ID.c @@ -0,0 +1,100 @@ +/* + 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 taler-merchant-httpd_get-private-pots-POT_ID.c + * @brief implementation of GET /private/pots/$POT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-pots-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; + size_t pot_total_len; + struct TALER_Amount *pot_totals; + 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_len, + &pot_totals); + + 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), + (0 == pot_total_len) + ? GNUNET_JSON_pack_array_steal ("pot_totals", + json_array ()) + : TALER_JSON_pack_amount_array ("pot_totals", + pot_total_len, + pot_totals)); + GNUNET_free (pot_totals); + GNUNET_free (pot_name); + GNUNET_free (description); + return ret; + } +} diff --git a/src/backend/taler-merchant-httpd_get-private-pots-POT_ID.h b/src/backend/taler-merchant-httpd_get-private-pots-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 taler-merchant-httpd_get-private-pots-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_get-private-pots.c b/src/backend/taler-merchant-httpd_get-private-pots.c @@ -0,0 +1,128 @@ +/* + 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 taler-merchant-httpd_get-private-pots.c + * @brief implementation of GET /private/pots + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-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_len length of the @a pot_totals array + * @param pot_totals current total amounts in the pot + */ +static void +add_pot (void *cls, + uint64_t money_pot_id, + const char *name, + size_t pot_total_len, + const struct TALER_Amount *pot_totals) +{ + 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), + (0 == pot_total_len) + ? GNUNET_JSON_pack_array_steal ("pot_totals", + json_array ()) + : TALER_JSON_pack_amount_array ("pot_totals", + pot_total_len, + pot_totals)); + 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_get-private-pots.h b/src/backend/taler-merchant-httpd_get-private-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 taler-merchant-httpd_get-private-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_get-private-products-PRODUCT_ID.c b/src/backend/taler-merchant-httpd_get-private-products-PRODUCT_ID.c @@ -0,0 +1,160 @@ +/* + This file is part of TALER + (C) 2019, 2020, 2021 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-httpd_get-private-products-PRODUCT_ID.c + * @brief implement GET /products/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-products-PRODUCT_ID.h" +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_util.h> + +/** + * Handle a GET "/products/$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_products_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; + enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; + json_t *jcategories; + + GNUNET_assert (NULL != mi); + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + hc->infix, + &pd, + &num_categories, + &categories); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product"); + } + if (0 == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + hc->infix); + } + jcategories = json_array (); + GNUNET_assert (NULL != jcategories); + for (size_t i = 0; i<num_categories; i++) + { + GNUNET_assert (0 == + json_array_append_new (jcategories, + json_integer (categories[i]))); + } + GNUNET_free (categories); + + { + MHD_RESULT ret; + int64_t total_stock_api; + char unit_total_stock_buf[64]; + + if (INT64_MAX == pd.total_stock) + total_stock_api = -1; + else + total_stock_api = (int64_t) pd.total_stock; + + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_STOCK, + pd.total_stock, + pd.total_stock_frac, + sizeof (unit_total_stock_buf), + unit_total_stock_buf); + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("product_name", + pd.product_name), + GNUNET_JSON_pack_string ("description", + pd.description), + GNUNET_JSON_pack_object_incref ("description_i18n", + pd.description_i18n), + GNUNET_JSON_pack_string ("unit", + pd.unit), + GNUNET_JSON_pack_array_steal ("categories", + jcategories), + // Note: deprecated field + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("price", + (0 == pd.price_array_length) + ? NULL + : &pd.price_array[0])), + TALER_JSON_pack_amount_array ("unit_price", + pd.price_array_length, + pd.price_array), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image", + pd.image)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("taxes", + pd.taxes)), + GNUNET_JSON_pack_int64 ("total_stock", + total_stock_api), + GNUNET_JSON_pack_string ("unit_total_stock", + unit_total_stock_buf), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + pd.allow_fractional_quantity), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + pd.fractional_precision_level), + TALER_JSON_pack_amount_array ("unit_price", + pd.price_array_length, + pd.price_array), + GNUNET_JSON_pack_uint64 ("total_sold", + pd.total_sold), + GNUNET_JSON_pack_uint64 ("total_lost", + pd.total_lost), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("address", + pd.address)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("next_restock", + (pd.next_restock))), + GNUNET_JSON_pack_uint64 ("product_group_id", + pd.product_group_id), + GNUNET_JSON_pack_uint64 ("money_pot_id", + pd.money_pot_id), + GNUNET_JSON_pack_bool ("price_is_net", + pd.price_is_net), + GNUNET_JSON_pack_uint64 ("minimum_age", + pd.minimum_age)); + TALER_MERCHANTDB_product_details_free (&pd); + return ret; + } +} + + +/* end of taler-merchant-httpd_get-private-products-PRODUCT_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-products-PRODUCT_ID.h b/src/backend/taler-merchant-httpd_get-private-products-PRODUCT_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_get-private-products-PRODUCT_ID.h + * @brief implement GET /products/$ID/ + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/products/$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_products_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-products-PRODUCT_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-products.c b/src/backend/taler-merchant-httpd_get-private-products.c @@ -0,0 +1,149 @@ +/* + This file is part of TALER + (C) 2019, 2020, 2021, 2024, 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-httpd_get-private-products.c + * @brief implement GET /products + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-products.h" + + +/** + * Add product details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param product_serial serial (row) number of the product in the database + * @param product_id ID of the product + */ +static void +add_product (void *cls, + uint64_t product_serial, + const char *product_id) +{ + json_t *pa = cls; + + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("product_serial", + product_serial), + GNUNET_JSON_pack_string ("product_id", + product_id)))); +} + + +/** + * Transforms an (untrusted) input filter into a Postgresql LIKE filter. + * Escapes "%" and "_" in the @a input and adds "%" at the beginning + * and the end to turn the @a input into a suitable Postgresql argument. + * + * @param input text to turn into a substring match expression, or NULL + * @return NULL if @a input was NULL, otherwise transformed @a input + */ +static char * +tr (const char *input) +{ + char *out; + size_t slen; + size_t wpos; + + if (NULL == input) + return NULL; + slen = strlen (input); + out = GNUNET_malloc (slen * 2 + 3); + wpos = 0; + out[wpos++] = '%'; + for (size_t i = 0; i<slen; i++) + { + char c = input[i]; + + if ( (c == '%') || + (c == '_') ) + out[wpos++] = '\\'; + out[wpos++] = c; + } + out[wpos++] = '%'; + GNUNET_assert (wpos < slen * 2 + 3); + return out; +} + + +MHD_RESULT +TMH_private_get_products (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + char *category_filter; + char *name_filter; + char *description_filter; + int64_t limit; + uint64_t offset; + + limit = 20; /* default */ + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit < 0) + offset = INT64_MAX; + else + offset = 0; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + category_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "category_filter")); + name_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "name_filter")); + description_filter = tr (MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "description_filter")); + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->lookup_products (TMH_db->cls, + hc->instance->settings.id, + offset, + limit, + category_filter, + name_filter, + description_filter, + &add_product, + pa); + GNUNET_free (category_filter); + GNUNET_free (name_filter); + GNUNET_free (description_filter); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("products", + pa)); +} + + +/* end of taler-merchant-httpd_get-private-products.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-products.h b/src/backend/taler-merchant-httpd_get-private-products.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2019, 2020 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-httpd_get-private-products.h + * @brief implement GET /products + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/products" 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_products (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-products.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.c b/src/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.c @@ -0,0 +1,135 @@ +/* + 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 taler-merchant-httpd_get-private-reports-REPORT_ID.c + * @brief implementation of GET /private/reports/$REPORT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-reports-REPORT_ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_get_report (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *report_id_str = hc->infix; + unsigned long long report_id; + char *report_program_section; + char *report_description; + char *mime_type; + char *data_source; + char *target_address; + struct GNUNET_TIME_Relative frequency; + struct GNUNET_TIME_Relative frequency_shift; + struct GNUNET_TIME_Absolute next_transmission; + enum TALER_ErrorCode last_error_code; + char *last_error_detail; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + + { + char dummy; + + if (1 != sscanf (report_id_str, + "%llu%c", + &report_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "report_id"); + } + } + + qs = TMH_db->select_report (TMH_db->cls, + hc->instance->settings.id, + (uint64_t) report_id, + &report_program_section, + &report_description, + &mime_type, + &data_source, + &target_address, + &frequency, + &frequency_shift, + &next_transmission, + &last_error_code, + &last_error_detail); + + 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_report"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, + report_id_str); + } + + { + json_t *response; + + response = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("report_serial", + report_id), + GNUNET_JSON_pack_string ("description", + report_description), + GNUNET_JSON_pack_string ("program_section", + report_program_section), + GNUNET_JSON_pack_string ("mime_type", + mime_type), + GNUNET_JSON_pack_string ("data_source", + data_source), + GNUNET_JSON_pack_string ("target_address", + target_address), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("last_error_detail", + last_error_detail)), + GNUNET_JSON_pack_time_rel ("report_frequency", + frequency), + GNUNET_JSON_pack_time_rel ("report_frequency_shift", + frequency_shift)); + GNUNET_free (report_program_section); + GNUNET_free (report_description); + GNUNET_free (mime_type); + GNUNET_free (data_source); + GNUNET_free (target_address); + GNUNET_free (last_error_detail); + if (TALER_EC_NONE != last_error_code) + { + GNUNET_assert (0 == + json_object_set_new (response, + "last_error_code", + json_integer (last_error_code))); + } + return TALER_MHD_reply_json_steal (connection, + response, + MHD_HTTP_OK); + } +} diff --git a/src/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.h b/src/backend/taler-merchant-httpd_get-private-reports-REPORT_ID.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 taler-merchant-httpd_get-private-reports-REPORT_ID.h + * @brief HTTP serving layer for getting report details + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORT_ID_H +#include "taler-merchant-httpd.h" + +/** + * Handle GET /private/reports/$REPORT_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_report (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-reports.c b/src/backend/taler-merchant-httpd_get-private-reports.c @@ -0,0 +1,119 @@ +/* + 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 taler-merchant-httpd_get-private-reports.c + * @brief implementation of GET /private/reports + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-reports.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> + + +/** + * Sensible bound on the limit. + */ +#define MAX_DELTA 1024 + + +/** + * Callback for listing reports. + * + * @param cls closure with a `json_t *` + * @param report_id unique identifier of the report + * @param report_description human-readable description + * @param frequency how often the report is generated + */ +static void +add_report (void *cls, + uint64_t report_id, + const char *report_description, + struct GNUNET_TIME_Relative frequency) +{ + json_t *reports = cls; + json_t *entry; + + entry = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("report_serial", + report_id), + GNUNET_JSON_pack_string ("description", + report_description), + GNUNET_JSON_pack_time_rel ("report_frequency", + frequency)); + GNUNET_assert (NULL != entry); + GNUNET_assert (0 == + json_array_append_new (reports, + entry)); +} + + +MHD_RESULT +TMH_private_get_reports (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + int64_t limit = -20; + uint64_t offset; + enum GNUNET_DB_QueryStatus qs; + json_t *reports; + + (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"); + } + + reports = json_array (); + GNUNET_assert (NULL != reports); + qs = TMH_db->select_reports (TMH_db->cls, + hc->instance->settings.id, + limit, + offset, + &add_report, + reports); + if (qs < 0) + { + json_decref (reports); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_reports"); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("reports", + reports)); +} diff --git a/src/backend/taler-merchant-httpd_get-private-reports.h b/src/backend/taler-merchant-httpd_get-private-reports.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 taler-merchant-httpd_get-private-reports.h + * @brief HTTP serving layer for listing reports + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORTS_H + +#include "taler-merchant-httpd.h" + +/** + * Handle GET /private/reports 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_reports (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-statistics-amount-SLUG.c b/src/backend/taler-merchant-httpd_get-private-statistics-amount-SLUG.c @@ -0,0 +1,254 @@ +/* + 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-httpd_get-private-statistics-amount-SLUG.c + * @brief implement GET /statistics-amount/$SLUG/ + * @author Martin Schanzenbach + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-statistics-amount-SLUG.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * Typically called by `lookup_statistics_amount_by_bucket`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param bucket_end end time of the bucket + * @param bucket_range range of the bucket + * @param amounts_len the length of @a cumulative_amounts + * @param amounts the cumulative amounts array + */ +static void +amount_by_bucket (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + struct GNUNET_TIME_Timestamp bucket_end, + const char *bucket_range, + unsigned int amounts_len, + const struct TALER_Amount amounts[static amounts_len]) +{ + json_t *root = cls; + json_t *amount_array; + json_t *buckets_array; + + GNUNET_assert (json_is_object (root)); + buckets_array = json_object_get (root, + "buckets"); + GNUNET_assert (NULL != buckets_array); + GNUNET_assert (json_is_array (buckets_array)); + + amount_array = json_array (); + GNUNET_assert (NULL != amount_array); + for (unsigned int i = 0; i < amounts_len; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new (amount_array, + TALER_JSON_from_amount (&amounts[i]))); + } + + GNUNET_assert ( + 0 == + json_array_append_new ( + buckets_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_timestamp ( + "end_time", + bucket_end), + GNUNET_JSON_pack_string ( + "range", + bucket_range), + GNUNET_JSON_pack_array_steal ( + "cumulative_amounts", + amount_array)))); + if (NULL == json_object_get (root, + "buckets_description")) + { + GNUNET_assert (0 == + json_object_set_new (root, + "buckets_description", + json_string (description))); + } +} + + +/** + * Typically called by `lookup_statistics_amount_by_interval`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param amounts_len the length of @a cumulative_amounts + * @param amounts the cumulative amounts array + */ +static void +amount_by_interval (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int amounts_len, + const struct TALER_Amount amounts[static amounts_len]) +{ + json_t *root; + json_t *amount_array; + json_t *intervals_array; + + root = cls; + GNUNET_assert (json_is_object (root)); + intervals_array = json_object_get (root, + "intervals"); + GNUNET_assert (NULL != intervals_array); + GNUNET_assert (json_is_array (intervals_array)); + + amount_array = json_array (); + GNUNET_assert (NULL != amount_array); + for (unsigned int i = 0; i < amounts_len; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new (amount_array, + TALER_JSON_from_amount (&amounts[i]))); + } + + + GNUNET_assert ( + 0 == + json_array_append_new ( + intervals_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_array_steal ( + "cumulative_amounts", + amount_array)))); + if (NULL == json_object_get (root, + "intervals_description")) + { + GNUNET_assert ( + 0 == + json_object_set_new (root, + "intervals_description", + json_string (description))); + } +} + + +/** + * Handle a GET "/statistics-amount/$SLUG" 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_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + json_t *root; + bool get_buckets = true; + bool get_intervals = true; + + GNUNET_assert (NULL != mi); + { + const char *filter; + + filter = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "by"); + if (NULL != filter) + { + if (0 == strcasecmp (filter, + "bucket")) + get_intervals = false; + else if (0 == strcasecmp (filter, + "interval")) + get_buckets = false; + else if (0 != strcasecmp (filter, + "any")) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "by"); + } + } + } + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("intervals", + json_array ()), + GNUNET_JSON_pack_array_steal ("buckets", + json_array ())); + if (get_buckets) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_amount_by_bucket ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &amount_by_bucket, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_amount_by_bucket"); + } + } + if (get_intervals) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_amount_by_interval ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &amount_by_interval, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_amount_by_interval"); + } + } + return TALER_MHD_reply_json (connection, + root, + MHD_HTTP_OK); +} + + +/* end of taler-merchant-httpd_get-private-statistics-amount-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-statistics-amount-SLUG.h b/src/backend/taler-merchant-httpd_get-private-statistics-amount-SLUG.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 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-httpd_get-private-statistics-amount-SLUG.h + * @brief implement GET /statistics-amount/$SLUG/ + * @author Martin Schanzenbach + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_AMOUNT_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_AMOUNT_SLUG_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/statistics-amount/$SLUG" 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_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-statistics-amount-SLUG.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-statistics-counter-SLUG.c b/src/backend/taler-merchant-httpd_get-private-statistics-counter-SLUG.c @@ -0,0 +1,227 @@ +/* + This file is part of TALER + (C) 2023, 2024, 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-httpd_get-private-statistics-counter-SLUG.c + * @brief implement GET /statistics-counter/$SLUG/ + * @author Martin Schanzenbach + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-statistics-counter-SLUG.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * Function returning integer-valued statistics. + * Typically called by `lookup_statistics_counter_by_bucket`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param bucket_start start time of the bucket + * @param bucket_end end time of the bucket + * @param bucket_range range of the bucket + * @param cumulative_number counter value + */ +static void +counter_by_bucket (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + struct GNUNET_TIME_Timestamp bucket_end, + const char *bucket_range, + uint64_t cumulative_number) +{ + json_t *root = cls; + json_t *buckets_array; + + GNUNET_assert (json_is_object (root)); + buckets_array = json_object_get (root, + "buckets"); + GNUNET_assert (NULL != buckets_array); + GNUNET_assert (json_is_array (buckets_array)); + GNUNET_assert ( + 0 == + json_array_append_new ( + buckets_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_timestamp ( + "end_time", + bucket_end), + GNUNET_JSON_pack_string ( + "range", + bucket_range), + GNUNET_JSON_pack_uint64 ( + "cumulative_counter", + cumulative_number)))); + if (NULL == json_object_get (root, + "buckets_description")) + { + GNUNET_assert ( + 0 == + json_object_set_new (root, + "buckets_description", + json_string (description))); + } +} + + +/** + * Function returning integer-valued statistics for a time interval. + * Called by `lookup_statistics_counter_by_interval`. + * + * @param cls a `json_t *` JSON array to build + * @param description description of the statistic + * @param bucket_start start time of the interval + * @param cumulative_number counter value + */ +static void +counter_by_interval (void *cls, + const char *description, + struct GNUNET_TIME_Timestamp bucket_start, + uint64_t cumulative_number) +{ + json_t *root = cls; + json_t *intervals_array; + + GNUNET_assert (json_is_object (root)); + intervals_array = json_object_get (root, + "intervals"); + GNUNET_assert (NULL != intervals_array); + GNUNET_assert (json_is_array (intervals_array)); + GNUNET_assert ( + 0 == + json_array_append_new ( + intervals_array, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ( + "start_time", + bucket_start), + GNUNET_JSON_pack_uint64 ( + "cumulative_counter", + cumulative_number)))); + if (NULL == json_object_get (root, + "intervals_description")) + { + GNUNET_assert ( + 0 == + json_object_set_new (root, + "intervals_description", + json_string (description))); + } +} + + +/** + * Handle a GET "/statistics-counter/$SLUG" 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_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + json_t *root; + bool get_buckets = true; + bool get_intervals = true; + + GNUNET_assert (NULL != mi); + { + const char *filter; + + filter = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "by"); + if (NULL != filter) + { + if (0 == strcasecmp (filter, + "bucket")) + get_intervals = false; + else if (0 == strcasecmp (filter, + "interval")) + get_buckets = false; + else if (0 != strcasecmp (filter, + "any")) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "by"); + } + } + } + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("intervals", + json_array ()), + GNUNET_JSON_pack_array_steal ("buckets", + json_array ())); + if (get_buckets) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_counter_by_bucket ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &counter_by_bucket, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_counter_by_bucket"); + } + } + if (get_intervals) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_statistics_counter_by_interval ( + TMH_db->cls, + mi->settings.id, + hc->infix, + &counter_by_interval, + root); + if (0 > qs) + { + GNUNET_break (0); + json_decref (root); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_counter_by_interval"); + } + } + return TALER_MHD_reply_json (connection, + root, + MHD_HTTP_OK); +} + + +/* end of taler-merchant-httpd_get-private-statistics-counter-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-statistics-counter-SLUG.h b/src/backend/taler-merchant-httpd_get-private-statistics-counter-SLUG.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 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-httpd_get-private-statistics-counter-SLUG.h + * @brief implement GET /statistics-counter/$SLUG/ + * @author Martin Schanzenbach + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_COUNTER_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_COUNTER_SLUG_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/statistics-counter/$SLUG" 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_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-statistics-counter-SLUG.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-statistics-report-transactions.c b/src/backend/taler-merchant-httpd_get-private-statistics-report-transactions.c @@ -0,0 +1,763 @@ +/* + 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-httpd_get-private-statistics-report-transactions.c + * @brief implement GET /statistics-report/transactions + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-statistics-report-transactions.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd_lib.h> + + +/** + * Closure for the detail_cb(). + */ +struct ResponseContext +{ + /** + * Format of the response we are to generate. + */ + enum + { + RCF_JSON, + RCF_PDF + } format; + + /** + * Stored in a DLL while suspended. + */ + struct ResponseContext *next; + + /** + * Stored in a DLL while suspended. + */ + struct ResponseContext *prev; + + /** + * Context for this request. + */ + struct TMH_HandlerContext *hc; + + /** + * Async context used to run Typst. + */ + struct TALER_MHD_TypstContext *tc; + + /** + * Response to return. + */ + struct MHD_Response *response; + + /** + * Time when we started processing the request. + */ + struct GNUNET_TIME_Timestamp now; + + /** + * Period of each bucket. + */ + struct GNUNET_TIME_Relative period; + + /** + * Granularity of the buckets. Matches @e period. + */ + const char *granularity; + + /** + * Number of buckets to return. + */ + uint64_t count; + + /** + * HTTP status to use with @e response. + */ + unsigned int http_status; + + /** + * Length of the @e labels array. + */ + unsigned int labels_cnt; + + /** + * Array of labels for the chart. + */ + char **labels; + + /** + * Data groups for the chart. + */ + json_t *data_groups; + + /** + * #GNUNET_YES if connection was suspended, + * #GNUNET_SYSERR if we were resumed on shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + +}; + + +/** + * DLL of requests awaiting Typst. + */ +static struct ResponseContext *rctx_head; + +/** + * DLL of requests awaiting Typst. + */ +static struct ResponseContext *rctx_tail; + + +void +TMH_handler_statistic_report_transactions_cleanup () +{ + struct ResponseContext *rctx; + + while (NULL != (rctx = rctx_head)) + { + GNUNET_CONTAINER_DLL_remove (rctx_head, + rctx_tail, + rctx); + rctx->suspended = GNUNET_SYSERR; + MHD_resume_connection (rctx->hc->connection); + } +} + + +/** + * Free resources from @a ctx + * + * @param[in] ctx the `struct ResponseContext` to clean up + */ +static void +free_rc (void *ctx) +{ + struct ResponseContext *rctx = ctx; + + if (NULL != rctx->tc) + { + TALER_MHD_typst_cancel (rctx->tc); + rctx->tc = NULL; + } + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + for (unsigned int i = 0; i<rctx->labels_cnt; i++) + GNUNET_free (rctx->labels[i]); + GNUNET_array_grow (rctx->labels, + rctx->labels_cnt, + 0); + json_decref (rctx->data_groups); + GNUNET_free (rctx); +} + + +/** + * Function called with the result of a #TALER_MHD_typst() operation. + * + * @param cls closure + * @param tr result of the operation + */ +static void +pdf_cb (void *cls, + const struct TALER_MHD_TypstResponse *tr) +{ + struct ResponseContext *rctx = cls; + + rctx->tc = NULL; + GNUNET_CONTAINER_DLL_remove (rctx_head, + rctx_tail, + rctx); + rctx->suspended = GNUNET_NO; + MHD_resume_connection (rctx->hc->connection); + TALER_MHD_daemon_trigger (); + if (TALER_EC_NONE != tr->ec) + { + rctx->http_status + = TALER_ErrorCode_get_http_status (tr->ec); + rctx->response + = TALER_MHD_make_error (tr->ec, + tr->details.hint); + return; + } + rctx->http_status + = MHD_HTTP_OK; + rctx->response + = TALER_MHD_response_from_pdf_file (tr->details.filename); +} + + +/** + * Typically called by `lookup_statistics_amount_by_bucket2`. + * + * @param[in,out] cls our `struct ResponseContext` to update + * @param bucket_start start time of the bucket + * @param amounts_len the length of @a amounts array + * @param amounts the cumulative amounts in the bucket + */ +static void +amount_by_bucket (void *cls, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int amounts_len, + const struct TALER_Amount amounts[static amounts_len]) +{ + struct ResponseContext *rctx = cls; + json_t *values; + + for (unsigned int i = 0; i<amounts_len; i++) + { + bool found = false; + + for (unsigned int j = 0; j<rctx->labels_cnt; j++) + { + if (0 == strcmp (amounts[i].currency, + rctx->labels[j])) + { + found = true; + break; + } + } + if (! found) + { + GNUNET_array_append (rctx->labels, + rctx->labels_cnt, + GNUNET_strdup (amounts[i].currency)); + } + } + + values = json_array (); + GNUNET_assert (NULL != values); + for (unsigned int i = 0; i<rctx->labels_cnt; i++) + { + const char *label = rctx->labels[i]; + double d = 0.0; + + for (unsigned int j = 0; j<amounts_len; j++) + { + const struct TALER_Amount *a = &amounts[j]; + + if (0 != strcmp (amounts[j].currency, + label)) + continue; + d = a->value * 1.0 + + (a->fraction * 1.0 / TALER_AMOUNT_FRAC_BASE); + break; + } /* for all amounts */ + GNUNET_assert (0 == + json_array_append_new (values, + json_real (d))); + } /* for all labels */ + + { + json_t *dg; + + dg = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + bucket_start), + GNUNET_JSON_pack_array_steal ("values", + values)); + GNUNET_assert (0 == + json_array_append_new (rctx->data_groups, + dg)); + + } +} + + +/** + * Create the transaction volume report. + * + * @param[in,out] rctx request context to use + * @param[in,out] charts JSON chart array to expand + * @return #GNUNET_OK on success, + * #GNUNET_NO to end with #MHD_YES, + * #GNUNET_NO to end with #MHD_NO. + */ +static enum GNUNET_GenericReturnValue +make_transaction_volume_report (struct ResponseContext *rctx, + json_t *charts) +{ + const char *bucket_name = "deposits-received"; + enum GNUNET_DB_QueryStatus qs; + json_t *chart; + json_t *labels; + + rctx->data_groups = json_array (); + GNUNET_assert (NULL != rctx->data_groups); + qs = TMH_db->lookup_statistics_amount_by_bucket2 ( + TMH_db->cls, + rctx->hc->instance->settings.id, + bucket_name, + rctx->granularity, + rctx->count, + &amount_by_bucket, + rctx); + if (0 > qs) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error ( + rctx->hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_amount_by_bucket2")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + json_decref (rctx->data_groups); + rctx->data_groups = NULL; + return GNUNET_OK; + } + + labels = json_array (); + GNUNET_assert (NULL != labels); + for (unsigned int i=0; i<rctx->labels_cnt; i++) + { + GNUNET_assert (0 == + json_array_append_new (labels, + json_string (rctx->labels[i]))); + GNUNET_free (rctx->labels[i]); + } + GNUNET_array_grow (rctx->labels, + rctx->labels_cnt, + 0); + chart = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("chart_name", + "Sales volume"), + GNUNET_JSON_pack_string ("y_label", + "Sales"), + GNUNET_JSON_pack_array_steal ("data_groups", + rctx->data_groups), + GNUNET_JSON_pack_array_steal ("labels", + labels), + GNUNET_JSON_pack_bool ("cumulative", + false)); + rctx->data_groups = NULL; + GNUNET_assert (0 == + json_array_append_new (charts, + chart)); + return GNUNET_OK; +} + + +/** + * Typically called by `lookup_statistics_counter_by_bucket2`. + * + * @param[in,out] cls our `struct ResponseContext` to update + * @param bucket_start start time of the bucket + * @param counters_len the length of @a cumulative_amounts + * @param descriptions description for the counter in the bucket + * @param counters the counters in the bucket + */ +static void +count_by_bucket (void *cls, + struct GNUNET_TIME_Timestamp bucket_start, + unsigned int counters_len, + const char *descriptions[static counters_len], + uint64_t counters[static counters_len]) +{ + struct ResponseContext *rctx = cls; + json_t *values; + + for (unsigned int i = 0; i<counters_len; i++) + { + bool found = false; + + for (unsigned int j = 0; j<rctx->labels_cnt; j++) + { + if (0 == strcmp (descriptions[i], + rctx->labels[j])) + { + found = true; + break; + } + } + if (! found) + { + GNUNET_array_append (rctx->labels, + rctx->labels_cnt, + GNUNET_strdup (descriptions[i])); + } + } + + values = json_array (); + GNUNET_assert (NULL != values); + for (unsigned int i = 0; i<rctx->labels_cnt; i++) + { + const char *label = rctx->labels[i]; + uint64_t v = 0; + + for (unsigned int j = 0; j<counters_len; j++) + { + if (0 != strcmp (descriptions[j], + label)) + continue; + v = counters[j]; + break; + } /* for all amounts */ + GNUNET_assert (0 == + json_array_append_new (values, + json_integer (v))); + } /* for all labels */ + + { + json_t *dg; + + dg = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("start_date", + bucket_start), + GNUNET_JSON_pack_array_steal ("values", + values)); + GNUNET_assert (0 == + json_array_append_new (rctx->data_groups, + dg)); + + } +} + + +/** + * Create the transaction count report. + * + * @param[in,out] rctx request context to use + * @param[in,out] charts JSON chart array to expand + * @return #GNUNET_OK on success, + * #GNUNET_NO to end with #MHD_YES, + * #GNUNET_NO to end with #MHD_NO. + */ +static enum GNUNET_GenericReturnValue +make_transaction_count_report (struct ResponseContext *rctx, + json_t *charts) +{ + const char *prefix = "orders-paid"; + enum GNUNET_DB_QueryStatus qs; + json_t *chart; + json_t *labels; + + rctx->data_groups = json_array (); + GNUNET_assert (NULL != rctx->data_groups); + qs = TMH_db->lookup_statistics_counter_by_bucket2 ( + TMH_db->cls, + rctx->hc->instance->settings.id, + prefix, /* prefix to match against bucket name */ + rctx->granularity, + rctx->count, + &count_by_bucket, + rctx); + if (0 > qs) + { + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error ( + rctx->hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_statistics_counter_by_bucket2")) + ? GNUNET_NO : GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + json_decref (rctx->data_groups); + rctx->data_groups = NULL; + return GNUNET_OK; + } + labels = json_array (); + GNUNET_assert (NULL != labels); + for (unsigned int i=0; i<rctx->labels_cnt; i++) + { + const char *label = rctx->labels[i]; + + /* This condition should always hold. */ + if (0 == + strncmp (prefix, + label, + strlen (prefix))) + label += strlen (prefix); + GNUNET_assert (0 == + json_array_append_new (labels, + json_string (label))); + GNUNET_free (rctx->labels[i]); + } + GNUNET_array_grow (rctx->labels, + rctx->labels_cnt, + 0); + chart = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("chart_name", + "Transaction counts"), + GNUNET_JSON_pack_string ("y_label", + "Number of transactions"), + GNUNET_JSON_pack_array_steal ("data_groups", + rctx->data_groups), + GNUNET_JSON_pack_array_steal ("labels", + labels), + GNUNET_JSON_pack_bool ("cumulative", + false)); + rctx->data_groups = NULL; + GNUNET_assert (0 == + json_array_append_new (charts, + chart)); + return GNUNET_OK; +} + + +/** + * Handle a GET "/private/statistics-report/transactions" 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_statistics_report_transactions ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct ResponseContext *rctx = hc->ctx; + struct TMH_MerchantInstance *mi = hc->instance; + json_t *charts; + + if (NULL != rctx) + { + GNUNET_assert (GNUNET_YES != rctx->suspended); + if (GNUNET_SYSERR == rctx->suspended) + return MHD_NO; + if (NULL == rctx->response) + { + GNUNET_break (0); + return MHD_NO; + } + return MHD_queue_response (connection, + rctx->http_status, + rctx->response); + } + rctx = GNUNET_new (struct ResponseContext); + rctx->hc = hc; + rctx->now = GNUNET_TIME_timestamp_get (); + hc->ctx = rctx; + hc->cc = &free_rc; + GNUNET_assert (NULL != mi); + + rctx->granularity = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "granularity"); + if (NULL == rctx->granularity) + { + rctx->granularity = "day"; + rctx->period = GNUNET_TIME_UNIT_DAYS; + rctx->count = 95; + } + else + { + const struct + { + const char *name; + struct GNUNET_TIME_Relative period; + uint64_t default_counter; + } map[] = { + { + .name = "second", + .period = GNUNET_TIME_UNIT_SECONDS, + .default_counter = 120, + }, + { + .name = "minute", + .period = GNUNET_TIME_UNIT_MINUTES, + .default_counter = 120, + }, + { + .name = "hour", + .period = GNUNET_TIME_UNIT_HOURS, + .default_counter = 48, + }, + { + .name = "day", + .period = GNUNET_TIME_UNIT_DAYS, + .default_counter = 95, + }, + { + .name = "month", + .period = GNUNET_TIME_UNIT_MONTHS, + .default_counter = 36, + }, + { + .name = "quarter", + .period = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MONTHS, + 3), + .default_counter = 40, + }, + { + .name = "year", + .period = GNUNET_TIME_UNIT_YEARS, + .default_counter = 10 + }, + { + .name = NULL + } + }; + + rctx->count = 0; + for (unsigned int i = 0; map[i].name != NULL; i++) + { + if (0 == strcasecmp (map[i].name, + rctx->granularity)) + { + rctx->count = map[i].default_counter; + rctx->period = map[i].period; + break; + } + } + if (0 == rctx->count) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "granularity"); + } + } /* end handling granularity */ + + /* Figure out desired output format */ + { + const char *mime; + + mime = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime) + mime = "application/json"; + if (0 == strcmp (mime, + "application/json")) + { + rctx->format = RCF_JSON; + } + else if (0 == strcmp (mime, + "application/pdf")) + { + + rctx->format = RCF_PDF; + } + else + { + GNUNET_break_op (0); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_NOT_ACCEPTABLE, + GNUNET_JSON_pack_string ("hint", + mime)); + } + } /* end of determine output format */ + + TALER_MHD_parse_request_number (connection, + "count", + &rctx->count); + + /* create charts */ + charts = json_array (); + GNUNET_assert (NULL != charts); + { + enum GNUNET_GenericReturnValue ret; + + ret = make_transaction_volume_report (rctx, + charts); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + ret = make_transaction_count_report (rctx, + charts); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + + /* generate response */ + { + struct GNUNET_TIME_Timestamp start_date; + struct GNUNET_TIME_Timestamp end_date; + json_t *root; + + end_date = rctx->now; + start_date + = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_subtract ( + end_date.abs_time, + GNUNET_TIME_relative_multiply (rctx->period, + rctx->count))); + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("business_name", + mi->settings.name), + GNUNET_JSON_pack_timestamp ("start_date", + start_date), + GNUNET_JSON_pack_timestamp ("end_date", + end_date), + GNUNET_JSON_pack_time_rel ("bucket_period", + rctx->period), + GNUNET_JSON_pack_array_steal ("charts", + charts)); + + switch (rctx->format) + { + case RCF_JSON: + return TALER_MHD_reply_json (connection, + root, + MHD_HTTP_OK); + case RCF_PDF: + { + struct TALER_MHD_TypstDocument doc = { + .form_name = "transactions", + .form_version = "0.0.0", + .data = root + }; + + rctx->tc = TALER_MHD_typst (TMH_cfg, + false, /* remove on exit */ + "merchant", + 1, /* one document, length of "array"! */ + &doc, + &pdf_cb, + rctx); + json_decref (root); + if (NULL == rctx->tc) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Client requested PDF, but Typst is unavailable\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, + NULL); + } + GNUNET_CONTAINER_DLL_insert (rctx_head, + rctx_tail, + rctx); + rctx->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + return MHD_YES; + } + } /* end switch */ + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_get-private-statistics-report-transactions.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-statistics-report-transactions.h b/src/backend/taler-merchant-httpd_get-private-statistics-report-transactions.h @@ -0,0 +1,49 @@ +/* + 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-httpd_get-private-statistics-report-transactions.h + * @brief implement GET /statistics-report/transactions + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_REPORT_TRANSACTIONS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_REPORT_TRANSACTIONS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/statistics-report/transactions" 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_statistics_report_transactions ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Cleanup ongoing report requests. + */ +void +TMH_handler_statistic_report_transactions_cleanup (void); + +/* end of taler-merchant-httpd_get-private-statistics-report-transactions.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_get-private-templates-TEMPLATE_ID.c @@ -0,0 +1,80 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-templates-TEMPLATE_ID.c + * @brief implement GET /templates/$ID + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_get_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + qs = TMH_db->lookup_template (TMH_db->cls, + mi->settings.id, + hc->infix, + &tp); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_template"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("editable_defaults", + tp.editable_defaults)), + GNUNET_JSON_pack_string ("template_description", + tp.template_description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + tp.otp_id)), + GNUNET_JSON_pack_object_incref ("template_contract", + tp.template_contract)); + TALER_MERCHANTDB_template_details_free (&tp); + return ret; + } +} + + +/* end of taler-merchant-httpd_get-private-templates-TEMPLATE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h b/src/backend/taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-templates-TEMPLATE_ID.h + * @brief implement GET /templates/$ID/ + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/templates/$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_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-templates-TEMPLATE_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-templates.c b/src/backend/taler-merchant-httpd_get-private-templates.c @@ -0,0 +1,79 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-templates.c + * @brief implement GET /templates + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-templates.h" + + +/** + * Add template details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param template_id ID of the template + * @param template_description human-readable description for the template + */ +static void +add_template (void *cls, + const char *template_id, + const char *template_description) +{ + json_t *pa = cls; + + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("template_id", template_id), + GNUNET_JSON_pack_string ("template_description", + template_description)))); +} + + +MHD_RESULT +TMH_private_get_templates (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->lookup_templates (TMH_db->cls, + hc->instance->settings.id, + &add_template, + pa); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_templates"); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("templates", + pa)); +} + + +/* end of taler-merchant-httpd_get-private-templates.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-templates.h b/src/backend/taler-merchant-httpd_get-private-templates.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-templates.h + * @brief implement GET /templates + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/templates" 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_templates (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-templates.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.c b/src/backend/taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.c @@ -0,0 +1,126 @@ +/* + This file is part of TALER + (C) 2023, 2024 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-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.c + * @brief implement GET /tokenfamilies/$SLUG/ + * @author Christian Blättler + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/tokenfamilies/$SLUG" 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_tokenfamilies_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 }; + enum GNUNET_DB_QueryStatus status; + + GNUNET_assert (NULL != mi); + status = TMH_db->lookup_token_family (TMH_db->cls, + mi->settings.id, + hc->infix, + &details); + if (0 > status) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_token_family"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == status) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TOKEN_FAMILY_UNKNOWN, + hc->infix); + } + { + char *kind = NULL; + MHD_RESULT result; + + if (TALER_MERCHANTDB_TFK_Subscription == details.kind) + { + kind = GNUNET_strdup ("subscription"); + } + else if (TALER_MERCHANTDB_TFK_Discount == details.kind) + { + kind = GNUNET_strdup ("discount"); + } + else + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "invalid_token_family_kind"); + } + + result = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("slug", + details.slug), + GNUNET_JSON_pack_string ("name", + details.name), + GNUNET_JSON_pack_string ("description", + details.description), + GNUNET_JSON_pack_object_steal ("description_i18n", + details.description_i18n), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("extra_data", + details.extra_data)), + GNUNET_JSON_pack_timestamp ("valid_after", + details.valid_after), + GNUNET_JSON_pack_timestamp ("valid_before", + details.valid_before), + GNUNET_JSON_pack_time_rel ("duration", + details.duration), + GNUNET_JSON_pack_time_rel ("validity_granularity", + details.validity_granularity), + GNUNET_JSON_pack_time_rel ("start_offset", + details.start_offset), + GNUNET_JSON_pack_string ("kind", + kind), + GNUNET_JSON_pack_int64 ("issued", + details.issued), + GNUNET_JSON_pack_int64 ("used", + details.used) + ); + + GNUNET_free (details.name); + GNUNET_free (details.description); + GNUNET_free (details.cipher_spec); + GNUNET_free (kind); + return result; + } +} + + +/* end of taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h b/src/backend/taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h + * @brief implement GET /tokenfamilies/$SLUG/ + * @author Christian Blättler + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_SLUG_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/tokenfamilies/$SLUG" 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_tokenfamilies_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-tokenfamilies-TOKEN_FAMILY_SLUG.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-tokenfamilies.c b/src/backend/taler-merchant-httpd_get-private-tokenfamilies.c @@ -0,0 +1,101 @@ +/* + This file is part of TALER + (C) 2023, 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-httpd_get-private-tokenfamilies.c + * @brief implement GET /tokenfamilies + * @author Christian Blättler + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-tokenfamilies.h" + + +/** + * Add token family details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param slug slug of the token family + * @param name name of the token family + * @param valid_after start time of the token family's validity period + * @param valid_before end time of the token family's validity period + * @param kind kind of the token family + */ +static void +add_token_family (void *cls, + const char *slug, + const char *name, + const char *description, + const json_t *description_i18n, + struct GNUNET_TIME_Timestamp valid_after, + struct GNUNET_TIME_Timestamp valid_before, + const char *kind) +{ + json_t *pa = cls; + + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("slug", + slug), + GNUNET_JSON_pack_string ("name", + name), + GNUNET_JSON_pack_string ("description", + description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + (json_t *) + description_i18n)), + GNUNET_JSON_pack_timestamp ("valid_after", + valid_after), + GNUNET_JSON_pack_timestamp ("valid_before", + valid_before), + GNUNET_JSON_pack_string ("kind", + kind)))); +} + + +MHD_RESULT +TMH_private_get_tokenfamilies (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *families; + enum GNUNET_DB_QueryStatus qs; + + families = json_array (); + GNUNET_assert (NULL != families); + qs = TMH_db->lookup_token_families (TMH_db->cls, + hc->instance->settings.id, + &add_token_family, + families); + if (0 > qs) + { + GNUNET_break (0); + json_decref (families); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ( + "token_families", + families)); +} + + +/* end of taler-merchant-httpd_get-private-tokenfamilies.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-tokenfamilies.h b/src/backend/taler-merchant-httpd_get-private-tokenfamilies.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_get-private-tokenfamilies.h + * @brief implement GET /tokenfamilies + * @author Christian Blättler + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/tokenfamilies" 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_tokenfamilies (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-tokenfamilies.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-tokens.c b/src/backend/taler-merchant-httpd_get-private-tokens.c @@ -0,0 +1,118 @@ +/* + 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-httpd_get-private-tokens.c + * @brief implement GET /tokens + * @author Martin Schanzenbach + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_auth.h" +#include "taler-merchant-httpd_get-private-tokens.h" + + +/** + * Add token details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param creation_time when the token was created + * @param expiration_time when the token will expire + * @param scope internal scope identifier for the token (mapped to string) + * @param description human-readable purpose or context of the token + * @param serial serial (row) number of the product in the database + */ +static void +add_token (void *cls, + struct GNUNET_TIME_Timestamp creation_time, + struct GNUNET_TIME_Timestamp expiration_time, + uint32_t scope, + const char *description, + uint64_t serial) +{ + json_t *pa = cls; + bool refreshable; + const char *as; + + as = TMH_get_name_by_scope (scope, + &refreshable); + if (NULL == as) + { + GNUNET_break (0); + return; + } + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("creation_time", + creation_time), + GNUNET_JSON_pack_timestamp ("expiration", + expiration_time), + GNUNET_JSON_pack_string ("scope", + as), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_string ("description", + description), + GNUNET_JSON_pack_uint64 ("serial", + serial)))); +} + + +MHD_RESULT +TMH_private_get_instances_ID_tokens (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *ta; + enum GNUNET_DB_QueryStatus qs; + int64_t limit = -20; /* default */ + uint64_t offset; + + 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); + ta = json_array (); + GNUNET_assert (NULL != ta); + qs = TMH_db->lookup_login_tokens (TMH_db->cls, + hc->instance->settings.id, + offset, + limit, + &add_token, + ta); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ta); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("tokens", + ta)); +} + + +/* end of taler-merchant-httpd_get-private-tokens.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-tokens.h b/src/backend/taler-merchant-httpd_get-private-tokens.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 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-httpd_get-private-tokens.h + * @brief implement GET /tokens + * @author Martin Schanzenbach + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_TOKENS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_TOKENS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/tokens" 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_instances_ID_tokens (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-tokens.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-transfers.c b/src/backend/taler-merchant-httpd_get-private-transfers.c @@ -0,0 +1,189 @@ +/* + This file is part of TALER + (C) 2014-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-httpd_get-private-transfers.c + * @brief implement API for obtaining a list of wire transfers + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_get-private-transfers.h" + + +/** + * Function called with information about a wire transfer. + * Generate a response (array entry) based on the given arguments. + * + * @param cls closure with a `json_t *` array to build up the response + * @param credit_amount amount expected to be wired to the merchant (minus fees), NULL if unknown + * @param wtid wire transfer identifier + * @param payto_uri target account that received the wire transfer + * @param exchange_url base URL of the exchange that made the wire transfer + * @param transfer_serial_id serial number identifying the transfer in the backend + * @param expected_transfer_serial_id serial number identifying the expected transfer in the backend, 0 if not @a expected + * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS + * if it did not yet happen + * @param expected true if the merchant acknowledged the wire transfer reception + */ +static void +transfer_cb (void *cls, + const struct TALER_Amount *credit_amount, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct TALER_FullPayto payto_uri, + const char *exchange_url, + uint64_t transfer_serial_id, + uint64_t expected_transfer_serial_id, + struct GNUNET_TIME_Absolute execution_time, + bool expected) +{ + json_t *ja = cls; + json_t *r; + + r = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("credit_amount", + credit_amount), + GNUNET_JSON_pack_data_auto ("wtid", + wtid), + TALER_JSON_pack_full_payto ("payto_uri", + payto_uri), + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + GNUNET_JSON_pack_uint64 ("transfer_serial_id", + transfer_serial_id), + (0 == expected_transfer_serial_id) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("dummy", + NULL)) + : GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", + expected_transfer_serial_id), + // FIXME: protocol breaking to remove... + GNUNET_JSON_pack_bool ("verified", + false), + // FIXME: protocol breaking to remove... + GNUNET_JSON_pack_bool ("confirmed", + true), + GNUNET_JSON_pack_bool ("expected", + expected), + GNUNET_TIME_absolute_is_zero (execution_time) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("dummy", + NULL)) + : GNUNET_JSON_pack_timestamp ( + "execution_time", + GNUNET_TIME_absolute_to_timestamp (execution_time))); + GNUNET_assert (0 == + json_array_append_new (ja, + r)); +} + + +/** + * Manages a GET /private/transfers call. + * + * @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_transfers (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_FullPayto payto_uri = { + .full_payto = NULL + }; + struct GNUNET_TIME_Timestamp before = GNUNET_TIME_UNIT_FOREVER_TS; + struct GNUNET_TIME_Timestamp after = GNUNET_TIME_UNIT_ZERO_TS; + int64_t limit = -20; + uint64_t offset; + enum TALER_EXCHANGE_YesNoAll expected; + + (void) rh; + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit < 0) + offset = INT64_MAX; + else + offset = 0; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + TALER_MHD_parse_request_yna (connection, + "expected", + TALER_EXCHANGE_YNA_ALL, + &expected); + TALER_MHD_parse_request_timestamp (connection, + "before", + &before); + TALER_MHD_parse_request_timestamp (connection, + "after", + &after); + { + const char *esc_payto; + + esc_payto = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "payto_uri"); + if (NULL != esc_payto) + { + payto_uri.full_payto + = GNUNET_strdup (esc_payto); + (void) MHD_http_unescape (payto_uri.full_payto); + } + } + TMH_db->preflight (TMH_db->cls); + { + json_t *ja; + enum GNUNET_DB_QueryStatus qs; + + ja = json_array (); + GNUNET_assert (NULL != ja); + qs = TMH_db->lookup_transfers (TMH_db->cls, + hc->instance->settings.id, + payto_uri, + before, + after, + limit, + offset, + expected, + &transfer_cb, + ja); + GNUNET_free (payto_uri.full_payto); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "transfers"); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("transfers", + ja)); + } +} + + +/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-transfers.h b/src/backend/taler-merchant-httpd_get-private-transfers.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2014-2020 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/> +*/ +/** + * @file taler-merchant-httpd_get-private-transfers.h + * @brief headers for GET /transfers handler + * @author Christian Grothoff + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TRANSFERS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TRANSFERS_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Manages a GET /private/transfers call. + * + * @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_transfers (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-units-UNIT.c b/src/backend/taler-merchant-httpd_get-private-units-UNIT.c @@ -0,0 +1,89 @@ +/* + 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-httpd_get-private-units-UNIT.c + * @brief implement GET /private/units/$UNIT + * @author Bohdan Potuzhnyi + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-units-UNIT.h" + + +MHD_RESULT +TMH_private_get_units_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_MERCHANTDB_UnitDetails ud = { 0 }; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; + + (void) rh; + GNUNET_assert (NULL != hc->infix); + qs = TMH_db->select_unit (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &ud); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "unit"); + } + + ret = TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("unit_serial", + ud.unit_serial), + GNUNET_JSON_pack_string ("unit", + ud.unit), + GNUNET_JSON_pack_string ("unit_name_long", + ud.unit_name_long), + GNUNET_JSON_pack_object_incref ( + "unit_name_long_i18n", + ud.unit_name_long_i18n), + GNUNET_JSON_pack_string ("unit_name_short", + ud.unit_name_short), + GNUNET_JSON_pack_object_incref ( + "unit_name_short_i18n", + ud.unit_name_short_i18n), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + ud.unit_allow_fraction + ), + GNUNET_JSON_pack_uint64 ( + "unit_precision_level", + ud.unit_precision_level), + GNUNET_JSON_pack_bool ("unit_active", + ud.unit_active), + GNUNET_JSON_pack_bool ("unit_builtin", + ud.unit_builtin)); + TALER_MERCHANTDB_unit_details_free (&ud); + return ret; +} + + +/* end of taler-merchant-httpd_get-private-units-UNIT.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-units-UNIT.h b/src/backend/taler-merchant-httpd_get-private-units-UNIT.h @@ -0,0 +1,33 @@ +/* + 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-httpd_get-private-units-UNIT.h + * @brief implement GET /private/units/$UNIT + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_ID_H + +#include "taler-merchant-httpd.h" + + +MHD_RESULT +TMH_private_get_units_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-units-UNIT.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-units.c b/src/backend/taler-merchant-httpd_get-private-units.c @@ -0,0 +1,91 @@ +/* + 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-httpd_get-private-units.c + * @brief implement GET /private/units + * @author Bohdan Potuzhnyi + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-units.h" + + +static void +add_unit (void *cls, + uint64_t unit_serial, + const struct TALER_MERCHANTDB_UnitDetails *ud) +{ + json_t *ua = cls; + + GNUNET_assert ( + 0 == + json_array_append_new ( + ua, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("unit_serial", + unit_serial), + GNUNET_JSON_pack_string ("unit", + ud->unit), + GNUNET_JSON_pack_string ("unit_name_long", + ud->unit_name_long), + GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", + (json_t *) ud->unit_name_long_i18n), + GNUNET_JSON_pack_string ("unit_name_short", + ud->unit_name_short), + GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", + (json_t *) ud->unit_name_short_i18n), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + ud->unit_allow_fraction), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + ud->unit_precision_level), + GNUNET_JSON_pack_bool ("unit_active", + ud->unit_active), + GNUNET_JSON_pack_bool ("unit_builtin", + ud->unit_builtin)))); +} + + +MHD_RESULT +TMH_private_get_units (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *ua; + enum GNUNET_DB_QueryStatus qs; + + (void) rh; + ua = json_array (); + GNUNET_assert (NULL != ua); + qs = TMH_db->lookup_units (TMH_db->cls, + hc->instance->settings.id, + &add_unit, + ua); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ua); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("units", + ua)); +} + + +/* end of taler-merchant-httpd_get-private-units.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-units.h b/src/backend/taler-merchant-httpd_get-private-units.h @@ -0,0 +1,33 @@ +/* + 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-httpd_get-private-units.h + * @brief implement GET /private/units + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_H + +#include "taler-merchant-httpd.h" + + +MHD_RESULT +TMH_private_get_units (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-units.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.c b/src/backend/taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.c @@ -0,0 +1,92 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-webhooks-WEBHOOK_ID.c + * @brief implement GET /webhooks/$ID + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h" +#include <taler/taler_json_lib.h> + + +/** + * Handle a GET "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_WebhookDetails wb = { 0 }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + qs = TMH_db->lookup_webhook (TMH_db->cls, + mi->settings.id, + hc->infix, + &wb); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_webhook"); + } + if (0 == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_WEBHOOK_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("event_type", + wb.event_type), + GNUNET_JSON_pack_string ("url", + wb.url), + GNUNET_JSON_pack_string ("http_method", + wb.http_method), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("header_template", + wb.header_template)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("body_template", + wb.body_template))); + GNUNET_free (wb.event_type); + GNUNET_free (wb.url); + GNUNET_free (wb.http_method); + GNUNET_free (wb.header_template); + GNUNET_free (wb.body_template); + + return ret; + } +} + + +/* end of taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h b/src/backend/taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-webhooks-WEBHOOK_ID.h + * @brief implement GET /webhooks/$ID/ + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-webhooks-WEBHOOK_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-private-webhooks.c b/src/backend/taler-merchant-httpd_get-private-webhooks.c @@ -0,0 +1,80 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-webhooks.c + * @brief implement GET /webhooks + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-private-webhooks.h" + + +/** + * Add webhook details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param webhook_id ID of the webhook + * @param event_type what type of event is the hook for + */ +static void +add_webhook (void *cls, + const char *webhook_id, + const char *event_type) +{ + json_t *pa = cls; + + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("webhook_id", + webhook_id), + GNUNET_JSON_pack_string ("event_type", + event_type)))); +} + + +MHD_RESULT +TMH_private_get_webhooks (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *pa; + enum GNUNET_DB_QueryStatus qs; + + pa = json_array (); + GNUNET_assert (NULL != pa); + qs = TMH_db->lookup_webhooks (TMH_db->cls, + hc->instance->settings.id, + &add_webhook, + pa); + if (0 > qs) + { + GNUNET_break (0); + json_decref (pa); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("webhooks", + pa)); +} + + +/* end of taler-merchant-httpd_get-private-webhooks.c */ diff --git a/src/backend/taler-merchant-httpd_get-private-webhooks.h b/src/backend/taler-merchant-httpd_get-private-webhooks.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_get-private-webhooks.h + * @brief implement GET /webhooks + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/webhooks" 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_webhooks (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-private-webhooks.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-products-HASH-image.c b/src/backend/taler-merchant-httpd_get-products-HASH-image.c @@ -1,87 +0,0 @@ -/* - 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 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 taler-merchant-httpd_get-products-image.c - * @brief implement GET /products/$HASH/image - * @author Bohdan Potuzhnyi - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_get-products-HASH-image.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_error_codes.h> - - -MHD_RESULT -TMH_get_products_image (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *image_hash = hc->infix; - char *image = NULL; - enum GNUNET_DB_QueryStatus qs; - (void) rh; - - { - /* Just simple check if the string is what we really expect */ - size_t ih_len = strlen (image_hash); - - if ( (sizeof (struct GNUNET_ShortHashCode) * 2) != ih_len) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image_hash"); - - for (size_t i = 0; i < ih_len; i++) - if (! isxdigit ((unsigned char) image_hash[i])) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image_hash"); - } - - qs = TMH_db->lookup_product_image_by_hash (TMH_db->cls, - mi->settings.id, - image_hash, - &image); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product_image_by_hash"); - } - if ( (0 == qs) || - (NULL == image) ) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - image_hash); - } - - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("image", - image)); - GNUNET_free (image); - return ret; - } -} diff --git a/src/backend/taler-merchant-httpd_get-products-HASH-image.h b/src/backend/taler-merchant-httpd_get-products-HASH-image.h @@ -1,31 +0,0 @@ -/* - 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 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 taler-merchant-httpd_get-products-image.h - * @brief implement GET /products/$HASH/image - * @author Bohdan Potuzhnyi - */ -#ifndef TALER_MERCHANT_HTTPD_GET_PRODUCTS_HASH_IMAGE_H -#define TALER_MERCHANT_HTTPD_GET_PRODUCTS_HASH_IMAGE_H - -#include "taler-merchant-httpd.h" - -MHD_RESULT -TMH_get_products_image (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif -\ No newline at end of file diff --git a/src/backend/taler-merchant-httpd_get-products-IMAGE_HASH-image.c b/src/backend/taler-merchant-httpd_get-products-IMAGE_HASH-image.c @@ -0,0 +1,87 @@ +/* + 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 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 taler-merchant-httpd_get-products-IMAGE_HASH-image.c + * @brief implement GET /products/$HASH/image + * @author Bohdan Potuzhnyi + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-products-IMAGE_HASH-image.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_error_codes.h> + + +MHD_RESULT +TMH_get_products_image (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *image_hash = hc->infix; + char *image = NULL; + enum GNUNET_DB_QueryStatus qs; + (void) rh; + + { + /* Just simple check if the string is what we really expect */ + size_t ih_len = strlen (image_hash); + + if ( (sizeof (struct GNUNET_ShortHashCode) * 2) != ih_len) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image_hash"); + + for (size_t i = 0; i < ih_len; i++) + if (! isxdigit ((unsigned char) image_hash[i])) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image_hash"); + } + + qs = TMH_db->lookup_product_image_by_hash (TMH_db->cls, + mi->settings.id, + image_hash, + &image); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product_image_by_hash"); + } + if ( (0 == qs) || + (NULL == image) ) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + image_hash); + } + + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("image", + image)); + GNUNET_free (image); + return ret; + } +} diff --git a/src/backend/taler-merchant-httpd_get-products-IMAGE_HASH-image.h b/src/backend/taler-merchant-httpd_get-products-IMAGE_HASH-image.h @@ -0,0 +1,31 @@ +/* + 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 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 taler-merchant-httpd_get-products-IMAGE_HASH-image.h + * @brief implement GET /products/$HASH/image + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_GET_PRODUCTS_HASH_IMAGE_H +#define TALER_MERCHANT_HTTPD_GET_PRODUCTS_HASH_IMAGE_H + +#include "taler-merchant-httpd.h" + +MHD_RESULT +TMH_get_products_image (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif +\ No newline at end of file diff --git a/src/backend/taler-merchant-httpd_get-sessions-ID.c b/src/backend/taler-merchant-httpd_get-sessions-ID.c @@ -1,312 +0,0 @@ -/* - 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-httpd_private-get-sessions-ID.c - * @brief implement GET /session/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_get-sessions-ID.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> - - -/** - * Context for a get sessions request. - */ -struct GetSessionContext -{ - /** - * Kept in a DLL. - */ - struct GetSessionContext *next; - - /** - * Kept in a DLL. - */ - struct GetSessionContext *prev; - - /** - * Request context. - */ - struct TMH_HandlerContext *hc; - - /** - * Entry in the #resume_timeout_heap for this check payment, if we are - * suspended. - */ - struct TMH_SuspendedConnection sc; - - /** - * Fulfillment URL from the HTTP request. - */ - const char *fulfillment_url; - - /** - * Database event we are waiting on to be resuming on payment. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Did we suspend @a connection and are thus in - * the #gsc_head DLL (#GNUNET_YES). Set to - * #GNUNET_NO if we are not suspended, and to - * #GNUNET_SYSERR if we should close the connection - * without a response due to shutdown. - */ - enum GNUNET_GenericReturnValue suspended; -}; - - -/** - * Kept in a DLL. - */ -static struct GetSessionContext *gsc_head; - -/** - * Kept in a DLL. - */ -static struct GetSessionContext *gsc_tail; - - -void -TMH_force_get_sessions_ID_resume (void) -{ - struct GetSessionContext *gsc; - - while (NULL != (gsc = gsc_head)) - { - GNUNET_CONTAINER_DLL_remove (gsc_head, - gsc_tail, - gsc); - gsc->suspended = GNUNET_SYSERR; - MHD_resume_connection (gsc->sc.con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - } -} - - -/** - * Cleanup helper for TMH_get_session_ID(). - * - * @param cls must be a `struct GetSessionContext` - */ -static void -gsc_cleanup (void *cls) -{ - struct GetSessionContext *gsc = cls; - - if (NULL != gsc->eh) - { - TMH_db->event_listen_cancel (gsc->eh); - gsc->eh = NULL; - } - GNUNET_free (gsc); -} - - -/** - * We have received a trigger from the database - * that we should (possibly) resume the request. - * - * @param cls a `struct GetOrderData` to resume - * @param extra string encoding refund amount (or NULL) - * @param extra_size number of bytes in @a extra - */ -static void -resume_by_event (void *cls, - const void *extra, - size_t extra_size) -{ - struct GetSessionContext *gsc = cls; - - if (GNUNET_YES != gsc->suspended) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Not suspended, ignoring event\n"); - return; /* duplicate event is possible */ - } - gsc->suspended = GNUNET_NO; - GNUNET_CONTAINER_DLL_remove (gsc_head, - gsc_tail, - gsc); - MHD_resume_connection (gsc->sc.con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -MHD_RESULT -TMH_get_sessions_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct GetSessionContext *gsc = hc->ctx; - struct TMH_MerchantInstance *mi = hc->instance; - char *order_id = NULL; - bool paid = false; - bool is_past; - - GNUNET_assert (NULL != mi); - if (NULL == gsc) - { - gsc = GNUNET_new (struct GetSessionContext); - gsc->hc = hc; - hc->ctx = gsc; - hc->cc = &gsc_cleanup; - gsc->sc.con = connection; - gsc->fulfillment_url = MHD_lookup_connection_value ( - connection, - MHD_GET_ARGUMENT_KIND, - "fulfillment_url"); - if (NULL == gsc->fulfillment_url) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "fulfillment_url"); - } - if (! TALER_is_web_url (gsc->fulfillment_url)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "fulfillment_url"); - } - - TALER_MHD_parse_request_timeout (connection, - &gsc->sc.long_poll_timeout); - - if (! GNUNET_TIME_absolute_is_past (gsc->sc.long_poll_timeout) ) - { - struct TMH_SessionEventP session_eh = { - .header.size = htons (sizeof (session_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), - .merchant_pub = gsc->hc->instance->merchant_pub - }; - - GNUNET_CRYPTO_hash (hc->infix, - strlen (hc->infix), - &session_eh.h_session_id); - GNUNET_CRYPTO_hash (gsc->fulfillment_url, - strlen (gsc->fulfillment_url), - &session_eh.h_fulfillment_url); - gsc->eh - = TMH_db->event_listen ( - TMH_db->cls, - &session_eh.header, - GNUNET_TIME_absolute_get_remaining (gsc->sc.long_poll_timeout), - &resume_by_event, - gsc); - } - } /* end first-time initialization (NULL == gsc) */ - - if (GNUNET_SYSERR == gsc->suspended) - return MHD_NO; /* close connection on service shutdown */ - - is_past = GNUNET_TIME_absolute_is_past (gsc->sc.long_poll_timeout); - /* figure out order_id */ - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_order_by_fulfillment (TMH_db->cls, - mi->settings.id, - gsc->fulfillment_url, - hc->infix, - false, - &order_id); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order_by_fulfillment"); - } - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) && - is_past) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_SESSION_UNKNOWN, - hc->infix); - } - } - - /* Check if paid */ - if (NULL != order_id) - { - enum GNUNET_DB_QueryStatus qs; - struct TALER_PrivateContractHashP h_contract_terms; - - qs = TMH_db->lookup_order_status (TMH_db->cls, - mi->settings.id, - order_id, - &h_contract_terms, - &paid); - if (0 >= qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order_status"); - } - } - - if (paid) - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("order_id", - order_id)); - GNUNET_free (order_id); - return ret; - } - - if (is_past) - { - MHD_RESULT ret; - - GNUNET_assert (NULL != order_id); - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("order_id", - order_id)); - GNUNET_free (order_id); - return ret; - } - - GNUNET_free (order_id); - GNUNET_CONTAINER_DLL_insert (gsc_head, - gsc_tail, - gsc); - gsc->suspended = GNUNET_YES; - MHD_suspend_connection (gsc->sc.con); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_get-templates-ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-sessions-ID.h b/src/backend/taler-merchant-httpd_get-sessions-ID.h @@ -1,48 +0,0 @@ -/* - 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-httpd_get-sessions-ID.h - * @brief implementation of GET /orders/$ID - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_GET_SESSIONS_ID_H -#define TALER_MERCHANT_HTTPD_GET_SESSIONS_ID_H - -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - -/** - * Force resuming all suspended session lookups, needed during shutdown. - */ -void -TMH_force_get_sessions_ID_resume (void); - - -/** - * Handle a GET "/sessions/$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_get_sessions_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_get-sessions-SESSION_ID.c b/src/backend/taler-merchant-httpd_get-sessions-SESSION_ID.c @@ -0,0 +1,312 @@ +/* + 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-httpd_get-sessions-SESSION_ID.c + * @brief implement GET /session/$ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-sessions-SESSION_ID.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> + + +/** + * Context for a get sessions request. + */ +struct GetSessionContext +{ + /** + * Kept in a DLL. + */ + struct GetSessionContext *next; + + /** + * Kept in a DLL. + */ + struct GetSessionContext *prev; + + /** + * Request context. + */ + struct TMH_HandlerContext *hc; + + /** + * Entry in the #resume_timeout_heap for this check payment, if we are + * suspended. + */ + struct TMH_SuspendedConnection sc; + + /** + * Fulfillment URL from the HTTP request. + */ + const char *fulfillment_url; + + /** + * Database event we are waiting on to be resuming on payment. + */ + struct GNUNET_DB_EventHandler *eh; + + /** + * Did we suspend @a connection and are thus in + * the #gsc_head DLL (#GNUNET_YES). Set to + * #GNUNET_NO if we are not suspended, and to + * #GNUNET_SYSERR if we should close the connection + * without a response due to shutdown. + */ + enum GNUNET_GenericReturnValue suspended; +}; + + +/** + * Kept in a DLL. + */ +static struct GetSessionContext *gsc_head; + +/** + * Kept in a DLL. + */ +static struct GetSessionContext *gsc_tail; + + +void +TMH_force_get_sessions_ID_resume (void) +{ + struct GetSessionContext *gsc; + + while (NULL != (gsc = gsc_head)) + { + GNUNET_CONTAINER_DLL_remove (gsc_head, + gsc_tail, + gsc); + gsc->suspended = GNUNET_SYSERR; + MHD_resume_connection (gsc->sc.con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + } +} + + +/** + * Cleanup helper for TMH_get_session_ID(). + * + * @param cls must be a `struct GetSessionContext` + */ +static void +gsc_cleanup (void *cls) +{ + struct GetSessionContext *gsc = cls; + + if (NULL != gsc->eh) + { + TMH_db->event_listen_cancel (gsc->eh); + gsc->eh = NULL; + } + GNUNET_free (gsc); +} + + +/** + * We have received a trigger from the database + * that we should (possibly) resume the request. + * + * @param cls a `struct GetOrderData` to resume + * @param extra string encoding refund amount (or NULL) + * @param extra_size number of bytes in @a extra + */ +static void +resume_by_event (void *cls, + const void *extra, + size_t extra_size) +{ + struct GetSessionContext *gsc = cls; + + if (GNUNET_YES != gsc->suspended) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Not suspended, ignoring event\n"); + return; /* duplicate event is possible */ + } + gsc->suspended = GNUNET_NO; + GNUNET_CONTAINER_DLL_remove (gsc_head, + gsc_tail, + gsc); + MHD_resume_connection (gsc->sc.con); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +MHD_RESULT +TMH_get_sessions_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct GetSessionContext *gsc = hc->ctx; + struct TMH_MerchantInstance *mi = hc->instance; + char *order_id = NULL; + bool paid = false; + bool is_past; + + GNUNET_assert (NULL != mi); + if (NULL == gsc) + { + gsc = GNUNET_new (struct GetSessionContext); + gsc->hc = hc; + hc->ctx = gsc; + hc->cc = &gsc_cleanup; + gsc->sc.con = connection; + gsc->fulfillment_url = MHD_lookup_connection_value ( + connection, + MHD_GET_ARGUMENT_KIND, + "fulfillment_url"); + if (NULL == gsc->fulfillment_url) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "fulfillment_url"); + } + if (! TALER_is_web_url (gsc->fulfillment_url)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "fulfillment_url"); + } + + TALER_MHD_parse_request_timeout (connection, + &gsc->sc.long_poll_timeout); + + if (! GNUNET_TIME_absolute_is_past (gsc->sc.long_poll_timeout) ) + { + struct TMH_SessionEventP session_eh = { + .header.size = htons (sizeof (session_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), + .merchant_pub = gsc->hc->instance->merchant_pub + }; + + GNUNET_CRYPTO_hash (hc->infix, + strlen (hc->infix), + &session_eh.h_session_id); + GNUNET_CRYPTO_hash (gsc->fulfillment_url, + strlen (gsc->fulfillment_url), + &session_eh.h_fulfillment_url); + gsc->eh + = TMH_db->event_listen ( + TMH_db->cls, + &session_eh.header, + GNUNET_TIME_absolute_get_remaining (gsc->sc.long_poll_timeout), + &resume_by_event, + gsc); + } + } /* end first-time initialization (NULL == gsc) */ + + if (GNUNET_SYSERR == gsc->suspended) + return MHD_NO; /* close connection on service shutdown */ + + is_past = GNUNET_TIME_absolute_is_past (gsc->sc.long_poll_timeout); + /* figure out order_id */ + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_order_by_fulfillment (TMH_db->cls, + mi->settings.id, + gsc->fulfillment_url, + hc->infix, + false, + &order_id); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order_by_fulfillment"); + } + if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) && + is_past) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_SESSION_UNKNOWN, + hc->infix); + } + } + + /* Check if paid */ + if (NULL != order_id) + { + enum GNUNET_DB_QueryStatus qs; + struct TALER_PrivateContractHashP h_contract_terms; + + qs = TMH_db->lookup_order_status (TMH_db->cls, + mi->settings.id, + order_id, + &h_contract_terms, + &paid); + if (0 >= qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order_status"); + } + } + + if (paid) + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("order_id", + order_id)); + GNUNET_free (order_id); + return ret; + } + + if (is_past) + { + MHD_RESULT ret; + + GNUNET_assert (NULL != order_id); + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_string ("order_id", + order_id)); + GNUNET_free (order_id); + return ret; + } + + GNUNET_free (order_id); + GNUNET_CONTAINER_DLL_insert (gsc_head, + gsc_tail, + gsc); + gsc->suspended = GNUNET_YES; + MHD_suspend_connection (gsc->sc.con); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_get-templates-TEMPLATE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-sessions-SESSION_ID.h b/src/backend/taler-merchant-httpd_get-sessions-SESSION_ID.h @@ -0,0 +1,48 @@ +/* + 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-httpd_get-sessions-SESSION_ID.h + * @brief implementation of GET /orders/$ID + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_GET_SESSIONS_ID_H +#define TALER_MERCHANT_HTTPD_GET_SESSIONS_ID_H + +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + +/** + * Force resuming all suspended session lookups, needed during shutdown. + */ +void +TMH_force_get_sessions_ID_resume (void); + + +/** + * Handle a GET "/sessions/$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_get_sessions_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_get-templates-ID.c b/src/backend/taler-merchant-httpd_get-templates-ID.c @@ -1,568 +0,0 @@ -/* - This file is part of TALER - (C) 2022-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-httpd_private-get-templates-ID.c - * @brief implement GET /templates/$ID - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_get-templates-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * Context for building inventory template payloads. - */ -struct InventoryPayloadContext -{ - /** - * Selected category IDs (as JSON array). - */ - const json_t *selected_categories; - - /** - * Selected product IDs (as JSON array) from contract_template. - */ - const json_t *selected_products; - - /** - * Whether all products are selected. - */ - bool selected_all; - - /** - * JSON array of products to build. - */ - json_t *products; - - /** - * JSON array of categories to build. - */ - json_t *category_payload; - - /** - * JSON array of units to build. - */ - json_t *unit_payload; - - /** - * Set of categories referenced by the products. - */ - struct TMH_CategorySet category_set; - - /** - * Set of unit identifiers referenced by the products. - */ - struct TMH_UnitSet unit_set; -}; - - -/** - * Release resources associated with an inventory payload context. - * - * @param ipc inventory payload context - */ -static void -inventory_payload_cleanup (struct InventoryPayloadContext *ipc) -{ - if (NULL != ipc->products) - json_decref (ipc->products); - if (NULL != ipc->category_payload) - json_decref (ipc->category_payload); - if (NULL != ipc->unit_payload) - json_decref (ipc->unit_payload); - GNUNET_free (ipc->category_set.ids); - TMH_unit_set_clear (&ipc->unit_set); - ipc->products = NULL; - ipc->category_payload = NULL; - ipc->unit_payload = NULL; - ipc->category_set.ids = NULL; - ipc->category_set.len = 0; -} - - -/** - * Add inventory product to JSON payload. - * - * @param cls inventory payload context - * @param product_id product identifier - * @param pd product details - * @param num_categories number of categories - * @param categories category IDs - */ -static void -add_inventory_product ( - void *cls, - const char *product_id, - const struct TALER_MERCHANTDB_InventoryProductDetails *pd, - size_t num_categories, - const uint64_t *categories) -{ - struct InventoryPayloadContext *ipc = cls; - json_t *jcategories; - json_t *product; - char remaining_stock_buf[64]; - - jcategories = json_array (); - GNUNET_assert (NULL != jcategories); - for (size_t i = 0; i < num_categories; i++) - { - /* Adding per product category */ - TMH_category_set_add (&ipc->category_set, - categories[i]); - GNUNET_assert (0 == - json_array_append_new (jcategories, - json_integer (categories[i]))); - } - GNUNET_assert (0 < pd->price_array_length); - TALER_MERCHANT_vk_format_fractional_string ( - TALER_MERCHANT_VK_STOCK, - pd->remaining_stock, - pd->remaining_stock_frac, - sizeof (remaining_stock_buf), - remaining_stock_buf); - product = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("product_id", - product_id), - GNUNET_JSON_pack_string ("product_name", - pd->product_name), - GNUNET_JSON_pack_string ("description", - pd->description), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("description_i18n", - pd->description_i18n)), - GNUNET_JSON_pack_string ("unit", - pd->unit), - TALER_JSON_pack_amount_array ("unit_prices", - pd->price_array_length, - pd->price_array), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - pd->allow_fractional_quantity), - GNUNET_JSON_pack_uint64 ("unit_precision_level", - pd->fractional_precision_level), - GNUNET_JSON_pack_string ("remaining_stock", - remaining_stock_buf), - GNUNET_JSON_pack_array_steal ("categories", - jcategories), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ("taxes", - pd->taxes)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("image_hash", - pd->image_hash))); - - /* Adding per product unit */ - TMH_unit_set_add (&ipc->unit_set, - pd->unit); - - GNUNET_assert (0 == - json_array_append_new (ipc->products, - product)); -} - - -/** - * Add an inventory category to the payload if referenced. - * - * @param cls category payload context - * @param category_id category identifier - * @param category_name category name - * @param category_name_i18n translated names - * @param product_count number of products (unused) - */ -static void -add_inventory_category (void *cls, - uint64_t category_id, - const char *category_name, - const json_t *category_name_i18n, - uint64_t product_count) -{ - struct InventoryPayloadContext *ipc = cls; - json_t *category; - - (void) product_count; - category = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("category_id", - category_id), - GNUNET_JSON_pack_string ("category_name", - category_name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("category_name_i18n", - (json_t *) category_name_i18n))); - GNUNET_assert (0 == - json_array_append_new (ipc->category_payload, - category)); -} - - -/** - * Add an inventory unit to the payload if referenced and non-builtin. - * - * @param cls unit payload context - * @param unit_serial unit identifier - * @param ud unit details - */ -static void -add_inventory_unit (void *cls, - uint64_t unit_serial, - const struct TALER_MERCHANTDB_UnitDetails *ud) -{ - struct InventoryPayloadContext *ipc = cls; - json_t *unit; - - (void) unit_serial; - - unit = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("unit", - ud->unit), - GNUNET_JSON_pack_string ("unit_name_long", - ud->unit_name_long), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", - ud->unit_name_long_i18n)), - GNUNET_JSON_pack_string ("unit_name_short", - ud->unit_name_short), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", - ud->unit_name_short_i18n)), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - ud->unit_allow_fraction), - GNUNET_JSON_pack_uint64 ("unit_precision_level", - ud->unit_precision_level)); - GNUNET_assert (0 == - json_array_append_new (ipc->unit_payload, - unit)); -} - - -/** - * Build wallet-facing payload for inventory templates. - * - * @param connection HTTP connection - * @param mi merchant instance - * @param tp template details - * @return MHD result - */ -static MHD_RESULT -handle_get_templates_inventory ( - struct MHD_Connection *connection, - const struct TMH_MerchantInstance *mi, - const struct TALER_MERCHANTDB_TemplateDetails *tp) -{ - struct InventoryPayloadContext ipc; - const char **product_ids = NULL; - uint64_t *category_ids = NULL; - size_t num_product_ids = 0; - size_t num_category_ids = 0; - json_t *inventory_payload; - json_t *template_contract; - - memset (&ipc, - 0, - sizeof (ipc)); - ipc.products = json_array (); - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("selected_categories", - &ipc.selected_categories), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("selected_products", - &ipc.selected_products), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("selected_all", - &ipc.selected_all), - NULL), - GNUNET_JSON_spec_end () - }; - const char *err_name; - unsigned int err_line; - - if (GNUNET_OK != - GNUNET_JSON_parse (tp->template_contract, - spec, - &err_name, - &err_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Invalid inventory template_contract for field %s\n", - err_name); - inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - err_name); - } - } - - if (! ipc.selected_all) - { - if (NULL != ipc.selected_products) - { - size_t max_ids; - - max_ids = json_array_size (ipc.selected_products); - if (0 < max_ids) - product_ids = GNUNET_new_array (max_ids, - const char *); - for (size_t i = 0; i < max_ids; i++) - { - const json_t *entry = json_array_get (ipc.selected_products, - i); - - if (json_is_string (entry)) - product_ids[num_product_ids++] = json_string_value (entry); - else - GNUNET_break (0); - } - } - if (NULL != ipc.selected_categories) - { - size_t max_categories; - - max_categories = json_array_size (ipc.selected_categories); - if (0 < max_categories) - category_ids = GNUNET_new_array (max_categories, - uint64_t); - for (size_t i = 0; i < max_categories; i++) - { - const json_t *entry = json_array_get (ipc.selected_categories, - i); - - if (json_is_integer (entry) && - (0 < json_integer_value (entry))) - category_ids[num_category_ids++] - = (uint64_t) json_integer_value (entry); - else - GNUNET_break (0); - } - } - } - - if (ipc.selected_all) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_inventory_products (TMH_db->cls, - mi->settings.id, - &add_inventory_product, - &ipc); - if (0 > qs) - { - GNUNET_break (0); - inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_inventory_products"); - } - } - else if ( (0 < num_product_ids) || - (0 < num_category_ids) ) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_inventory_products_filtered ( - TMH_db->cls, - mi->settings.id, - product_ids, - num_product_ids, - category_ids, - num_category_ids, - &add_inventory_product, - &ipc); - GNUNET_free (product_ids); - GNUNET_free (category_ids); - if (0 > qs) - { - GNUNET_break (0); - inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_inventory_products_filtered"); - } - } - - ipc.category_payload = json_array (); - GNUNET_assert (NULL != ipc.category_payload); - if (0 < ipc.category_set.len) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_categories_by_ids ( - TMH_db->cls, - mi->settings.id, - ipc.category_set.ids, - ipc.category_set.len, - &add_inventory_category, - &ipc); - if (0 > qs) - { - GNUNET_break (0); - inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_categories_by_ids"); - } - } - - ipc.unit_payload = json_array (); - GNUNET_assert (NULL != ipc.unit_payload); - if (0 < ipc.unit_set.len) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_custom_units_by_names ( - TMH_db->cls, - mi->settings.id, - (const char *const *) ipc.unit_set.units, - ipc.unit_set.len, - &add_inventory_unit, - &ipc); - if (0 > qs) - { - GNUNET_break (0); - inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_custom_units_by_names"); - } - } - - inventory_payload = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("products", - ipc.products), - GNUNET_JSON_pack_array_steal ("categories", - ipc.category_payload), - GNUNET_JSON_pack_array_steal ("units", - ipc.unit_payload)); - ipc.products = NULL; - ipc.category_payload = NULL; - ipc.unit_payload = NULL; - - template_contract = json_deep_copy (tp->template_contract); - GNUNET_assert (NULL != template_contract); - /* remove internal fields */ - (void) json_object_del (template_contract, - "selected_categories"); - (void) json_object_del (template_contract, - "selected_products"); - (void) json_object_del (template_contract, - "selected_all"); - /* add inventory data */ - GNUNET_assert (0 == - json_object_set_new (template_contract, - "inventory_payload", - inventory_payload)); - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("editable_defaults", - tp->editable_defaults)), - GNUNET_JSON_pack_object_steal ("template_contract", - template_contract)); - inventory_payload_cleanup (&ipc); - return ret; - } -} - - -MHD_RESULT -TMH_get_templates_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - qs = TMH_db->lookup_template (TMH_db->cls, - mi->settings.id, - hc->infix, - &tp); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_template"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - hc->infix); - } - { - MHD_RESULT ret; - - switch (TALER_MERCHANT_template_type_from_contract (tp.template_contract)) - { - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - ret = handle_get_templates_inventory (connection, - mi, - &tp); - TALER_MERCHANTDB_template_details_free (&tp); - return ret; - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("editable_defaults", - tp.editable_defaults)), - GNUNET_JSON_pack_object_incref ("template_contract", - tp.template_contract)); - TALER_MERCHANTDB_template_details_free (&tp); - return ret; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - break; - } - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "template_type"); - TALER_MERCHANTDB_template_details_free (&tp); - return ret; - } -} - - -/* end of taler-merchant-httpd_get-templates-ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-templates-ID.h b/src/backend/taler-merchant-httpd_get-templates-ID.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-get-templates-ID.h - * @brief implement GET /templates/$ID/ - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_GET_TEMPLATES_ID_H -#define TALER_MERCHANT_HTTPD_GET_TEMPLATES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/templates/$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_get_templates_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_get-templates-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c @@ -0,0 +1,568 @@ +/* + This file is part of TALER + (C) 2022-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-httpd_get-templates-TEMPLATE_ID.c + * @brief implement GET /templates/$ID + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_get-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * Context for building inventory template payloads. + */ +struct InventoryPayloadContext +{ + /** + * Selected category IDs (as JSON array). + */ + const json_t *selected_categories; + + /** + * Selected product IDs (as JSON array) from contract_template. + */ + const json_t *selected_products; + + /** + * Whether all products are selected. + */ + bool selected_all; + + /** + * JSON array of products to build. + */ + json_t *products; + + /** + * JSON array of categories to build. + */ + json_t *category_payload; + + /** + * JSON array of units to build. + */ + json_t *unit_payload; + + /** + * Set of categories referenced by the products. + */ + struct TMH_CategorySet category_set; + + /** + * Set of unit identifiers referenced by the products. + */ + struct TMH_UnitSet unit_set; +}; + + +/** + * Release resources associated with an inventory payload context. + * + * @param ipc inventory payload context + */ +static void +inventory_payload_cleanup (struct InventoryPayloadContext *ipc) +{ + if (NULL != ipc->products) + json_decref (ipc->products); + if (NULL != ipc->category_payload) + json_decref (ipc->category_payload); + if (NULL != ipc->unit_payload) + json_decref (ipc->unit_payload); + GNUNET_free (ipc->category_set.ids); + TMH_unit_set_clear (&ipc->unit_set); + ipc->products = NULL; + ipc->category_payload = NULL; + ipc->unit_payload = NULL; + ipc->category_set.ids = NULL; + ipc->category_set.len = 0; +} + + +/** + * Add inventory product to JSON payload. + * + * @param cls inventory payload context + * @param product_id product identifier + * @param pd product details + * @param num_categories number of categories + * @param categories category IDs + */ +static void +add_inventory_product ( + void *cls, + const char *product_id, + const struct TALER_MERCHANTDB_InventoryProductDetails *pd, + size_t num_categories, + const uint64_t *categories) +{ + struct InventoryPayloadContext *ipc = cls; + json_t *jcategories; + json_t *product; + char remaining_stock_buf[64]; + + jcategories = json_array (); + GNUNET_assert (NULL != jcategories); + for (size_t i = 0; i < num_categories; i++) + { + /* Adding per product category */ + TMH_category_set_add (&ipc->category_set, + categories[i]); + GNUNET_assert (0 == + json_array_append_new (jcategories, + json_integer (categories[i]))); + } + GNUNET_assert (0 < pd->price_array_length); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_STOCK, + pd->remaining_stock, + pd->remaining_stock_frac, + sizeof (remaining_stock_buf), + remaining_stock_buf); + product = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + product_id), + GNUNET_JSON_pack_string ("product_name", + pd->product_name), + GNUNET_JSON_pack_string ("description", + pd->description), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + pd->description_i18n)), + GNUNET_JSON_pack_string ("unit", + pd->unit), + TALER_JSON_pack_amount_array ("unit_prices", + pd->price_array_length, + pd->price_array), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + pd->allow_fractional_quantity), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + pd->fractional_precision_level), + GNUNET_JSON_pack_string ("remaining_stock", + remaining_stock_buf), + GNUNET_JSON_pack_array_steal ("categories", + jcategories), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("taxes", + pd->taxes)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image_hash", + pd->image_hash))); + + /* Adding per product unit */ + TMH_unit_set_add (&ipc->unit_set, + pd->unit); + + GNUNET_assert (0 == + json_array_append_new (ipc->products, + product)); +} + + +/** + * Add an inventory category to the payload if referenced. + * + * @param cls category payload context + * @param category_id category identifier + * @param category_name category name + * @param category_name_i18n translated names + * @param product_count number of products (unused) + */ +static void +add_inventory_category (void *cls, + uint64_t category_id, + const char *category_name, + const json_t *category_name_i18n, + uint64_t product_count) +{ + struct InventoryPayloadContext *ipc = cls; + json_t *category; + + (void) product_count; + category = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("category_id", + category_id), + GNUNET_JSON_pack_string ("category_name", + category_name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("category_name_i18n", + (json_t *) category_name_i18n))); + GNUNET_assert (0 == + json_array_append_new (ipc->category_payload, + category)); +} + + +/** + * Add an inventory unit to the payload if referenced and non-builtin. + * + * @param cls unit payload context + * @param unit_serial unit identifier + * @param ud unit details + */ +static void +add_inventory_unit (void *cls, + uint64_t unit_serial, + const struct TALER_MERCHANTDB_UnitDetails *ud) +{ + struct InventoryPayloadContext *ipc = cls; + json_t *unit; + + (void) unit_serial; + + unit = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("unit", + ud->unit), + GNUNET_JSON_pack_string ("unit_name_long", + ud->unit_name_long), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", + ud->unit_name_long_i18n)), + GNUNET_JSON_pack_string ("unit_name_short", + ud->unit_name_short), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", + ud->unit_name_short_i18n)), + GNUNET_JSON_pack_bool ("unit_allow_fraction", + ud->unit_allow_fraction), + GNUNET_JSON_pack_uint64 ("unit_precision_level", + ud->unit_precision_level)); + GNUNET_assert (0 == + json_array_append_new (ipc->unit_payload, + unit)); +} + + +/** + * Build wallet-facing payload for inventory templates. + * + * @param connection HTTP connection + * @param mi merchant instance + * @param tp template details + * @return MHD result + */ +static MHD_RESULT +handle_get_templates_inventory ( + struct MHD_Connection *connection, + const struct TMH_MerchantInstance *mi, + const struct TALER_MERCHANTDB_TemplateDetails *tp) +{ + struct InventoryPayloadContext ipc; + const char **product_ids = NULL; + uint64_t *category_ids = NULL; + size_t num_product_ids = 0; + size_t num_category_ids = 0; + json_t *inventory_payload; + json_t *template_contract; + + memset (&ipc, + 0, + sizeof (ipc)); + ipc.products = json_array (); + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_categories", + &ipc.selected_categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("selected_products", + &ipc.selected_products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("selected_all", + &ipc.selected_all), + NULL), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (tp->template_contract, + spec, + &err_name, + &err_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid inventory template_contract for field %s\n", + err_name); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + err_name); + } + } + + if (! ipc.selected_all) + { + if (NULL != ipc.selected_products) + { + size_t max_ids; + + max_ids = json_array_size (ipc.selected_products); + if (0 < max_ids) + product_ids = GNUNET_new_array (max_ids, + const char *); + for (size_t i = 0; i < max_ids; i++) + { + const json_t *entry = json_array_get (ipc.selected_products, + i); + + if (json_is_string (entry)) + product_ids[num_product_ids++] = json_string_value (entry); + else + GNUNET_break (0); + } + } + if (NULL != ipc.selected_categories) + { + size_t max_categories; + + max_categories = json_array_size (ipc.selected_categories); + if (0 < max_categories) + category_ids = GNUNET_new_array (max_categories, + uint64_t); + for (size_t i = 0; i < max_categories; i++) + { + const json_t *entry = json_array_get (ipc.selected_categories, + i); + + if (json_is_integer (entry) && + (0 < json_integer_value (entry))) + category_ids[num_category_ids++] + = (uint64_t) json_integer_value (entry); + else + GNUNET_break (0); + } + } + } + + if (ipc.selected_all) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_inventory_products (TMH_db->cls, + mi->settings.id, + &add_inventory_product, + &ipc); + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_inventory_products"); + } + } + else if ( (0 < num_product_ids) || + (0 < num_category_ids) ) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_inventory_products_filtered ( + TMH_db->cls, + mi->settings.id, + product_ids, + num_product_ids, + category_ids, + num_category_ids, + &add_inventory_product, + &ipc); + GNUNET_free (product_ids); + GNUNET_free (category_ids); + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_inventory_products_filtered"); + } + } + + ipc.category_payload = json_array (); + GNUNET_assert (NULL != ipc.category_payload); + if (0 < ipc.category_set.len) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_categories_by_ids ( + TMH_db->cls, + mi->settings.id, + ipc.category_set.ids, + ipc.category_set.len, + &add_inventory_category, + &ipc); + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_categories_by_ids"); + } + } + + ipc.unit_payload = json_array (); + GNUNET_assert (NULL != ipc.unit_payload); + if (0 < ipc.unit_set.len) + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_custom_units_by_names ( + TMH_db->cls, + mi->settings.id, + (const char *const *) ipc.unit_set.units, + ipc.unit_set.len, + &add_inventory_unit, + &ipc); + if (0 > qs) + { + GNUNET_break (0); + inventory_payload_cleanup (&ipc); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_custom_units_by_names"); + } + } + + inventory_payload = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("products", + ipc.products), + GNUNET_JSON_pack_array_steal ("categories", + ipc.category_payload), + GNUNET_JSON_pack_array_steal ("units", + ipc.unit_payload)); + ipc.products = NULL; + ipc.category_payload = NULL; + ipc.unit_payload = NULL; + + template_contract = json_deep_copy (tp->template_contract); + GNUNET_assert (NULL != template_contract); + /* remove internal fields */ + (void) json_object_del (template_contract, + "selected_categories"); + (void) json_object_del (template_contract, + "selected_products"); + (void) json_object_del (template_contract, + "selected_all"); + /* add inventory data */ + GNUNET_assert (0 == + json_object_set_new (template_contract, + "inventory_payload", + inventory_payload)); + { + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("editable_defaults", + tp->editable_defaults)), + GNUNET_JSON_pack_object_steal ("template_contract", + template_contract)); + inventory_payload_cleanup (&ipc); + return ret; + } +} + + +MHD_RESULT +TMH_get_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + qs = TMH_db->lookup_template (TMH_db->cls, + mi->settings.id, + hc->infix, + &tp); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_template"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + hc->infix); + } + { + MHD_RESULT ret; + + switch (TALER_MERCHANT_template_type_from_contract (tp.template_contract)) + { + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + ret = handle_get_templates_inventory (connection, + mi, + &tp); + TALER_MERCHANTDB_template_details_free (&tp); + return ret; + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("editable_defaults", + tp.editable_defaults)), + GNUNET_JSON_pack_object_incref ("template_contract", + tp.template_contract)); + TALER_MERCHANTDB_template_details_free (&tp); + return ret; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "template_type"); + TALER_MERCHANTDB_template_details_free (&tp); + return ret; + } +} + + +/* end of taler-merchant-httpd_get-templates-TEMPLATE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.h b/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_get-templates-TEMPLATE_ID.h + * @brief implement GET /templates/$ID/ + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_GET_TEMPLATES_ID_H +#define TALER_MERCHANT_HTTPD_GET_TEMPLATES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/templates/$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_get_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_get-templates-TEMPLATE_ID.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_get-terms.c b/src/backend/taler-merchant-httpd_get-terms.c @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_terms.c + * @file taler-merchant-httpd_get-terms.c * @brief Handle /terms requests to return the terms of service * @author Christian Grothoff */ @@ -76,4 +76,4 @@ TMH_load_terms (const struct GNUNET_CONFIGURATION_Handle *cfg) } -/* end of taler-merchant-httpd_terms.c */ +/* end of taler-merchant-httpd_get-terms.c */ diff --git a/src/backend/taler-merchant-httpd_get-terms.h b/src/backend/taler-merchant-httpd_get-terms.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_terms.h + * @file taler-merchant-httpd_get-terms.h * @brief Handle /terms requests to return the terms of service * @author Christian Grothoff */ diff --git a/src/backend/taler-merchant-httpd_get-webui.c b/src/backend/taler-merchant-httpd_get-webui.c @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_spa.c + * @file taler-merchant-httpd_get-webui.c * @brief logic to load the single page app (/) * @author Christian Grothoff */ diff --git a/src/backend/taler-merchant-httpd_get-webui.h b/src/backend/taler-merchant-httpd_get-webui.h @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** - * @file taler-merchant-httpd_spa.h + * @file taler-merchant-httpd_get-webui.h * @brief logic to preload and serve static files * @author Christian Grothoff */ diff --git a/src/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.c b/src/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.c @@ -0,0 +1,514 @@ +/* + This file is part of TALER + (C) 2020-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-httpd_patch-management-instances-INSTANCE.c + * @brief implementing PATCH /instances/$ID request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-management-instances-INSTANCE.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include "taler-merchant-httpd_mfa.h" + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Free memory used by @a wm + * + * @param wm wire method to free + */ +static void +free_wm (struct TMH_WireMethod *wm) +{ + GNUNET_free (wm->payto_uri.full_payto); + GNUNET_free (wm->wire_method); + GNUNET_free (wm); +} + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @param mi instance to patch + * @param mfa_check true if a MFA check is required + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +static MHD_RESULT +patch_instances_ID (struct TMH_MerchantInstance *mi, + bool mfa_check, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_MERCHANTDB_InstanceSettings is; + const char *name; + struct TMH_WireMethod *wm_head = NULL; + struct TMH_WireMethod *wm_tail = NULL; + const char *iphone = NULL; + bool no_transfer_delay; + bool no_pay_delay; + bool no_refund_delay; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("website", + (const char **) &is.website), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("email", + (const char **) &is.email), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("phone_number", + &iphone), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("logo", + (const char **) &is.logo), + NULL), + GNUNET_JSON_spec_json ("address", + &is.address), + GNUNET_JSON_spec_json ("jurisdiction", + &is.jurisdiction), + GNUNET_JSON_spec_bool ("use_stefan", + &is.use_stefan), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("default_pay_delay", + &is.default_pay_delay), + &no_pay_delay), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("default_refund_delay", + &is.default_refund_delay), + &no_refund_delay), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay", + &is.default_wire_transfer_delay), + &no_transfer_delay), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_time_rounder_interval ( + "default_wire_transfer_rounding_interval", + &is.default_wire_transfer_rounding_interval), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + memset (&is, + 0, + sizeof (is)); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + if (! TMH_location_object_valid (is.address)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + } + if ( (NULL != is.logo) && + (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "logo"); + } + + if (! TMH_location_object_valid (is.jurisdiction)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "jurisdiction"); + } + + if (no_transfer_delay) + is.default_wire_transfer_delay = mi->settings.default_wire_transfer_delay; + if (no_pay_delay) + is.default_pay_delay = mi->settings.default_pay_delay; + if (no_refund_delay) + is.default_refund_delay = mi->settings.default_refund_delay; + if (GNUNET_TIME_relative_is_forever (is.default_pay_delay)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "default_pay_delay"); + } + if (GNUNET_TIME_relative_is_forever (is.default_refund_delay)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "default_refund_delay"); + } + if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "default_wire_transfer_delay"); + } + if (NULL != iphone) + { + is.phone = TALER_MERCHANT_phone_validate_normalize (iphone, + false); + if (NULL == is.phone) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "phone_number"); + } + if ( (NULL != TMH_phone_regex) && + (0 != + regexec (&TMH_phone_rx, + is.phone, + 0, + NULL, + 0)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "phone_number"); + } + } + if ( (NULL != is.email) && + (! TALER_MERCHANT_email_valid (is.email)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "email"); + } + if ( (NULL != is.phone) && + (NULL != mi->settings.phone) && + (0 == strcmp (mi->settings.phone, + is.phone)) ) + is.phone_validated = mi->settings.phone_validated; + if ( (NULL != is.email) && + (NULL != mi->settings.email) && + (0 == strcmp (mi->settings.email, + is.email)) ) + is.email_validated = mi->settings.email_validated; + if (mfa_check) + { + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels; + + if ( (0 != (mtc & TEH_TCS_SMS)) && + (NULL != mi->settings.phone) && + (NULL == is.phone) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "phone_number"); + } + if ( (0 != (mtc & TEH_TCS_EMAIL)) && + (NULL != mi->settings.email) && + (NULL == is.email) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "email"); + } + if ( (is.phone_validated || + (NULL == is.phone) ) && + (0 != (mtc & TEH_TCS_SMS)) ) + mtc -= TEH_TCS_SMS; + if ( (is.email_validated || + (NULL == is.email) ) && + (0 != (mtc & TEH_TCS_EMAIL)) ) + mtc -= TEH_TCS_EMAIL; + switch (mtc) + { + case TEH_TCS_NONE: + ret = GNUNET_OK; + break; + case TEH_TCS_SMS: + GNUNET_assert (NULL != is.phone); + is.phone_validated = true; + /* validate new phone number, if possible require old e-mail + address for authorization */ + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + 0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL + & TEH_mandatory_tan_channels) + ? TALER_MERCHANT_MFA_CHANNEL_NONE + : TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL: + GNUNET_assert (NULL != is.email); + is.email_validated = true; + /* validate new e-mail address, if possible require old phone + address for authorization */ + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + 0 == (TALER_MERCHANT_MFA_CHANNEL_SMS + & TEH_mandatory_tan_channels) + ? TALER_MERCHANT_MFA_CHANNEL_NONE + : TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL_AND_SMS: + GNUNET_assert (NULL != mi->settings.phone); + GNUNET_assert (NULL != mi->settings.email); + is.phone_validated = true; + is.email_validated = true; + /* To change both, we require both old and both new + addresses to consent */ + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + } + if (GNUNET_OK != ret) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + for (unsigned int retry = 0; retry<MAX_RETRIES; retry++) + { + /* Cleanup after earlier loops */ + { + struct TMH_WireMethod *wm; + + while (NULL != (wm = wm_head)) + { + GNUNET_CONTAINER_DLL_remove (wm_head, + wm_tail, + wm); + free_wm (wm); + } + } + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "PATCH /instances")) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + /* Check for equality of settings */ + if (! ( (0 == strcmp (mi->settings.name, + name)) && + ((mi->settings.email == is.email) || + (NULL != is.email && NULL != mi->settings.email && + 0 == strcmp (mi->settings.email, + is.email))) && + ((mi->settings.phone == is.phone) || + (NULL != is.phone && NULL != mi->settings.phone && + 0 == strcmp (mi->settings.phone, + is.phone))) && + ((mi->settings.website == is.website) || + (NULL != is.website && NULL != mi->settings.website && + 0 == strcmp (mi->settings.website, + is.website))) && + ((mi->settings.logo == is.logo) || + (NULL != is.logo && NULL != mi->settings.logo && + 0 == strcmp (mi->settings.logo, + is.logo))) && + (1 == json_equal (mi->settings.address, + is.address)) && + (1 == json_equal (mi->settings.jurisdiction, + is.jurisdiction)) && + (mi->settings.use_stefan == is.use_stefan) && + (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay, + ==, + is.default_wire_transfer_delay)) && + (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay, + ==, + is.default_refund_delay)) && + (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay, + ==, + is.default_pay_delay)) ) ) + { + is.id = mi->settings.id; + is.name = GNUNET_strdup (name); + qs = TMH_db->update_instance (TMH_db->cls, + &is); + GNUNET_free (is.name); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + goto retry; + else + goto giveup; + } + } + qs = TMH_db->commit (TMH_db->cls); +retry: + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + continue; + break; + } /* for(... MAX_RETRIES) */ +giveup: + /* Update our 'settings' */ + GNUNET_free (mi->settings.name); + GNUNET_free (mi->settings.email); + GNUNET_free (mi->settings.phone); + GNUNET_free (mi->settings.website); + GNUNET_free (mi->settings.logo); + json_decref (mi->settings.address); + json_decref (mi->settings.jurisdiction); + is.id = mi->settings.id; + mi->settings = is; + mi->settings.address = json_incref (mi->settings.address); + mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); + mi->settings.name = GNUNET_strdup (name); + if (NULL != is.email) + mi->settings.email = GNUNET_strdup (is.email); + mi->settings.phone = is.phone; + is.phone = NULL; + if (NULL != is.website) + mi->settings.website = GNUNET_strdup (is.website); + if (NULL != is.logo) + mi->settings.logo = GNUNET_strdup (is.logo); + + GNUNET_JSON_parse_free (spec); + TMH_reload_instances (mi->settings.id); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +MHD_RESULT +TMH_private_patch_instances_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + return patch_instances_ID (mi, + true, + connection, + hc); +} + + +MHD_RESULT +TMH_private_patch_instances_default_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi; + + mi = TMH_lookup_instance (hc->infix); + if (NULL == mi) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); + } + if (mi->deleted) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED, + hc->infix); + } + return patch_instances_ID (mi, + false, + connection, + hc); +} + + +/* end of taler-merchant-httpd_patch-management-instances-INSTANCE.c */ diff --git a/src/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.h b/src/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.h @@ -0,0 +1,59 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_patch-management-instances-INSTANCE.h + * @brief implementing POST /instances request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_INSTANCES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_INSTANCES_ID_H +#include "taler-merchant-httpd.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * This is the handler called using the instance's own authentication. + * + * @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_instances_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * PATCH configuration of an existing instance, given its configuration. + * This is the handler called using the default instance's authentication. + * + * @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_instances_default_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-accounts-H_WIRE.c b/src/backend/taler-merchant-httpd_patch-private-accounts-H_WIRE.c @@ -0,0 +1,149 @@ +/* + This file is part of TALER + (C) 2023, 2025, 2026 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-httpd_patch-private-accounts-H_WIRE.c + * @brief implementing PATCH /accounts/$ID request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-accounts-H_WIRE.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_mfa.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_accounts_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *h_wire_s = hc->infix; + enum GNUNET_DB_QueryStatus qs; + const json_t *cfc; + const char *extra_wire_subject_metadata = NULL; + const char *cfu; + struct TALER_MerchantWireHashP h_wire; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("credit_facade_url", + &cfu), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("extra_wire_subject_metadata", + &extra_wire_subject_metadata), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("credit_facade_credentials", + &cfc), + NULL), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != h_wire_s); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (h_wire_s, + strlen (h_wire_s), + &h_wire, + sizeof (h_wire))) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_GENERIC_H_WIRE_MALFORMED, + h_wire_s); + } + if (! TALER_is_valid_subject_metadata_string ( + extra_wire_subject_metadata)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "extra_wire_subject_metadata"); + } + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + qs = TMH_db->update_account (TMH_db->cls, + mi->settings.id, + &h_wire, + extra_wire_subject_metadata, + cfu, + cfc); + { + MHD_RESULT ret = MHD_NO; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_account"); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ACCOUNT_UNKNOWN, + h_wire_s); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_patch-private-accounts-H_WIRE.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-accounts-H_WIRE.h b/src/backend/taler-merchant-httpd_patch-private-accounts-H_WIRE.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_patch-private-accounts-H_WIRE.h + * @brief implementing PATCH /accounts request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ACCOUNTS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ACCOUNTS_ID_H +#include "taler-merchant-httpd.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_accounts_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-categories-CATEGORY_ID.c b/src/backend/taler-merchant-httpd_patch-private-categories-CATEGORY_ID.c @@ -0,0 +1,120 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_patch-private-categories-CATEGORY_ID.c + * @brief implementing PATCH /categories/$ID request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-categories-CATEGORY_ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_patch_categories_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + unsigned long long cnum; + char dummy; + const char *category_name; + const json_t *category_name_i18n; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &category_name), + GNUNET_JSON_spec_object_const ("name_i18n", + &category_name_i18n), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &cnum, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "category_id must be a number"); + } + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + qs = TMH_db->update_category (TMH_db->cls, + mi->settings.id, + cnum, + category_name, + category_name_i18n); + { + MHD_RESULT ret = MHD_NO; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_category"); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + category_name); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_patch-private-categories-CATEGORY_ID.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-categories-CATEGORY_ID.h b/src/backend/taler-merchant-httpd_patch-private-categories-CATEGORY_ID.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_patch-private-categories-CATEGORY_ID.h + * @brief implementing PATCH /private/categories/$ID request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_CATEGORIES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_CATEGORIES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * PATCH descriptions of an existing product category. + * + * @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_categories_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-groups-GROUP_ID.c b/src/backend/taler-merchant-httpd_patch-private-groups-GROUP_ID.c @@ -0,0 +1,110 @@ +/* + 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 taler-merchant-httpd_patch-private-groups-GROUP_ID.c + * @brief implementation of PATCH /private/groups/$GROUP_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-groups-GROUP_ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_patch_group (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *group_id_str = hc->infix; + unsigned long long group_id; + const char *group_name; + const char *description; + enum GNUNET_DB_QueryStatus qs; + bool conflict; + char dummy; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("group_name", + &group_name), + GNUNET_JSON_spec_string ("description", + &description), + GNUNET_JSON_spec_end () + }; + + (void) rh; + if (1 != sscanf (group_id_str, + "%llu%c", + &group_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "group_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_product_group (TMH_db->cls, + hc->instance->settings.id, + (uint64_t) group_id, + group_name, + description, + &conflict); + + 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_product_group"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, + group_id_str); + } + if (conflict) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PRODUCT_GROUP_CONFLICTING_NAME, + group_name); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_patch-private-groups-GROUP_ID.h b/src/backend/taler-merchant-httpd_patch-private-groups-GROUP_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 taler-merchant-httpd_patch-private-groups-GROUP_ID.h + * @brief HTTP serving layer for updating product groups + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_GROUP_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_GROUP_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Handle PATCH /private/groups/$GROUP_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_group (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.c b/src/backend/taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.c @@ -0,0 +1,243 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_patch-private-orders-ORDER_ID-forget.c + * @brief implementing PATCH /orders/$ORDER_ID/forget request handling + * @author Jonathan Buchanan + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the UPDATE database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Forget part of the contract terms. + * + * @param cls pointer to the result of the forget operation. + * @param object_id name of the object to forget. + * @param parent parent of the object at @e object_id. + */ +static void +forget (void *cls, + const char *object_id, + json_t *parent) +{ + int *res = cls; + int ret; + + ret = TALER_JSON_contract_part_forget (parent, + object_id); + if (GNUNET_SYSERR == ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Matching path `%s' not forgettable!\n", + object_id); + *res = GNUNET_SYSERR; + } + if (GNUNET_NO == ret) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Matching path `%s' already forgotten!\n", + object_id); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Forgot `%s'\n", + object_id); + if (GNUNET_NO == *res) + *res = GNUNET_OK; + } +} + + +/** + * Forget fields of an order's contract terms. + * + * @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_orders_ID_forget (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *order_id = hc->infix; + enum GNUNET_DB_QueryStatus qs; + uint64_t order_serial; + + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + const json_t *fields; + json_t *contract_terms; + bool changed = false; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "forget order")) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->lookup_contract_terms (TMH_db->cls, + hc->instance->settings.id, + order_id, + &contract_terms, + &order_serial, + NULL); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract terms"); + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + order_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_assert (NULL != contract_terms); + break; + } + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("fields", + &fields), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + { + TMH_db->rollback (TMH_db->cls); + json_decref (contract_terms); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + { + size_t index; + json_t *value; + + json_array_foreach (fields, index, value) { + int forget_status = GNUNET_NO; + int expand_status; + + if (! (json_is_string (value))) + { + TMH_db->rollback (TMH_db->cls); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT, + "field is not a string"); + } + expand_status = TALER_JSON_expand_path (contract_terms, + json_string_value (value), + &forget, + &forget_status); + if (GNUNET_SYSERR == forget_status) + { + /* We tried to forget a field that isn't forgettable */ + TMH_db->rollback (TMH_db->cls); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE, + json_string_value (value)); + } + if (GNUNET_OK == forget_status) + changed = true; + if (GNUNET_SYSERR == expand_status) + { + /* One of the paths was malformed and couldn't be expanded */ + TMH_db->rollback (TMH_db->cls); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT, + json_string_value (value)); + } + } + } + + if (! changed) + { + TMH_db->rollback (TMH_db->cls); + json_decref (contract_terms); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + qs = TMH_db->update_contract_terms (TMH_db->cls, + hc->instance->settings.id, + order_id, + contract_terms); + json_decref (contract_terms); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + else + { + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + } + if (0 > qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + + return TALER_MHD_reply_static (connection, + MHD_HTTP_OK, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.h b/src/backend/taler-merchant-httpd_patch-private-orders-ORDER_ID-forget.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_patch-private-orders-ORDER_ID-forget.h + * @brief implementing PATCH /orders/$ORDER_ID/forget request handling + * @author Jonathan Buchanan + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ORDERS_ID_FORGET_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ORDERS_ID_FORGET_H +#include "taler-merchant-httpd.h" + + +/** + * Forget fields of an order's contract terms. + * + * @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_orders_ID_forget (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.c b/src/backend/taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.c @@ -0,0 +1,114 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_patch-private-otp-devices-DEVICE_ID.c + * @brief implementing PATCH /otp-devices/$ID request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_patch_otp_devices_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *device_id = hc->infix; + struct TALER_MERCHANTDB_OtpDeviceDetails tp = {0}; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("otp_device_description", + (const char **) &tp.otp_description), + TALER_JSON_spec_otp_type ("otp_algorithm", + &tp.otp_algorithm), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("otp_ctr", + &tp.otp_ctr), + NULL), + GNUNET_JSON_spec_mark_optional ( + + TALER_JSON_spec_otp_key ("otp_key", + (const char **) &tp.otp_key), + NULL), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != device_id); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + qs = TMH_db->update_otp (TMH_db->cls, + mi->settings.id, + device_id, + &tp); + { + MHD_RESULT ret = MHD_NO; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_pos"); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, + device_id); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.h b/src/backend/taler-merchant-httpd_patch-private-otp-devices-DEVICE_ID.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_patch-private-otp-devices-DEVICE_ID.h + * @brief implementing PATCH /otp-devices/$ID request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_OTP_DEVICES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_OTP_DEVICES_ID_H + +#include "taler-merchant-httpd.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_otp_devices_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-pots-POT_ID.c b/src/backend/taler-merchant-httpd_patch-private-pots-POT_ID.c @@ -0,0 +1,152 @@ +/* + 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 taler-merchant-httpd_patch-private-pots-POT_ID.c + * @brief implementation of PATCH /private/pots/$POT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-pots-POT_ID.h" +#include <taler/taler_json_lib.h> + + +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; + size_t expected_pot_total_len; + struct TALER_Amount *expected_pot_totals; + bool no_expected_total; + size_t new_pot_total_len; + struct TALER_Amount *new_pot_totals; + 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_array ("expected_pot_total", + &expected_pot_total_len, + &expected_pot_totals), + &no_expected_total), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any_array ("new_pot_totals", + &new_pot_total_len, + &new_pot_totals), + &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, + expected_pot_total_len, + no_expected_total + ? NULL + : expected_pot_totals, + new_pot_total_len, + no_new_total + ? NULL + : new_pot_totals, + &conflict_total, + &conflict_name); + GNUNET_JSON_parse_free (spec); + 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_patch-private-pots-POT_ID.h b/src/backend/taler-merchant-httpd_patch-private-pots-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 taler-merchant-httpd_patch-private-pots-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_patch-private-products-PRODUCT_ID.c b/src/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.c @@ -0,0 +1,482 @@ +/* + This file is part of TALER + (C) 2020--2026 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-httpd_patch-private-products-PRODUCT_ID.c + * @brief implementing PATCH /products/$ID request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-products-PRODUCT_ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_products_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *product_id = hc->infix; + struct TALER_MERCHANTDB_ProductDetails pd = {0}; + const json_t *categories = NULL; + int64_t total_stock; + const char *unit_total_stock = NULL; + bool unit_total_stock_missing; + bool total_stock_missing; + struct TALER_Amount price; + bool price_missing; + bool unit_price_missing; + bool unit_allow_fraction; + bool unit_allow_fraction_missing; + uint32_t unit_precision_level; + bool unit_precision_missing; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + /* new in protocol v20, thus optional for backwards-compatibility */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("product_name", + (const char **) &pd.product_name), + NULL), + GNUNET_JSON_spec_string ("description", + (const char **) &pd.description), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("description_i18n", + &pd.description_i18n), + NULL), + GNUNET_JSON_spec_string ("unit", + (const char **) &pd.unit), + // FIXME: deprecated API + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("price", + &price), + &price_missing), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any_array ("unit_price", + &pd.price_array_length, + &pd.price_array), + &unit_price_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("image", + (const char **) &pd.image), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("taxes", + &pd.taxes), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("categories", + &categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_total_stock", + &unit_total_stock), + &unit_total_stock_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_int64 ("total_stock", + &total_stock), + &total_stock_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("unit_allow_fraction", + &unit_allow_fraction), + &unit_allow_fraction_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("unit_precision_level", + &unit_precision_level), + &unit_precision_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("total_lost", + &pd.total_lost), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("product_group_id", + &pd.product_group_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("money_pot_id", + &pd.money_pot_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("address", + &pd.address), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("next_restock", + &pd.next_restock), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("minimum_age", + &pd.minimum_age), + NULL), + GNUNET_JSON_spec_end () + }; + MHD_RESULT ret; + size_t num_cats = 0; + uint64_t *cats = NULL; + bool no_instance; + ssize_t no_cat; + bool no_product; + bool lost_reduced; + bool sold_reduced; + bool stock_reduced; + bool no_group; + bool no_pot; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != product_id); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + /* For pre-v20 clients, we use the description given as the + product name; remove once we make product_name mandatory. */ + if (NULL == pd.product_name) + pd.product_name = pd.description; + } + if (! unit_price_missing) + { + if (! price_missing) + { + if (0 != TALER_amount_cmp (&price, + &pd.price_array[0])) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "price,unit_price mismatch"); + goto cleanup; + } + } + if (GNUNET_OK != + TMH_validate_unit_price_array (pd.price_array, + pd.price_array_length)) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_price"); + goto cleanup; + } + } + else + { + if (price_missing) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "price missing"); + goto cleanup; + } + pd.price_array = GNUNET_new_array (1, + struct TALER_Amount); + pd.price_array[0] = price; + pd.price_array_length = 1; + } + if (! unit_precision_missing) + { + if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_precision_level"); + goto cleanup; + } + } + { + bool default_allow_fractional; + uint32_t default_precision_level; + + if (GNUNET_OK != + TMH_unit_defaults_for_instance (mi, + pd.unit, + &default_allow_fractional, + &default_precision_level)) + { + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "unit defaults"); + goto cleanup; + } + if (unit_allow_fraction_missing) + unit_allow_fraction = default_allow_fractional; + if (unit_precision_missing) + unit_precision_level = default_precision_level; + + if (! unit_allow_fraction) + unit_precision_level = 0; + pd.fractional_precision_level = unit_precision_level; + } + { + const char *eparam; + if (GNUNET_OK != + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_STOCK, + unit_allow_fraction, + total_stock_missing, + total_stock, + unit_total_stock_missing, + unit_total_stock, + &pd.total_stock, + &pd.total_stock_frac, + &eparam)) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + goto cleanup; + } + pd.allow_fractional_quantity = unit_allow_fraction; + } + if (NULL == pd.address) + pd.address = json_object (); + + if (! TMH_location_object_valid (pd.address)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + goto cleanup; + } + num_cats = json_array_size (categories); + cats = GNUNET_new_array (num_cats, + uint64_t); + { + size_t idx; + json_t *val; + + json_array_foreach (categories, idx, val) + { + if (! json_is_integer (val)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "categories"); + goto cleanup; + } + cats[idx] = json_integer_value (val); + } + } + + if (NULL == pd.description_i18n) + pd.description_i18n = json_object (); + + if (! TALER_JSON_check_i18n (pd.description_i18n)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + goto cleanup; + } + + if (NULL == pd.taxes) + pd.taxes = json_array (); + /* check taxes is well-formed */ + if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "taxes"); + goto cleanup; + } + + if (NULL == pd.image) + pd.image = (char *) ""; + if (! TALER_MERCHANT_image_data_url_valid (pd.image)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image"); + goto cleanup; + } + + if ( (pd.total_stock < pd.total_sold + pd.total_lost) || + (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS, + NULL); + goto cleanup; + } + + qs = TMH_db->update_product (TMH_db->cls, + mi->settings.id, + product_id, + &pd, + num_cats, + cats, + &no_instance, + &no_cat, + &no_product, + &lost_reduced, + &sold_reduced, + &stock_reduced, + &no_group, + &no_pot); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected problem in stored procedure"); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if (no_instance) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + goto cleanup; + } + if (-1 != no_cat) + { + char cat_str[24]; + + GNUNET_snprintf (cat_str, + sizeof (cat_str), + "%llu", + (unsigned long long) no_cat); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + cat_str); + goto cleanup; + } + if (no_product) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + product_id); + goto cleanup; + } + if (no_group) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, + NULL); + goto cleanup; + } + if (no_pot) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + NULL); + goto cleanup; + } + if (lost_reduced) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED, + NULL); + goto cleanup; + } + if (sold_reduced) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED, + NULL); + goto cleanup; + } + if (stock_reduced) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED, + NULL); + goto cleanup; + } + /* success! */ + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +cleanup: + GNUNET_free (cats); + GNUNET_free (pd.price_array); + GNUNET_JSON_parse_free (spec); + return ret; +} + + +/* end of taler-merchant-httpd_patch-private-products-PRODUCT_ID.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.h b/src/backend/taler-merchant-httpd_patch-private-products-PRODUCT_ID.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_patch-private-products-PRODUCT_ID.h + * @brief implementing PATCH /products/$ID request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_PRODUCTS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_PRODUCTS_ID_H +#include "taler-merchant-httpd.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_products_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-reports-REPORT_ID.c b/src/backend/taler-merchant-httpd_patch-private-reports-REPORT_ID.c @@ -0,0 +1,146 @@ +/* + 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 taler-merchant-httpd_patch-private-reports-REPORT_ID.c + * @brief implementation of PATCH /private/reports/$REPORT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-reports-REPORT_ID.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> + +MHD_RESULT +TMH_private_patch_report (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *report_id_str = hc->infix; + unsigned long long report_id; + const char *description; + const char *program_section; + const char *mime_type; + const char *data_source; + const char *target_address; + struct GNUNET_TIME_Relative frequency; + struct GNUNET_TIME_Relative frequency_shift; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("description", + &description), + GNUNET_JSON_spec_string ("program_section", + &program_section), + GNUNET_JSON_spec_string ("mime_type", + &mime_type), + GNUNET_JSON_spec_string ("data_source", + &data_source), + GNUNET_JSON_spec_string ("target_address", + &target_address), + GNUNET_JSON_spec_relative_time ("report_frequency", + &frequency), + GNUNET_JSON_spec_relative_time ("report_frequency_shift", + &frequency_shift), + GNUNET_JSON_spec_end () + }; + + (void) rh; + { + char dummy; + + if (1 != sscanf (report_id_str, + "%llu%c", + &report_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "report_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; + } + } + if ('/' != data_source[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "data_source"); + + } + + qs = TMH_db->update_report (TMH_db->cls, + hc->instance->settings.id, + report_id, + program_section, + description, + mime_type, + data_source, + target_address, + frequency, + frequency_shift); + 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_report"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, + report_id_str); + } + + /* FIXME-Optimization: Trigger MERCHANT_REPORT_UPDATE event inside of UPDATE transaction */ + { + struct GNUNET_DB_EventHeaderP ev = { + .size = htons (sizeof (ev)), + .type = htons (TALER_DBEVENT_MERCHANT_REPORT_UPDATE) + }; + + TMH_db->event_notify (TMH_db->cls, + &ev, + NULL, + 0); + } + + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} diff --git a/src/backend/taler-merchant-httpd_patch-private-reports-REPORT_ID.h b/src/backend/taler-merchant-httpd_patch-private-reports-REPORT_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 taler-merchant-httpd_patch-private-reports-REPORT_ID.h + * @brief HTTP serving layer for updating reports + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_REPORT_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_REPORT_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Handle PATCH /private/reports/$REPORT_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_report (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.c @@ -0,0 +1,217 @@ +/* + This file is part of TALER + (C) 2022, 2024 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-httpd_patch-private-templates-TEMPLATE_ID.c + * @brief implementing PATCH /templates/$ID request handling + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * Determine the cause of the PATCH failure in more detail and report. + * + * @param connection connection to report on + * @param instance_id instance we are processing + * @param template_id ID of the product to patch + * @param tp template details we failed to set + */ +static MHD_RESULT +determine_cause (struct MHD_Connection *connection, + const char *instance_id, + const char *template_id, + const struct TALER_MERCHANTDB_TemplateDetails *tp) +{ + struct TALER_MERCHANTDB_TemplateDetails tpx; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_template (TMH_db->cls, + instance_id, + template_id, + &tpx); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + template_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* do below */ + } + + { + enum TALER_ErrorCode ec; + + ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + TALER_MERCHANTDB_template_details_free (&tpx); + GNUNET_break (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE != ec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + ec, + NULL); + } +} + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *template_id = hc->infix; + struct TALER_MERCHANTDB_TemplateDetails tp = {0}; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("template_description", + (const char **) &tp.template_description), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("otp_id", + (const char **) &tp.otp_id), + NULL), + GNUNET_JSON_spec_json ("template_contract", + &tp.template_contract), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("editable_defaults", + &tp.editable_defaults), + NULL), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != template_id); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + if (! TALER_MERCHANT_template_contract_valid (tp.template_contract)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "template_contract"); + } + if (NULL != tp.editable_defaults) + { + const char *key; + json_t *val; + + json_object_foreach (tp.editable_defaults, key, val) + { + if (NULL != + json_object_get (tp.template_contract, + key)) + { + char *msg; + MHD_RESULT ret; + + GNUNET_break_op (0); + GNUNET_asprintf (&msg, + "editable_defaults::%s conflicts with template_contract", + key); + GNUNET_JSON_parse_free (spec); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + msg); + GNUNET_free (msg); + return ret; + } + } + } + + qs = TMH_db->update_template (TMH_db->cls, + mi->settings.id, + template_id, + &tp); + { + MHD_RESULT ret = MHD_NO; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = determine_cause (connection, + mi->settings.id, + template_id, + &tp); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.h b/src/backend/taler-merchant-httpd_patch-private-templates-TEMPLATE_ID.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_patch-private-templates-TEMPLATE_ID.h + * @brief implementing PATCH /templates request handling + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TEMPLATES_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TEMPLATES_ID_H +#include "taler-merchant-httpd.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.c b/src/backend/taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.c @@ -0,0 +1,159 @@ +/* + This file is part of TALER + (C) 2023, 2024 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-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.c + * @brief implementing PATCH /tokenfamilies/$SLUG request handling + * @author Christian Blättler + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Handle a PATCH "/tokenfamilies/$slug" 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_token_family_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *slug = hc->infix; + struct TALER_MERCHANTDB_TokenFamilyDetails details = {0}; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + (const char **) &details.name), + GNUNET_JSON_spec_string ("description", + (const char **) &details.description), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("description_i18n", + &details.description_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("extra_data", + &details.extra_data), + NULL), + GNUNET_JSON_spec_timestamp ("valid_after", + &details.valid_after), + GNUNET_JSON_spec_timestamp ("valid_before", + &details.valid_before), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != slug); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + /* Ensure that valid_after is before valid_before */ + if (GNUNET_TIME_timestamp_cmp (details.valid_after, + >=, + details.valid_before)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "valid_after >= valid_before"); + } + + if (NULL == details.description_i18n) + { + details.description_i18n = json_object (); + GNUNET_assert (NULL != details.description_i18n); + } + if (! TALER_JSON_check_i18n (details.description_i18n)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + } + + { + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret = MHD_NO; + + qs = TMH_db->update_token_family (TMH_db->cls, + mi->settings.id, + slug, + &details); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND, + slug); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h b/src/backend/taler-merchant-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_patch-private-tokenfamilies-TOKEN_FAMILY_SLUG.h + * @brief implementing PATCH /tokenfamilies/$SLUG request handling + * @author Christian Blättler + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TOKEN_FAMILIES_SLUG_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TOKEN_FAMILIES_SLUG_H +#include "taler-merchant-httpd.h" + + +/** + * Handle a PATCH "/tokenfamilies/$slug" 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_token_family_SLUG (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-units-UNIT.c b/src/backend/taler-merchant-httpd_patch-private-units-UNIT.c @@ -0,0 +1,242 @@ +/* + 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-httpd_patch-private-units-UNIT.c + * @brief implement PATCH /private/units/$UNIT + * @author Bohdan Potuzhnyi + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-units-UNIT.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + +#define TMH_MAX_UNIT_PRECISION_LEVEL 6 + + +MHD_RESULT +TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *unit_id = hc->infix; + struct TALER_MERCHANTDB_UnitDetails nud = { 0 }; + bool unit_allow_fraction_missing = true; + bool unit_precision_missing = true; + bool unit_active_missing = true; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_name_long", + (const char **) &nud.unit_name_long), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("unit_name_long_i18n", + &nud.unit_name_long_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_name_short", + (const char **) &nud.unit_name_short), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("unit_name_short_i18n", + &nud.unit_name_short_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("unit_allow_fraction", + &nud.unit_allow_fraction), + &unit_allow_fraction_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("unit_precision_level", + &nud.unit_precision_level), + &unit_precision_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("unit_active", + &nud.unit_active), + &unit_active_missing), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const bool *unit_allow_fraction_ptr = NULL; + const uint32_t *unit_precision_ptr = NULL; + const bool *unit_active_ptr = NULL; + enum GNUNET_DB_QueryStatus qs; + bool no_instance = false; + bool no_unit = false; + bool builtin_conflict = false; + MHD_RESULT ret = MHD_YES; + + (void) rh; + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != unit_id); + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + + if (NULL == nud.unit_name_long && + NULL == nud.unit_name_long_i18n && + NULL == nud.unit_name_short && + NULL == nud.unit_name_short_i18n && + unit_allow_fraction_missing && + unit_precision_missing && + unit_active_missing) + { + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + goto cleanup; + } + + if (! unit_precision_missing) + { + if (nud.unit_precision_level > TMH_MAX_UNIT_PRECISION_LEVEL) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_precision_level"); + goto cleanup; + } + unit_precision_ptr = &nud.unit_precision_level; + } + + if (! unit_allow_fraction_missing) + { + unit_allow_fraction_ptr = &nud.unit_allow_fraction; + if (! nud.unit_allow_fraction) + { + nud.unit_precision_level = 0; + unit_precision_missing = false; + unit_precision_ptr = &nud.unit_precision_level; + } + } + + if (! unit_active_missing) + unit_active_ptr = &nud.unit_active; + + if (NULL != nud.unit_name_long_i18n) + { + if (! TALER_JSON_check_i18n (nud.unit_name_long_i18n)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_name_long_i18n"); + goto cleanup; + } + } + + if (NULL != nud.unit_name_short_i18n) + { + if (! TALER_JSON_check_i18n (nud.unit_name_short_i18n)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_name_short_i18n"); + goto cleanup; + } + } + + qs = TMH_db->update_unit (TMH_db->cls, + mi->settings.id, + unit_id, + nud.unit_name_long, + nud.unit_name_long_i18n, + nud.unit_name_short, + nud.unit_name_short_i18n, + unit_allow_fraction_ptr, + unit_precision_ptr, + unit_active_ptr, + &no_instance, + &no_unit, + &builtin_conflict); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "update_unit"); + goto cleanup; + case GNUNET_DB_STATUS_HARD_ERROR: + default: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_unit"); + goto cleanup; + } + + if (no_instance) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + goto cleanup; + } + if (no_unit) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, + unit_id); + goto cleanup; + } + if (builtin_conflict) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN, + unit_id); + goto cleanup; + } + + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + +cleanup: + if (NULL != nud.unit_name_long_i18n) + { + json_decref (nud.unit_name_long_i18n); + nud.unit_name_long_i18n = NULL; + } + if (NULL != nud.unit_name_short_i18n) + { + json_decref (nud.unit_name_short_i18n); + nud.unit_name_short_i18n = NULL; + } + GNUNET_JSON_parse_free (spec); + return ret; +} + + +/* end of taler-merchant-httpd_patch-private-units-UNIT.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-units-UNIT.h b/src/backend/taler-merchant-httpd_patch-private-units-UNIT.h @@ -0,0 +1,33 @@ +/* + 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-httpd_patch-private-units-UNIT.h + * @brief implement PATCH /private/units/$UNIT + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_UNITS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_UNITS_ID_H + +#include "taler-merchant-httpd.h" + + +MHD_RESULT +TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_patch-private-units-UNIT.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.c b/src/backend/taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.c @@ -0,0 +1,188 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_patch-private-webhooks-WEBHOOK_ID.c + * @brief implementing PATCH /webhooks/$ID request handling + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Determine the cause of the PATCH failure in more detail and report. + * + * @param connection connection to report on + * @param instance_id instance we are processing + * @param webhook_id ID of the webhook to patch + * @param wb webhook details we failed to set + */ +static MHD_RESULT +determine_cause (struct MHD_Connection *connection, + const char *instance_id, + const char *webhook_id, + const struct TALER_MERCHANTDB_WebhookDetails *wb) +{ + struct TALER_MERCHANTDB_WebhookDetails wpx; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_webhook (TMH_db->cls, + instance_id, + webhook_id, + &wpx); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_WEBHOOK_UNKNOWN, + webhook_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* do below */ + } + + { + enum TALER_ErrorCode ec; + + ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + TALER_MERCHANTDB_webhook_details_free (&wpx); + GNUNET_break (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE != ec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + ec, + NULL); + } +} + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_webhooks_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *webhook_id = hc->infix; + struct TALER_MERCHANTDB_WebhookDetails wb = {0}; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("event_type", + (const char **) &wb.event_type), + TALER_JSON_spec_web_url ("url", + (const char **) &wb.url), + GNUNET_JSON_spec_string ("http_method", + (const char **) &wb.http_method), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("header_template", + (const char **) &wb.header_template), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("body_template", + (const char **) &wb.body_template), + NULL), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != webhook_id); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + + qs = TMH_db->update_webhook (TMH_db->cls, + mi->settings.id, + webhook_id, + &wb); + { + MHD_RESULT ret = MHD_NO; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "unexpected serialization problem"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = determine_cause (connection, + mi->settings.id, + webhook_id, + &wb); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + break; + } + GNUNET_JSON_parse_free (spec); + return ret; + } +} + + +/* end of taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.c */ diff --git a/src/backend/taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.h b/src/backend/taler-merchant-httpd_patch-private-webhooks-WEBHOOK_ID.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_patch-private-webhooks-WEBHOOK_ID.h + * @brief implementing PATCH /webhooks request handling + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_WEBHOOKS_ID_H +#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_WEBHOOKS_ID_H +#include "taler-merchant-httpd.h" + + +/** + * PATCH configuration of an existing instance, given its configuration. + * + * @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_webhooks_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c b/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c @@ -0,0 +1,342 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems SA + + GNU 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. + + GNU 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-httpd_post-management-instances-INSTANCE-auth.c + * @brief implementing POST /instances/$ID/auth request handling + * @author Christian Grothoff + * @author Florian Dold + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-management-instances-INSTANCE-auth.h" +#include "taler-merchant-httpd_auth.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_mfa.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Change the authentication settings of an instance. + * + * @param mi instance to modify settings of + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @param auth_override The authentication settings for this instance + * do not apply due to administrative action. Do not check + * against the DB value when updating the auth token. + * @param tcs set of multi-factor authorizations required + * @return MHD result code + */ +static MHD_RESULT +post_instances_ID_auth (struct TMH_MerchantInstance *mi, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + bool auth_override, + enum TEH_TanChannelSet tcs) +{ + struct TALER_MERCHANTDB_InstanceAuthSettings ias; + const char *auth_pw = NULL; + json_t *jauth = hc->request_body; + + { + enum GNUNET_GenericReturnValue ret; + + ret = TMH_check_auth_config (connection, + jauth, + &auth_pw); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + + if ( (0 != (tcs & TEH_TCS_SMS) && + ( (NULL == mi->settings.phone) || + (NULL == TMH_helper_sms) || + (! mi->settings.phone_validated) ) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot reset password: SMS factor not available\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_MFA_MISSING, + "phone_number"); + } + if ( (0 != (tcs & TEH_TCS_EMAIL) && + ( (NULL == mi->settings.email) || + (NULL == TMH_helper_email) || + (! mi->settings.email_validated) ) ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot reset password: E-mail factor not available\n"); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_MFA_MISSING, + "email"); + } + if (! auth_override) + { + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized + + switch (tcs) + { + case TEH_TCS_NONE: + ret = GNUNET_OK; + break; + case TEH_TCS_SMS: + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL: + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL_AND_SMS: + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + } + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + if (NULL == auth_pw) + { + memset (&ias.auth_salt, + 0, + sizeof (ias.auth_salt)); + memset (&ias.auth_hash, + 0, + sizeof (ias.auth_hash)); + } + else + { + TMH_compute_auth (auth_pw, + &ias.auth_salt, + &ias.auth_hash); + } + + /* Store the new auth information in the database */ + { + enum GNUNET_DB_QueryStatus qs; + + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "post /instances/$ID/auth")) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + + /* Make the authentication update a serializable operation. + We first check that the authentication information + that the caller's request authenticated with + is still up to date. + Otherwise, we've detected a conflicting update + to the authentication. */ + { + struct TALER_MERCHANTDB_InstanceAuthSettings db_ias; + enum TALER_ErrorCode ec; + + qs = TMH_db->lookup_instance_auth (TMH_db->cls, + mi->settings.id, + &db_ias); + + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Instance got purged. */ + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + goto retry; + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Success! */ + break; + } + + if (! auth_override) + { + // FIXME are we sure what the scope here is? + ec = TMH_check_token (hc->auth_token, + mi->settings.id, + &hc->auth_scope); + if (TALER_EC_NONE != ec) + { + TMH_db->rollback (TMH_db->cls); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refusing auth change: `%s'\n", + TALER_ErrorCode_get_hint (ec)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, + NULL); + } + } + } + + qs = TMH_db->update_instance_auth (TMH_db->cls, + mi->settings.id, + &ias); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + goto retry; + } + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +retry: + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; /* success! -- or hard failure */ + } /* for .. MAX_RETRIES */ + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + /* Finally, also update our running process */ + mi->auth = ias; + } + TMH_reload_instances (mi->settings.id); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +MHD_RESULT +TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + return post_instances_ID_auth (mi, + connection, + hc, + false, + TEH_TCS_NONE); +} + + +MHD_RESULT +TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + return post_instances_ID_auth (mi, + connection, + hc, + false, + TEH_mandatory_tan_channels); +} + + +MHD_RESULT +TMH_private_post_instances_default_ID_auth ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi; + MHD_RESULT ret; + + if ( (NULL == hc->infix) || + (0 == strcmp ("admin", + hc->infix)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_MFA_MISSING, + "not allowed for 'admin' account"); + } + mi = TMH_lookup_instance (hc->infix); + if (NULL == mi) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); + } + ret = post_instances_ID_auth (mi, + connection, + hc, + true, + TEH_TCS_NONE); + return ret; +} + + +/* end of taler-merchant-httpd_post-management-instances-INSTANCE-auth.c */ diff --git a/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.h b/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.h @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems SA + + GNU 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. + + GNU 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-httpd_post-management-instances-INSTANCE-auth.h + * @brief implements POST /instances/$ID/auth request handling + * @author Christian Grothoff + * @author Florian Dold + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_AUTH_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_AUTH_H +#include "taler-merchant-httpd.h" + + +/** + * Change the instance's auth settings. + * This is the handler called using the instance's own authentication. + * + * @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_instances_ID_auth ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Change the instance's auth settings. + * This is the handler called using the default instance's authentication. + * + * @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_instances_default_ID_auth ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Change the instance's auth settings. + * This is the public handler used to reset a password if + * the original password was forgotten. Always requires + * 2-FA to be configured for the account with two additional + * factors. + * + * @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_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-management-instances.c b/src/backend/taler-merchant-httpd_post-management-instances.c @@ -0,0 +1,694 @@ +/* + This file is part of TALER + (C) 2020-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-httpd_post-management-instances.c + * @brief implementing POST /instances request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-management-instances.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_auth.h" +#include "taler-merchant-httpd_mfa.h" +#include "taler/taler_merchant_bank_lib.h" +#include <taler/taler_dbevents.h> +#include <taler/taler_json_lib.h> +#include <regex.h> + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Generate an instance, given its configuration. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @param login_token_expiration set to how long a login token validity + * should be, use zero if no login token should be created + * @param validation_needed true if self-provisioned and + * email/phone registration is required before the + * instance can become fully active + * @return MHD result code + */ +static MHD_RESULT +post_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + struct GNUNET_TIME_Relative login_token_expiration, + bool validation_needed) +{ + struct TALER_MERCHANTDB_InstanceSettings is = { 0 }; + struct TALER_MERCHANTDB_InstanceAuthSettings ias; + const char *auth_password = NULL; + struct TMH_WireMethod *wm_head = NULL; + struct TMH_WireMethod *wm_tail = NULL; + const json_t *jauth; + const char *iphone = NULL; + bool no_pay_delay; + bool no_refund_delay; + bool no_transfer_delay; + bool no_rounding_interval; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("id", + (const char **) &is.id), + GNUNET_JSON_spec_string ("name", + (const char **) &is.name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("email", + (const char **) &is.email), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("phone_number", + &iphone), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("website", + (const char **) &is.website), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("logo", + (const char **) &is.logo), + NULL), + GNUNET_JSON_spec_object_const ("auth", + &jauth), + GNUNET_JSON_spec_json ("address", + &is.address), + GNUNET_JSON_spec_json ("jurisdiction", + &is.jurisdiction), + GNUNET_JSON_spec_bool ("use_stefan", + &is.use_stefan), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("default_pay_delay", + &is.default_pay_delay), + &no_pay_delay), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("default_refund_delay", + &is.default_refund_delay), + &no_refund_delay), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay", + &is.default_wire_transfer_delay), + &no_transfer_delay), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_time_rounder_interval ( + "default_wire_transfer_rounding_interval", + &is.default_wire_transfer_rounding_interval), + &no_rounding_interval), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + if (no_pay_delay) + is.default_pay_delay = TMH_default_pay_delay; + if (no_refund_delay) + is.default_refund_delay = TMH_default_refund_delay; + if (no_transfer_delay) + is.default_wire_transfer_delay = TMH_default_wire_transfer_delay; + if (no_rounding_interval) + is.default_wire_transfer_rounding_interval + = TMH_default_wire_transfer_rounding_interval; + if (GNUNET_TIME_relative_is_forever (is.default_pay_delay)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "default_pay_delay"); + } + if (GNUNET_TIME_relative_is_forever (is.default_refund_delay)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "default_refund_delay"); + } + if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "default_wire_transfer_delay"); + } + if (NULL != iphone) + { + is.phone = TALER_MERCHANT_phone_validate_normalize (iphone, + false); + if (NULL == is.phone) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "phone_number"); + } + if ( (NULL != TMH_phone_regex) && + (0 != + regexec (&TMH_phone_rx, + is.phone, + 0, + NULL, + 0)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "phone_number"); + } + } + if ( (NULL != is.email) && + (! TALER_MERCHANT_email_valid (is.email)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "email"); + } + + { + enum GNUNET_GenericReturnValue ret; + + ret = TMH_check_auth_config (connection, + jauth, + &auth_password); + if (GNUNET_OK != ret) + { + GNUNET_free (is.phone); + GNUNET_JSON_parse_free (spec); + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + } + + /* check 'id' well-formed */ + { + static bool once; + static regex_t reg; + bool id_wellformed = true; + + if (! once) + { + once = true; + GNUNET_assert (0 == + regcomp (&reg, + "^[A-Za-z0-9][A-Za-z0-9_.@-]+$", + REG_EXTENDED)); + } + + if (0 != regexec (&reg, + is.id, + 0, NULL, 0)) + id_wellformed = false; + if (! id_wellformed) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "id"); + } + } + + if (! TMH_location_object_valid (is.address)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + } + + if (! TMH_location_object_valid (is.jurisdiction)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "jurisdiction"); + } + + if ( (NULL != is.logo) && + (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "logo"); + } + + { + /* Test if an instance of this id is known */ + struct TMH_MerchantInstance *mi; + + mi = TMH_lookup_instance (is.id); + if (NULL != mi) + { + if (mi->deleted) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED, + is.id); + } + /* Check for idempotency */ + if ( (0 == strcmp (mi->settings.id, + is.id)) && + (0 == strcmp (mi->settings.name, + is.name)) && + ((mi->settings.email == is.email) || + (NULL != is.email && NULL != mi->settings.email && + 0 == strcmp (mi->settings.email, + is.email))) && + ((mi->settings.website == is.website) || + (NULL != is.website && NULL != mi->settings.website && + 0 == strcmp (mi->settings.website, + is.website))) && + ((mi->settings.logo == is.logo) || + (NULL != is.logo && NULL != mi->settings.logo && + 0 == strcmp (mi->settings.logo, + is.logo))) && + ( ( (NULL != auth_password) && + (GNUNET_OK == + TMH_check_auth (auth_password, + &mi->auth.auth_salt, + &mi->auth.auth_hash)) ) || + ( (NULL == auth_password) && + (GNUNET_YES == + GNUNET_is_zero (&mi->auth.auth_hash))) ) && + (1 == json_equal (mi->settings.address, + is.address)) && + (1 == json_equal (mi->settings.jurisdiction, + is.jurisdiction)) && + (mi->settings.use_stefan == is.use_stefan) && + (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay, + ==, + is.default_wire_transfer_delay)) && + (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay, + ==, + is.default_pay_delay)) && + (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay, + ==, + is.default_refund_delay)) ) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, + is.id); + } + } + + /* Check MFA is satisfied */ + if (validation_needed) + { + enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; + + if ( (0 != (TEH_TCS_SMS & TEH_mandatory_tan_channels)) && + (NULL == is.phone) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); /* does nothing... */ + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "phone_number"); + + } + if ( (0 != (TEH_TCS_EMAIL & TEH_mandatory_tan_channels)) && + (NULL == is.email) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "email"); + } + switch (TEH_mandatory_tan_channels) + { + case TEH_TCS_NONE: + GNUNET_assert (0); + ret = GNUNET_OK; + break; + case TEH_TCS_SMS: + is.phone_validated = true; + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL: + is.email_validated = true; + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL_AND_SMS: + is.phone_validated = true; + is.email_validated = true; + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + } + if (GNUNET_OK != ret) + { + GNUNET_JSON_parse_free (spec); + GNUNET_free (is.phone); + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + /* handle authentication token setup */ + if (NULL == auth_password) + { + memset (&ias.auth_salt, + 0, + sizeof (ias.auth_salt)); + memset (&ias.auth_hash, + 0, + sizeof (ias.auth_hash)); + } + else + { + /* Sets 'auth_salt' and 'auth_hash' */ + TMH_compute_auth (auth_password, + &ias.auth_salt, + &ias.auth_hash); + } + + /* create in-memory data structure */ + { + struct TMH_MerchantInstance *mi; + enum GNUNET_DB_QueryStatus qs; + + mi = GNUNET_new (struct TMH_MerchantInstance); + mi->wm_head = wm_head; + mi->wm_tail = wm_tail; + mi->settings = is; + mi->settings.address = json_incref (mi->settings.address); + mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); + mi->settings.id = GNUNET_strdup (is.id); + mi->settings.name = GNUNET_strdup (is.name); + if (NULL != is.email) + mi->settings.email = GNUNET_strdup (is.email); + mi->settings.phone = is.phone; + is.phone = NULL; + if (NULL != is.website) + mi->settings.website = GNUNET_strdup (is.website); + if (NULL != is.logo) + mi->settings.logo = GNUNET_strdup (is.logo); + mi->auth = ias; + mi->validation_needed = validation_needed; + GNUNET_CRYPTO_eddsa_key_create (&mi->merchant_priv.eddsa_priv); + GNUNET_CRYPTO_eddsa_key_get_public (&mi->merchant_priv.eddsa_priv, + &mi->merchant_pub.eddsa_pub); + + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "post /instances")) + { + mi->rc = 1; + TMH_instance_decref (mi); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->insert_instance (TMH_db->cls, + &mi->merchant_pub, + &mi->merchant_priv, + &mi->settings, + &mi->auth, + validation_needed); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + { + MHD_RESULT ret; + + TMH_db->rollback (TMH_db->cls); + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + is.id); + mi->rc = 1; + TMH_instance_decref (mi); + GNUNET_JSON_parse_free (spec); + return ret; + } + case GNUNET_DB_STATUS_SOFT_ERROR: + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + { + MHD_RESULT ret; + + TMH_db->rollback (TMH_db->cls); + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, + is.id); + mi->rc = 1; + TMH_instance_decref (mi); + GNUNET_JSON_parse_free (spec); + return ret; + } + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* handled below */ + break; + } + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; +retry: + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; /* success! -- or hard failure */ + } /* for .. MAX_RETRIES */ + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + mi->rc = 1; + TMH_instance_decref (mi); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + /* Finally, also update our running process */ + GNUNET_assert (GNUNET_OK == + TMH_add_instance (mi)); + TMH_reload_instances (mi->settings.id); + } + GNUNET_JSON_parse_free (spec); + if (GNUNET_TIME_relative_is_zero (login_token_expiration)) + { + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + + { + struct TALER_MERCHANTDB_LoginTokenP btoken; + enum TMH_AuthScope iscope = TMH_AS_REFRESHABLE | TMH_AS_SPA; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp expiration_time; + bool refreshable = true; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &btoken, + sizeof (btoken)); + expiration_time + = GNUNET_TIME_relative_to_timestamp (login_token_expiration); + qs = TMH_db->insert_login_token (TMH_db->cls, + is.id, + &btoken, + GNUNET_TIME_timestamp_get (), + expiration_time, + iscope, + "login token from instance creation"); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_login_token"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + { + char *tok; + MHD_RESULT ret; + char *val; + + val = GNUNET_STRINGS_data_to_string_alloc (&btoken, + sizeof (btoken)); + GNUNET_asprintf (&tok, + RFC_8959_PREFIX "%s", + val); + GNUNET_free (val); + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("access_token", + tok), + GNUNET_JSON_pack_string ("token", + tok), + GNUNET_JSON_pack_string ("scope", + TMH_get_name_by_scope (iscope, + &refreshable)), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_timestamp ("expiration", + expiration_time)); + GNUNET_free (tok); + return ret; + } + } +} + + +/** + * Generate an instance, given its configuration. + * + * @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_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + return post_instances (rh, + connection, + hc, + GNUNET_TIME_UNIT_ZERO, + false); +} + + +/** + * Generate an instance, given its configuration. + * Public handler to be used when self-provisioning. + * + * @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_public_post_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct GNUNET_TIME_Relative expiration; + + TALER_MHD_parse_request_rel_time (connection, + "token_validity_ms", + &expiration); + if (GNUNET_YES != + TMH_have_self_provisioning) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, + "Self-provisioning is not enabled"); + } + + return post_instances (rh, + connection, + hc, + expiration, + TEH_TCS_NONE != + TEH_mandatory_tan_channels); +} + + +/* end of taler-merchant-httpd_post-management-instances.c */ diff --git a/src/backend/taler-merchant-httpd_post-management-instances.h b/src/backend/taler-merchant-httpd_post-management-instances.h @@ -0,0 +1,59 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_post-management-instances.h + * @brief implementing POST /instances request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_H +#include "taler-merchant-httpd.h" + + +/** + * Generate an instance, given its configuration. + * + * @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_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Generate an instance, given its configuration. + * Public handler to be used when self-provisioning. + * + * @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_public_post_instances (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ID-abort.c @@ -1,1043 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2021 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-httpd_post-orders-ID-abort.c - * @brief handling of POST /orders/$ID/abort requests - * @author Marcello Stanisci - * @author Christian Grothoff - * @author Florian Dold - */ -#include "taler/platform.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_post-orders-ID-abort.h" - - -/** - * How long to wait before giving up processing with the exchange? - */ -#define ABORT_GENERIC_TIMEOUT (GNUNET_TIME_relative_multiply ( \ - GNUNET_TIME_UNIT_SECONDS, \ - 30)) - -/** - * How often do we retry the (complex!) database transaction? - */ -#define MAX_RETRIES 5 - -/** - * Information we keep for an individual call to the /abort handler. - */ -struct AbortContext; - -/** - * Information kept during a /abort request for each coin. - */ -struct RefundDetails -{ - - /** - * Public key of the coin. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Signature from the exchange confirming the refund. - * Set if we were successful (status 200). - */ - struct TALER_ExchangeSignatureP exchange_sig; - - /** - * Public key used for @e exchange_sig. - * Set if we were successful (status 200). - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Reference to the main AbortContext - */ - struct AbortContext *ac; - - /** - * Handle to the refund operation we are performing for - * this coin, NULL after the operation is done. - */ - struct TALER_EXCHANGE_PostCoinsRefundHandle *rh; - - /** - * URL of the exchange that issued this coin. - */ - char *exchange_url; - - /** - * Body of the response from the exchange. Note that the body returned MUST - * be freed (if non-NULL). - */ - json_t *exchange_reply; - - /** - * Amount this coin contributes to the total purchase price. - * This amount includes the deposit fee. - */ - struct TALER_Amount amount_with_fee; - - /** - * Offset of this coin into the `rd` array of all coins in the - * @e ac. - */ - unsigned int index; - - /** - * HTTP status returned by the exchange (if any). - */ - unsigned int http_status; - - /** - * Did we try to process this refund yet? - */ - bool processed; - - /** - * Did we find the deposit in our own database? - */ - bool found_deposit; -}; - - -/** - * Information we keep for an individual call to the /abort handler. - */ -struct AbortContext -{ - - /** - * Hashed contract terms (according to client). - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Context for our operation. - */ - struct TMH_HandlerContext *hc; - - /** - * Stored in a DLL. - */ - struct AbortContext *next; - - /** - * Stored in a DLL. - */ - struct AbortContext *prev; - - /** - * Array with @e coins_cnt coins we are despositing. - */ - struct RefundDetails *rd; - - /** - * MHD connection to return to - */ - struct MHD_Connection *connection; - - /** - * Task called when the (suspended) processing for - * the /abort request times out. - * Happens when we don't get a response from the exchange. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * Response to return, NULL if we don't have one yet. - */ - struct MHD_Response *response; - - /** - * Handle to the exchange that we are doing the abortment with. - * (initially NULL while @e fo is trying to find a exchange). - */ - struct TALER_EXCHANGE_Handle *mh; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_KeysOperation *fo; - - /** - * URL of the exchange used for the last @e fo. - */ - const char *current_exchange; - - /** - * Number of coins this abort is for. Length of the @e rd array. - */ - size_t coins_cnt; - - /** - * How often have we retried the 'main' transaction? - */ - unsigned int retry_counter; - - /** - * Number of transactions still pending. Initially set to - * @e coins_cnt, decremented on each transaction that - * successfully finished. - */ - size_t pending; - - /** - * Number of transactions still pending for the currently selected - * exchange. Initially set to the number of coins started at the - * exchange, decremented on each transaction that successfully - * finished. Once it hits zero, we pick the next exchange. - */ - size_t pending_at_ce; - - /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). - */ - unsigned int response_code; - - /** - * #GNUNET_NO if the @e connection was not suspended, - * #GNUNET_YES if the @e connection was suspended, - * #GNUNET_SYSERR if @e connection was resumed to as - * part of #MH_force_ac_resume during shutdown. - */ - int suspended; - -}; - - -/** - * Head of active abort context DLL. - */ -static struct AbortContext *ac_head; - -/** - * Tail of active abort context DLL. - */ -static struct AbortContext *ac_tail; - - -/** - * Abort all pending /deposit operations. - * - * @param ac abort context to abort - */ -static void -abort_refunds (struct AbortContext *ac) -{ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Aborting pending /deposit operations\n"); - for (size_t i = 0; i<ac->coins_cnt; i++) - { - struct RefundDetails *rdi = &ac->rd[i]; - - if (NULL != rdi->rh) - { - TALER_EXCHANGE_post_coins_refund_cancel (rdi->rh); - rdi->rh = NULL; - } - } -} - - -void -TMH_force_ac_resume () -{ - for (struct AbortContext *ac = ac_head; - NULL != ac; - ac = ac->next) - { - abort_refunds (ac); - if (NULL != ac->timeout_task) - { - GNUNET_SCHEDULER_cancel (ac->timeout_task); - ac->timeout_task = NULL; - } - if (GNUNET_YES == ac->suspended) - { - ac->suspended = GNUNET_SYSERR; - MHD_resume_connection (ac->connection); - } - } -} - - -/** - * Resume the given abort context and send the given response. - * Stores the response in the @a ac and signals MHD to resume - * the connection. Also ensures MHD runs immediately. - * - * @param ac abortment context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_abort_with_response (struct AbortContext *ac, - unsigned int response_code, - struct MHD_Response *response) -{ - abort_refunds (ac); - ac->response_code = response_code; - ac->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /abort handling as exchange interaction is done (%u)\n", - response_code); - if (NULL != ac->timeout_task) - { - GNUNET_SCHEDULER_cancel (ac->timeout_task); - ac->timeout_task = NULL; - } - GNUNET_assert (GNUNET_YES == ac->suspended); - ac->suspended = GNUNET_NO; - MHD_resume_connection (ac->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -/** - * Resume abortment processing with an error. - * - * @param ac operation to resume - * @param http_status http status code to return - * @param ec taler error code to return - * @param msg human readable error message - */ -static void -resume_abort_with_error (struct AbortContext *ac, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *msg) -{ - resume_abort_with_response (ac, - http_status, - TALER_MHD_make_error (ec, - msg)); -} - - -/** - * Generate a response that indicates abortment success. - * - * @param ac abortment context - */ -static void -generate_success_response (struct AbortContext *ac) -{ - json_t *refunds; - unsigned int hc = MHD_HTTP_OK; - - refunds = json_array (); - if (NULL == refunds) - { - GNUNET_break (0); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, - "could not create JSON array"); - return; - } - for (size_t i = 0; i<ac->coins_cnt; i++) - { - struct RefundDetails *rdi = &ac->rd[i]; - json_t *detail; - - if (rdi->found_deposit) - { - if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) && - (MHD_HTTP_NOT_FOUND != rdi->http_status) && - (MHD_HTTP_GONE != rdi->http_status) ) || - (0 == rdi->http_status) || - (NULL == rdi->exchange_reply) ) - { - hc = MHD_HTTP_BAD_GATEWAY; - } - } - if (! rdi->found_deposit) - { - detail = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "undeposited")); - } - else - { - if (MHD_HTTP_OK != rdi->http_status) - { - detail = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "failure"), - GNUNET_JSON_pack_uint64 ("exchange_status", - rdi->http_status), - GNUNET_JSON_pack_uint64 ("exchange_code", - (NULL != rdi->exchange_reply) - ? TALER_JSON_get_error_code ( - rdi->exchange_reply) - : TALER_EC_GENERIC_INVALID_RESPONSE), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("exchange_reply", - rdi->exchange_reply))); - } - else - { - detail = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "success"), - GNUNET_JSON_pack_uint64 ("exchange_status", - rdi->http_status), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &rdi->exchange_sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &rdi->exchange_pub)); - } - } - GNUNET_assert (0 == - json_array_append_new (refunds, - detail)); - } - - /* Resume and send back the response. */ - resume_abort_with_response ( - ac, - hc, - TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("refunds", - refunds))); -} - - -/** - * Custom cleanup routine for a `struct AbortContext`. - * - * @param cls the `struct AbortContext` to clean up. - */ -static void -abort_context_cleanup (void *cls) -{ - struct AbortContext *ac = cls; - - if (NULL != ac->timeout_task) - { - GNUNET_SCHEDULER_cancel (ac->timeout_task); - ac->timeout_task = NULL; - } - abort_refunds (ac); - for (size_t i = 0; i<ac->coins_cnt; i++) - { - struct RefundDetails *rdi = &ac->rd[i]; - - if (NULL != rdi->exchange_reply) - { - json_decref (rdi->exchange_reply); - rdi->exchange_reply = NULL; - } - GNUNET_free (rdi->exchange_url); - } - GNUNET_free (ac->rd); - if (NULL != ac->fo) - { - TMH_EXCHANGES_keys4exchange_cancel (ac->fo); - ac->fo = NULL; - } - if (NULL != ac->response) - { - MHD_destroy_response (ac->response); - ac->response = NULL; - } - GNUNET_CONTAINER_DLL_remove (ac_head, - ac_tail, - ac); - GNUNET_free (ac); -} - - -/** - * Find the exchange we need to talk to for the next - * pending deposit permission. - * - * @param ac abortment context we are processing - */ -static void -find_next_exchange (struct AbortContext *ac); - - -/** - * Function called with the result from the exchange (to be - * passed back to the wallet). - * - * @param cls closure - * @param rr response data - */ -static void -refund_cb (void *cls, - const struct TALER_EXCHANGE_PostCoinsRefundResponse *rr) -{ - struct RefundDetails *rd = cls; - const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr; - struct AbortContext *ac = rd->ac; - - rd->rh = NULL; - rd->http_status = hr->http_status; - rd->exchange_reply = json_incref ((json_t*) hr->reply); - if (MHD_HTTP_OK == hr->http_status) - { - rd->exchange_pub = rr->details.ok.exchange_pub; - rd->exchange_sig = rr->details.ok.exchange_sig; - } - ac->pending_at_ce--; - if (0 == ac->pending_at_ce) - find_next_exchange (ac); -} - - -/** - * Function called with the result of our exchange lookup. - * - * @param cls the `struct AbortContext` - * @param keys keys of the exchange - * @param exchange representation of the exchange - */ -static void -process_abort_with_exchange (void *cls, - struct TALER_EXCHANGE_Keys *keys, - struct TMH_Exchange *exchange) -{ - struct AbortContext *ac = cls; - - (void) exchange; - ac->fo = NULL; - GNUNET_assert (GNUNET_YES == ac->suspended); - if (NULL == keys) - { - resume_abort_with_response ( - ac, - MHD_HTTP_GATEWAY_TIMEOUT, - TALER_MHD_make_error ( - TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, - NULL)); - return; - } - /* Initiate refund operation for all coins of - the current exchange (!) */ - GNUNET_assert (0 == ac->pending_at_ce); - for (size_t i = 0; i<ac->coins_cnt; i++) - { - struct RefundDetails *rdi = &ac->rd[i]; - - if (rdi->processed) - continue; - GNUNET_assert (NULL == rdi->rh); - if (0 != strcmp (rdi->exchange_url, - ac->current_exchange)) - continue; - rdi->processed = true; - ac->pending--; - if (! rdi->found_deposit) - { - /* Coin wasn't even deposited yet, we do not need to refund it. */ - continue; - } - rdi->rh = TALER_EXCHANGE_post_coins_refund_create ( - TMH_curl_ctx, - ac->current_exchange, - keys, - &rdi->amount_with_fee, - &ac->h_contract_terms, - &rdi->coin_pub, - 0, /* rtransaction_id */ - &ac->hc->instance->merchant_priv); - if (NULL == rdi->rh) - { - GNUNET_break_op (0); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED, - "Failed to start refund with exchange"); - return; - } - GNUNET_assert (TALER_EC_NONE == - TALER_EXCHANGE_post_coins_refund_start (rdi->rh, - &refund_cb, - rdi)); - ac->pending_at_ce++; - } - /* Still continue if no coins for this exchange were deposited. */ - if (0 == ac->pending_at_ce) - find_next_exchange (ac); -} - - -/** - * Begin of the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. - * - * @param ac abortment context to transact - */ -static void -begin_transaction (struct AbortContext *ac); - - -/** - * Find the exchange we need to talk to for the next - * pending deposit permission. - * - * @param ac abortment context we are processing - */ -static void -find_next_exchange (struct AbortContext *ac) -{ - for (size_t i = 0; i<ac->coins_cnt; i++) - { - struct RefundDetails *rdi = &ac->rd[i]; - - if (! rdi->processed) - { - ac->current_exchange = rdi->exchange_url; - ac->fo = TMH_EXCHANGES_keys4exchange (ac->current_exchange, - false, - &process_abort_with_exchange, - ac); - if (NULL == ac->fo) - { - /* strange, should have happened on pay! */ - GNUNET_break (0); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, - ac->current_exchange); - return; - } - return; - } - } - ac->current_exchange = NULL; - GNUNET_assert (0 == ac->pending); - /* We are done with all the HTTP requests, go back and try - the 'big' database transaction! (It should work now!) */ - begin_transaction (ac); -} - - -/** - * Function called with information about a coin that was deposited. - * - * @param cls closure - * @param exchange_url exchange where @a coin_pub was deposited - * @param coin_pub public key of the coin - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param refund_fee fee the exchange will charge for refunding this coin - */ -static void -refund_coins (void *cls, - const char *exchange_url, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee) -{ - struct AbortContext *ac = cls; - struct GNUNET_TIME_Timestamp now; - - (void) deposit_fee; - (void) refund_fee; - now = GNUNET_TIME_timestamp_get (); - for (size_t i = 0; i<ac->coins_cnt; i++) - { - struct RefundDetails *rdi = &ac->rd[i]; - enum GNUNET_DB_QueryStatus qs; - - if ( (0 != - GNUNET_memcmp (coin_pub, - &rdi->coin_pub)) || - (0 != - strcmp (exchange_url, - rdi->exchange_url)) ) - continue; /* not in request */ - rdi->found_deposit = true; - rdi->amount_with_fee = *amount_with_fee; - /* Store refund in DB */ - qs = TMH_db->refund_coin (TMH_db->cls, - ac->hc->instance->settings.id, - &ac->h_contract_terms, - now, - coin_pub, - /* justification */ - "incomplete abortment aborted"); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_transaction (ac); - return; - } - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "refund_coin"); - return; - } - } /* for all coins */ -} - - -/** - * Begin of the DB transaction. If required (from soft/serialization errors), - * the transaction can be restarted here. - * - * @param ac abortment context to transact - */ -static void -begin_transaction (struct AbortContext *ac) -{ - enum GNUNET_DB_QueryStatus qs; - - /* Avoid re-trying transactions on soft errors forever! */ - if (ac->retry_counter++ > MAX_RETRIES) - { - GNUNET_break (0); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - return; - } - GNUNET_assert (GNUNET_YES == ac->suspended); - - /* First, try to see if we have all we need already done */ - TMH_db->preflight (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "run abort")) - { - GNUNET_break (0); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - return; - } - - /* check payment was indeed incomplete - (now that we are in the transaction scope!) */ - { - struct TALER_PrivateContractHashP h_contract_terms; - bool paid; - - qs = TMH_db->lookup_order_status (TMH_db->cls, - ac->hc->instance->settings.id, - ac->hc->infix, - &h_contract_terms, - &paid); - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_HARD_ERROR: - /* Always report on hard error to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_transaction (ac); - return; - } - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "order status"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - TMH_db->rollback (TMH_db->cls); - resume_abort_with_error (ac, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND, - "Could not find contract"); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if (paid) - { - /* Payment is complete, refuse to abort. */ - TMH_db->rollback (TMH_db->cls); - resume_abort_with_error (ac, - MHD_HTTP_PRECONDITION_FAILED, - TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE, - "Payment was complete, refusing to abort"); - return; - } - } - if (0 != - GNUNET_memcmp (&ac->h_contract_terms, - &h_contract_terms)) - { - GNUNET_break_op (0); - TMH_db->rollback (TMH_db->cls); - resume_abort_with_error (ac, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH, - "Provided hash does not match order on file"); - return; - } - } - - /* Mark all deposits we have in our database for the order as refunded. */ - qs = TMH_db->lookup_deposits (TMH_db->cls, - ac->hc->instance->settings.id, - &ac->h_contract_terms, - &refund_coins, - ac); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_transaction (ac); - return; - } - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "deposits"); - return; - } - - qs = TMH_db->commit (TMH_db->cls); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - begin_transaction (ac); - return; - } - resume_abort_with_error (ac, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return; - } - - /* At this point, the refund got correctly committed - into the database. Tell exchange about abort/refund. */ - if (ac->pending > 0) - { - find_next_exchange (ac); - return; - } - generate_success_response (ac); -} - - -/** - * Try to parse the abort request into the given abort context. - * Schedules an error response in the connection on failure. - * - * @param connection HTTP connection we are receiving abortment on - * @param hc context we use to handle the abortment - * @param ac state of the /abort call - * @return #GNUNET_OK on success, - * #GNUNET_NO on failure (response was queued with MHD) - * #GNUNET_SYSERR on hard error (MHD connection must be dropped) - */ -static enum GNUNET_GenericReturnValue -parse_abort (struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - struct AbortContext *ac) -{ - const json_t *coins; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("coins", - &coins), - GNUNET_JSON_spec_fixed_auto ("h_contract", - &ac->h_contract_terms), - - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - return res; - } - ac->coins_cnt = json_array_size (coins); - if (0 == ac->coins_cnt) - { - GNUNET_break_op (0); - return (MHD_YES == - TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY, - "coins")) - ? GNUNET_NO - : GNUNET_SYSERR; - } - /* note: 1 coin = 1 deposit confirmation expected */ - ac->pending = ac->coins_cnt; - ac->rd = GNUNET_new_array (ac->coins_cnt, - struct RefundDetails); - /* This loop populates the array 'rd' in 'ac' */ - { - unsigned int coins_index; - json_t *coin; - json_array_foreach (coins, coins_index, coin) - { - struct RefundDetails *rd = &ac->rd[coins_index]; - const char *exchange_url; - struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_web_url ("exchange_url", - &exchange_url), - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &rd->coin_pub), - GNUNET_JSON_spec_end () - }; - - res = TALER_MHD_parse_json_data (connection, - coin, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - return res; - } - rd->exchange_url = GNUNET_strdup (exchange_url); - rd->index = coins_index; - rd->ac = ac; - } - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling /abort for order `%s' with contract hash `%s'\n", - ac->hc->infix, - GNUNET_h2s (&ac->h_contract_terms.hash)); - return GNUNET_OK; -} - - -/** - * Handle a timeout for the processing of the abort request. - * - * @param cls our `struct AbortContext` - */ -static void -handle_abort_timeout (void *cls) -{ - struct AbortContext *ac = cls; - - ac->timeout_task = NULL; - GNUNET_assert (GNUNET_YES == ac->suspended); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming abort with error after timeout\n"); - if (NULL != ac->fo) - { - TMH_EXCHANGES_keys4exchange_cancel (ac->fo); - ac->fo = NULL; - } - resume_abort_with_error (ac, - MHD_HTTP_GATEWAY_TIMEOUT, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, - NULL); -} - - -MHD_RESULT -TMH_post_orders_ID_abort (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct AbortContext *ac = hc->ctx; - - if (NULL == ac) - { - ac = GNUNET_new (struct AbortContext); - GNUNET_CONTAINER_DLL_insert (ac_head, - ac_tail, - ac); - ac->connection = connection; - ac->hc = hc; - hc->ctx = ac; - hc->cc = &abort_context_cleanup; - } - if (GNUNET_SYSERR == ac->suspended) - return MHD_NO; /* during shutdown, we don't generate any more replies */ - if (0 != ac->response_code) - { - MHD_RESULT res; - - /* We are *done* processing the request, - just queue the response (!) */ - if (UINT_MAX == ac->response_code) - { - GNUNET_break (0); - return MHD_NO; /* hard error */ - } - res = MHD_queue_response (connection, - ac->response_code, - ac->response); - MHD_destroy_response (ac->response); - ac->response = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Queueing response (%u) for /abort (%s).\n", - (unsigned int) ac->response_code, - res ? "OK" : "FAILED"); - return res; - } - { - enum GNUNET_GenericReturnValue ret; - - ret = parse_abort (connection, - hc, - ac); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - - /* Abort not finished, suspend while we interact with the exchange */ - GNUNET_assert (GNUNET_NO == ac->suspended); - MHD_suspend_connection (connection); - ac->suspended = GNUNET_YES; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Suspending abort handling while working with the exchange\n"); - ac->timeout_task = GNUNET_SCHEDULER_add_delayed (ABORT_GENERIC_TIMEOUT, - &handle_abort_timeout, - ac); - begin_transaction (ac); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_post-orders-ID-abort.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-abort.h b/src/backend/taler-merchant-httpd_post-orders-ID-abort.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 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/> -*/ -/** - * @file taler-merchant-httpd_post-orders-ID-abort.h - * @brief headers for POST /orders/$ID/abort handler - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_ABORT_H -#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_ABORT_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Force all abort contexts to be resumed as we are about - * to shut down MHD. - */ -void -TMH_force_ac_resume (void); - - -/** - * Abort payment for a claimed order. - * - * @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_post_orders_ID_abort (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c @@ -1,331 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2018, 2020 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-httpd_post-orders-ID-claim.c - * @brief headers for POST /orders/$ID/claim handler - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_signatures.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler-merchant-httpd_post-orders-ID-claim.h" - - -/** - * How often do we retry the database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Run transaction to claim @a order_id for @a nonce. - * - * @param hc handler context with information about instance to claim order at - * @param order_id order to claim - * @param nonce nonce to use for the claim - * @param claim_token the token that should be used to verify the claim - * @param[out] contract_terms set to the resulting contract terms - * (for any non-negative result; - * @return transaction status code - * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the order was claimed by a different - * nonce (@a contract_terms set to non-NULL) - * OR if the order is is unknown (@a contract_terms is NULL) - * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the order was successfully claimed - */ -static enum GNUNET_DB_QueryStatus -claim_order (struct TMH_HandlerContext *hc, - const char *order_id, - const struct GNUNET_CRYPTO_EddsaPublicKey *nonce, - const struct TALER_ClaimTokenP *claim_token, - json_t **contract_terms) -{ - const char *instance_id = hc->instance->settings.id; - struct TALER_ClaimTokenP order_ct; - enum GNUNET_DB_QueryStatus qs; - uint64_t order_serial; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "claim order")) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - instance_id, - order_id, - contract_terms, - &order_serial, - NULL); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - return qs; - } - - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - /* We already have claimed contract terms for this order_id */ - struct GNUNET_CRYPTO_EddsaPublicKey stored_nonce; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("nonce", - &stored_nonce), - GNUNET_JSON_spec_end () - }; - - TMH_db->rollback (TMH_db->cls); - GNUNET_assert (NULL != *contract_terms); - - if (GNUNET_OK != - GNUNET_JSON_parse (*contract_terms, - spec, - NULL, - NULL)) - { - /* this should not be possible: contract_terms should always - have a nonce! */ - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - if (0 != - GNUNET_memcmp (&stored_nonce, - nonce)) - { - GNUNET_JSON_parse_free (spec); - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } - GNUNET_JSON_parse_free (spec); - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - } - - GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs); - - /* Now we need to claim the order. */ - { - struct TALER_MerchantPostDataHashP unused; - struct GNUNET_TIME_Timestamp timestamp; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_timestamp ("timestamp", - &timestamp), - GNUNET_JSON_spec_end () - }; - - /* see if we have this order in our table of unclaimed orders */ - qs = TMH_db->lookup_order (TMH_db->cls, - instance_id, - order_id, - &order_ct, - &unused, - contract_terms); - if (0 >= qs) - { - TMH_db->rollback (TMH_db->cls); - return qs; - } - GNUNET_assert (NULL != *contract_terms); - if (GNUNET_OK != - GNUNET_JSON_parse (*contract_terms, - spec, - NULL, - NULL)) - { - /* this should not be possible: contract_terms should always - have a timestamp! */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - GNUNET_assert (0 == - json_object_set_new ( - *contract_terms, - "nonce", - GNUNET_JSON_from_data_auto (nonce))); - if (0 != GNUNET_memcmp_priv (&order_ct, - claim_token)) - { - TMH_db->rollback (TMH_db->cls); - json_decref (*contract_terms); - *contract_terms = NULL; - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - } - qs = TMH_db->insert_contract_terms (TMH_db->cls, - instance_id, - order_id, - *contract_terms, - &order_serial); - if (0 >= qs) - { - TMH_db->rollback (TMH_db->cls); - json_decref (*contract_terms); - *contract_terms = NULL; - return qs; - } - // FIXME: unify notifications? or do we need both? - TMH_notify_order_change (TMH_lookup_instance (instance_id), - TMH_OSF_CLAIMED, - timestamp, - order_serial); - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about status change of order %s\n", - order_id); - GNUNET_CRYPTO_hash (order_id, - strlen (order_id), - &pay_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &pay_eh.header, - NULL, - 0); - } - qs = TMH_db->commit (TMH_db->cls); - if (0 > qs) - return qs; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - } -} - - -MHD_RESULT -TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *order_id = hc->infix; - struct GNUNET_CRYPTO_EddsaPublicKey nonce; - enum GNUNET_DB_QueryStatus qs; - json_t *contract_terms; - struct TALER_ClaimTokenP claim_token = { 0 }; - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("nonce", - &nonce), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("token", - &claim_token), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - json_dumpf (hc->request_body, - stderr, - JSON_INDENT (2)); - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - } - contract_terms = NULL; - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - TMH_db->preflight (TMH_db->cls); - qs = claim_order (hc, - order_id, - &nonce, - &claim_token, - &contract_terms); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - if (NULL == contract_terms) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND, - order_id); - /* already claimed! */ - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED, - order_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - GNUNET_assert (NULL != contract_terms); - break; /* Good! return signature (below) */ - } - - /* create contract signature */ - { - struct TALER_PrivateContractHashP hash; - struct TALER_MerchantSignatureP merchant_sig; - - /** - * Hash of the JSON contract in UTF-8 including 0-termination, - * using JSON_COMPACT | JSON_SORT_KEYS - */ - - if (GNUNET_OK != - TALER_JSON_contract_hash (contract_terms, - &hash)) - { - GNUNET_break (0); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - NULL); - } - - TALER_merchant_contract_sign (&hash, - &hc->instance->merchant_priv, - &merchant_sig); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_object_steal ("contract_terms", - contract_terms), - GNUNET_JSON_pack_data_auto ("sig", - &merchant_sig)); - } -} - - -/* end of taler-merchant-httpd_post-orders-ID-claim.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.h b/src/backend/taler-merchant-httpd_post-orders-ID-claim.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2020 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/> -*/ -/** - * @file taler-merchant-httpd_post-orders-ID-claim.h - * @brief headers for POST /orders/$ID/claim handler - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_POST_ORDERS_ID_CLAIM_H -#define TALER_MERCHANT_HTTPD_POST_ORDERS_ID_CLAIM_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - -/** - * Manage a POST /orders/$ID/claim request. Allows the client to - * claim the order (unless already claims) and creates the respective - * contract. Returns the contract terms. - * - * @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_post_orders_ID_claim (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-paid.c b/src/backend/taler-merchant-httpd_post-orders-ID-paid.c @@ -1,198 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2023 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-httpd_post-orders-ID-paid.c - * @brief handling of POST /orders/$ID/paid requests - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <taler/taler_dbevents.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_post-orders-ID-paid.h" - - -/** - * Use database to notify other clients about the - * session being captured. - * - * @param hc http context - * @param session_id the captured session - * @param fulfillment_url the URL that is now paid for by @a session_id - */ -static void -trigger_session_notification (struct TMH_HandlerContext *hc, - const char *session_id, - const char *fulfillment_url) -{ - struct TMH_SessionEventP session_eh = { - .header.size = htons (sizeof (session_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_CRYPTO_hash (session_id, - strlen (session_id), - &session_eh.h_session_id); - GNUNET_CRYPTO_hash (fulfillment_url, - strlen (fulfillment_url), - &session_eh.h_fulfillment_url); - TMH_db->event_notify (TMH_db->cls, - &session_eh.header, - NULL, - 0); -} - - -MHD_RESULT -TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *order_id = hc->infix; - struct TALER_MerchantSignatureP merchant_sig; - const char *session_id; - struct TALER_PrivateContractHashP hct; - char *fulfillment_url; - enum GNUNET_DB_QueryStatus qs; - bool refunded; - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("sig", - &merchant_sig), - GNUNET_JSON_spec_fixed_auto ("h_contract", - &hct), - GNUNET_JSON_spec_string ("session_id", - &session_id), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - } - - if (GNUNET_OK != - TALER_merchant_pay_verify (&hct, - &hc->instance->merchant_pub, - &merchant_sig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID, - NULL); - } - - TMH_db->preflight (TMH_db->cls); - - qs = TMH_db->update_contract_session (TMH_db->cls, - hc->instance->settings.id, - &hct, - session_id, - &fulfillment_url, - &refunded); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Unknown order id given: `%s'\n", - order_id); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "update_contract_session"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "update_contract_session"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* continued below */ - break; - } - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Marking contract %s with %s/%s as paid\n", - order_id, - session_id, - fulfillment_url); - - /* Wake everybody up who waits for this fulfillment_url and session_id */ - if ( (NULL != fulfillment_url) && - (NULL != session_id) ) - trigger_session_notification (hc, - session_id, - fulfillment_url); - /*Trigger webhook */ - /*Commented out until its purpose is defined - { - enum GNUNET_DB_QueryStatus qs; - json_t *jhook; - - jhook = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_object_incref ("contract_terms", - contract_terms), - GNUNET_JSON_pack_string ("order_id", - order_id) - ); - GNUNET_assert (NULL != jhook); - qs = TMH_trigger_webhook (hc->instance->settings.id, - "paid", - jhook); - json_decref (jhook); - if (qs < 0) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to init the webhook for contract %s with %s/%s as paid\n", - order_id, - session_id, - fulfillment_url); - } - }*/ - GNUNET_free (fulfillment_url); - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_bool ("refunded", - refunded)); -} - - -/* end of taler-merchant-httpd_post-orders-ID-paid.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-paid.h b/src/backend/taler-merchant-httpd_post-orders-ID-paid.h @@ -1,40 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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/> -*/ -/** - * @file taler-merchant-httpd_post-orders-ID-paid.h - * @brief headers for POST /orders/$ID/paid handler - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAID_H -#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAID_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Process proof of payment for a paid order. - * - * @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_post_orders_ID_paid (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -1,5308 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2026 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-httpd_post-orders-ID-pay.c - * @brief handling of POST /orders/$ID/pay requests - * @author Marcello Stanisci - * @author Christian Grothoff - * @author Florian Dold - */ -#include "taler/platform.h" -#include <gnunet/gnunet_common.h> -#include <gnunet/gnunet_db_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_time_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <stddef.h> -#include <stdint.h> -#include <string.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_error_codes.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_post-orders-ID-pay.h" -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler/taler_merchant_util.h" -#include "taler/taler_merchantdb_plugin.h" - -#ifdef HAVE_DONAU_DONAU_SERVICE_H -#include <donau/donau_service.h> -#include <donau/donau_util.h> -#include <donau/donau_json_lib.h> -#endif - -/** - * How often do we retry the (complex!) database transaction? - */ -#define MAX_RETRIES 5 - -/** - * Maximum number of coins that we allow per transaction. - * Note that the limit for each batch deposit request to - * the exchange is lower, so we may break a very large - * number of coins up into multiple smaller requests to - * the exchange. - */ -#define MAX_COIN_ALLOWED_COINS 1024 - -/** - * Maximum number of tokens that we allow as inputs per transaction - */ -#define MAX_TOKEN_ALLOWED_INPUTS 64 - -/** - * Maximum number of tokens that we allow as outputs per transaction - */ -#define MAX_TOKEN_ALLOWED_OUTPUTS 64 - -/** - * How often do we ask the exchange again about our - * KYC status? Very rarely, as if the user actively - * changes it, we should usually notice anyway. - */ -#define KYC_RETRY_FREQUENCY GNUNET_TIME_UNIT_WEEKS - -/** - * Information we keep for an individual call to the pay handler. - */ -struct PayContext; - - -/** - * Different phases of processing the /pay request. - */ -enum PayPhase -{ - /** - * Initial phase where the request is parsed. - */ - PP_PARSE_PAY = 0, - - /** - * Parse wallet data object from the pay request. - */ - PP_PARSE_WALLET_DATA, - - /** - * Check database state for the given order. - */ - PP_CHECK_CONTRACT, - - /** - * Validate provided tokens and token envelopes. - */ - PP_VALIDATE_TOKENS, - - /** - * Check if contract has been paid. - */ - PP_CONTRACT_PAID, - - /** - * Compute money pot changes. - */ - PP_COMPUTE_MONEY_POTS, - - /** - * Execute payment transaction. - */ - PP_PAY_TRANSACTION, - - /** - * Communicate with DONAU to generate a donation receipt from the donor BUDIs. - */ - PP_REQUEST_DONATION_RECEIPT, - - /** - * Process the donation receipt response from DONAU (save the donau_sigs to the db). - */ - PP_FINAL_OUTPUT_TOKEN_PROCESSING, - - /** - * Notify other processes about successful payment. - */ - PP_PAYMENT_NOTIFICATION, - - /** - * Create final success response. - */ - PP_SUCCESS_RESPONSE, - - /** - * Perform batch deposits with exchange(s). - */ - PP_BATCH_DEPOSITS, - - /** - * Return response in payment context. - */ - PP_RETURN_RESPONSE, - - /** - * An exchange denied a deposit, fail for - * legal reasons. - */ - PP_FAIL_LEGAL_REASONS, - - /** - * Return #MHD_YES to end processing. - */ - PP_END_YES, - - /** - * Return #MHD_NO to end processing. - */ - PP_END_NO -}; - - -/** - * Information kept during a pay request for each coin. - */ -struct DepositConfirmation -{ - - /** - * Reference to the main PayContext - */ - struct PayContext *pc; - - /** - * URL of the exchange that issued this coin. - */ - char *exchange_url; - - /** - * Details about the coin being deposited. - */ - struct TALER_EXCHANGE_CoinDepositDetail cdd; - - /** - * Fee charged by the exchange for the deposit operation of this coin. - */ - struct TALER_Amount deposit_fee; - - /** - * Fee charged by the exchange for the refund operation of this coin. - */ - struct TALER_Amount refund_fee; - - /** - * If a minimum age was required (i. e. pc->minimum_age is large enough), - * this is the signature of the minimum age (as a single uint8_t), using the - * private key to the corresponding age group. Might be all zeroes for no - * age attestation. - */ - struct TALER_AgeAttestationP minimum_age_sig; - - /** - * If a minimum age was required (i. e. pc->minimum_age is large enough), - * this is the age commitment (i. e. age mask and vector of EdDSA public - * keys, one per age group) that went into the mining of the coin. The - * SHA256 hash of the mask and the vector of public keys was bound to the - * key. - */ - struct TALER_AgeCommitment age_commitment; - - /** - * Age mask in the denomination that defines the age groups. Only - * applicable, if minimum age was required. - */ - struct TALER_AgeMask age_mask; - - /** - * Offset of this coin into the `dc` array of all coins in the - * @e pc. - */ - unsigned int index; - - /** - * true, if no field "age_commitment" was found in the JSON blob - */ - bool no_age_commitment; - - /** - * True, if no field "minimum_age_sig" was found in the JSON blob - */ - bool no_minimum_age_sig; - - /** - * true, if no field "h_age_commitment" was found in the JSON blob - */ - bool no_h_age_commitment; - - /** - * true if we found this coin in the database. - */ - bool found_in_db; - - /** - * true if we #deposit_paid_check() matched this coin in the database. - */ - bool matched_in_db; - -}; - -struct TokenUseConfirmation -{ - - /** - * Signature on the deposit request made using the token use private key. - */ - struct TALER_TokenUseSignatureP sig; - - /** - * Token use public key. This key was blindly signed by the merchant during - * the token issuance process. - */ - struct TALER_TokenUsePublicKeyP pub; - - /** - * Unblinded signature on the token use public key done by the merchant. - */ - struct TALER_TokenIssueSignature unblinded_sig; - - /** - * Hash of the token issue public key associated with this token. - * Note this is set in the validate_tokens phase. - */ - struct TALER_TokenIssuePublicKeyHashP h_issue; - - /** - * true if we found this token in the database. - */ - bool found_in_db; - -}; - - -/** - * Information about a token envelope. - */ -struct TokenEnvelope -{ - - /** - * Blinded token use public keys waiting to be signed. - */ - struct TALER_TokenEnvelope blinded_token; - -}; - - -/** - * (Blindly) signed token to be returned to the wallet. - */ -struct SignedOutputToken -{ - - /** - * Index of the output token that produced - * this blindly signed token. - */ - unsigned int output_index; - - /** - * Blinded token use public keys waiting to be signed. - */ - struct TALER_BlindedTokenIssueSignature sig; - - /** - * Hash of token issue public key. - */ - struct TALER_TokenIssuePublicKeyHashP h_issue; - -}; - - -/** - * Information kept during a pay request for each exchange. - */ -struct ExchangeGroup -{ - - /** - * Payment context this group is part of. - */ - struct PayContext *pc; - - /** - * Handle to the batch deposit operation we are performing for this - * exchange, NULL after the operation is done. - */ - struct TALER_EXCHANGE_PostBatchDepositHandle *bdh; - - /** - * Handle for operation to lookup /keys (and auditors) from - * the exchange used for this transaction; NULL if no operation is - * pending. - */ - struct TMH_EXCHANGES_KeysOperation *fo; - - /** - * URL of the exchange that issued this coin. Aliases - * the exchange URL of one of the coins, do not free! - */ - const char *exchange_url; - - /** - * Total deposit amount in this exchange group. - */ - struct TALER_Amount total; - - /** - * Wire fee that applies to this exchange for the - * given payment context's wire method. - */ - struct TALER_Amount wire_fee; - - /** - * true if we already tried a forced /keys download. - */ - bool tried_force_keys; - - /** - * Did this exchange deny the transaction for legal reasons? - */ - bool got_451; -}; - - -/** - * Information about donau, that can be fetched even - * if the merhchant doesn't support donau - */ -struct DonauData -{ - /** - * The user-selected Donau URL. - */ - char *donau_url; - - /** - * The donation year, as parsed from "year". - */ - uint64_t donation_year; - - /** - * The original BUDI key-pairs array from the donor - * to be used for the receipt creation. - */ - const json_t *budikeypairs; -}; - -/** - * Information we keep for an individual call to the /pay handler. - */ -struct PayContext -{ - - /** - * Stored in a DLL. - */ - struct PayContext *next; - - /** - * Stored in a DLL. - */ - struct PayContext *prev; - - /** - * MHD connection to return to - */ - struct MHD_Connection *connection; - - /** - * Details about the client's request. - */ - struct TMH_HandlerContext *hc; - - /** - * Transaction ID given in @e root. - */ - const char *order_id; - - /** - * Response to return, NULL if we don't have one yet. - */ - struct MHD_Response *response; - - /** - * Array with @e output_tokens_len signed tokens returned in - * the response to the wallet. - */ - struct SignedOutputToken *output_tokens; - - /** - * Number of output tokens to return in the response. - * Length of the @e output_tokens array. - */ - unsigned int output_tokens_len; - - /** - * Counter used to generate the output index in append_output_token_sig(). - */ - unsigned int output_index_gen; - - /** - * Counter used to generate the output index in append_output_token_sig(). - * - * Counts the generated tokens _within_ the current output_index_gen. - */ - unsigned int output_token_cnt; - - /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). - */ - unsigned int response_code; - - /** - * Payment processing phase we are in. - */ - enum PayPhase phase; - - /** - * #GNUNET_NO if the @e connection was not suspended, - * #GNUNET_YES if the @e connection was suspended, - * #GNUNET_SYSERR if @e connection was resumed to as - * part of #MH_force_pc_resume during shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * Results from the phase_parse_pay() - */ - struct - { - - /** - * Array with @e num_exchanges exchanges we are depositing - * coins into. - */ - struct ExchangeGroup **egs; - - /** - * Array with @e coins_cnt coins we are despositing. - */ - struct DepositConfirmation *dc; - - /** - * Array with @e tokens_cnt input tokens passed to this request. - */ - struct TokenUseConfirmation *tokens; - - /** - * Optional session id given in @e root. - * NULL if not given. - */ - char *session_id; - - /** - * Wallet data json object from the request. Containing additional - * wallet data such as the selected choice_index. - */ - const json_t *wallet_data; - - /** - * Number of coins this payment is made of. Length - * of the @e dc array. - */ - size_t coins_cnt; - - /** - * Number of input tokens passed to this request. Length - * of the @e tokens array. - */ - size_t tokens_cnt; - - /** - * Number of exchanges involved in the payment. Length - * of the @e eg array. - */ - unsigned int num_exchanges; - - } parse_pay; - - /** - * Results from the phase_wallet_data() - */ - struct - { - - /** - * Array with @e token_envelopes_cnt (blinded) token envelopes. - */ - struct TokenEnvelope *token_envelopes; - - /** - * Index of selected choice in the @e contract_terms choices array. - */ - int16_t choice_index; - - /** - * Number of token envelopes passed to this request. - * Length of the @e token_envelopes array. - */ - size_t token_envelopes_cnt; - - /** - * Hash of the canonicalized wallet data json object. - */ - struct GNUNET_HashCode h_wallet_data; - - /** - * Donau related information - */ - struct DonauData donau; - - /** - * Serial from the DB of the donau instance that we are using - */ - uint64_t donau_instance_serial; - -#ifdef HAVE_DONAU_DONAU_SERVICE_H - /** - * Number of the blinded key pairs @e bkps - */ - unsigned int num_bkps; - - /** - * Blinded key pairs received from the wallet - */ - struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps; - - /** - * The id of the charity as saved on the donau. - */ - uint64_t charity_id; - - /** - * Private key of the charity(related to the private key of the merchant). - */ - struct DONAU_CharityPrivateKeyP charity_priv; - - /** - * Maximum amount of donations that the charity can receive per year. - */ - struct TALER_Amount charity_max_per_year; - - /** - * Amount of donations that the charity has received so far this year. - */ - struct TALER_Amount charity_receipts_to_date; - - /** - * Donau keys, that we are using to get the information about the bkps. - */ - struct DONAU_Keys *donau_keys; - - /** - * Amount from BKPS - */ - struct TALER_Amount donation_amount; -#endif - - } parse_wallet_data; - - /** - * Results from the phase_check_contract() - */ - struct - { - - /** - * Hashed @e contract_terms. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Our contract (or NULL if not available). - */ - json_t *contract_terms_json; - - /** - * Parsed contract terms, NULL when parsing failed. - */ - struct TALER_MERCHANT_Contract *contract_terms; - - /** - * What wire method (of the @e mi) was selected by the wallet? - * Set in #phase_parse_pay(). - */ - struct TMH_WireMethod *wm; - - /** - * Set to the POS key, if applicable for this order. - */ - char *pos_key; - - /** - * Serial number of this order in the database (set once we did the lookup). - */ - uint64_t order_serial; - - /** - * Algorithm chosen for generating the confirmation code. - */ - enum TALER_MerchantConfirmationAlgorithm pos_alg; - - } check_contract; - - /** - * Results from the phase_validate_tokens() - */ - struct - { - - /** - * Maximum fee the merchant is willing to pay, from @e root. - * Note that IF the total fee of the exchange is higher, that is - * acceptable to the merchant if the customer is willing to - * pay the difference - * (i.e. amount - max_fee <= actual_amount - actual_fee). - */ - struct TALER_Amount max_fee; - - /** - * Amount from @e root. This is the amount the merchant expects - * to make, minus @e max_fee. - */ - struct TALER_Amount brutto; - - /** - * Index of the donau output in the list of tokens. - * Set to -1 if no donau output exists. - */ - int donau_output_index; - - } validate_tokens; - - - struct - { - /** - * Length of the @a pots and @a increments arrays. - */ - unsigned int num_pots; - - /** - * Serial IDs of money pots to increment. - */ - uint64_t *pots; - - /** - * Increment for the respective money pot. - */ - struct TALER_Amount *increments; - - /** - * True if the money pots have already been computed. - */ - bool pots_computed; - - } compute_money_pots; - - /** - * Results from the phase_execute_pay_transaction() - */ - struct - { - - /** - * Considering all the coins with the "found_in_db" flag - * set, what is the total amount we were so far paid on - * this contract? - */ - struct TALER_Amount total_paid; - - /** - * Considering all the coins with the "found_in_db" flag - * set, what is the total amount we had to pay in deposit - * fees so far on this contract? - */ - struct TALER_Amount total_fees_paid; - - /** - * Considering all the coins with the "found_in_db" flag - * set, what is the total amount we already refunded? - */ - struct TALER_Amount total_refunded; - - /** - * Number of coin deposits pending. - */ - unsigned int pending; - - /** - * How often have we retried the 'main' transaction? - */ - unsigned int retry_counter; - - /** - * Set to true if the deposit currency of a coin - * does not match the contract currency. - */ - bool deposit_currency_mismatch; - - /** - * Set to true if the database contains a (bogus) - * refund for a different currency. - */ - bool refund_currency_mismatch; - - } pay_transaction; - - /** - * Results from the phase_batch_deposits() - */ - struct - { - - /** - * Task called when the (suspended) processing for - * the /pay request times out. - * Happens when we don't get a response from the exchange. - */ - struct GNUNET_SCHEDULER_Task *timeout_task; - - /** - * Number of batch transactions pending. - */ - unsigned int pending_at_eg; - - /** - * Did any exchange deny a deposit for legal reasons? - */ - bool got_451; - - } batch_deposits; - -#ifdef HAVE_DONAU_DONAU_SERVICE_H - /** - * Struct for #phase_request_donation_receipt() - */ - struct - { - /** - * Handler of the donau request - */ - struct DONAU_BatchIssueReceiptHandle *birh; - - } donau_receipt; -#endif -}; - - -/** - * Head of active pay context DLL. - */ -static struct PayContext *pc_head; - -/** - * Tail of active pay context DLL. - */ -static struct PayContext *pc_tail; - - -void -TMH_force_pc_resume () -{ - for (struct PayContext *pc = pc_head; - NULL != pc; - pc = pc->next) - { - if (NULL != pc->batch_deposits.timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->batch_deposits.timeout_task); - pc->batch_deposits.timeout_task = NULL; - } - if (GNUNET_YES == pc->suspended) - { - pc->suspended = GNUNET_SYSERR; - MHD_resume_connection (pc->connection); - } - } -} - - -/** - * Resume payment processing. - * - * @param[in,out] pc payment process to resume - */ -static void -pay_resume (struct PayContext *pc) -{ - GNUNET_assert (GNUNET_YES == pc->suspended); - pc->suspended = GNUNET_NO; - MHD_resume_connection (pc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -/** - * Resume the given pay context and send the given response. - * Stores the response in the @a pc and signals MHD to resume - * the connection. Also ensures MHD runs immediately. - * - * @param pc payment context - * @param response_code response code to use - * @param response response data to send back - */ -static void -resume_pay_with_response (struct PayContext *pc, - unsigned int response_code, - struct MHD_Response *response) -{ - pc->response_code = response_code; - pc->response = response; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming /pay handling. HTTP status for our reply is %u.\n", - response_code); - for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) - { - struct ExchangeGroup *eg = pc->parse_pay.egs[i]; - - if (NULL != eg->fo) - { - TMH_EXCHANGES_keys4exchange_cancel (eg->fo); - eg->fo = NULL; - pc->batch_deposits.pending_at_eg--; - } - if (NULL != eg->bdh) - { - TALER_EXCHANGE_post_batch_deposit_cancel (eg->bdh); - eg->bdh = NULL; - pc->batch_deposits.pending_at_eg--; - } - } - GNUNET_assert (0 == pc->batch_deposits.pending_at_eg); - if (NULL != pc->batch_deposits.timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->batch_deposits.timeout_task); - pc->batch_deposits.timeout_task = NULL; - } - pc->phase = PP_RETURN_RESPONSE; - pay_resume (pc); -} - - -/** - * Resume payment processing with an error. - * - * @param pc operation to resume - * @param ec taler error code to return - * @param msg human readable error message - */ -static void -resume_pay_with_error (struct PayContext *pc, - enum TALER_ErrorCode ec, - const char *msg) -{ - resume_pay_with_response ( - pc, - TALER_ErrorCode_get_http_status_safe (ec), - TALER_MHD_make_error (ec, - msg)); -} - - -/** - * Conclude payment processing for @a pc with the - * given @a res MHD status code. - * - * @param[in,out] pc payment context for final state transition - * @param res MHD return code to end with - */ -static void -pay_end (struct PayContext *pc, - MHD_RESULT res) -{ - pc->phase = (MHD_YES == res) - ? PP_END_YES - : PP_END_NO; -} - - -/** - * Return response stored in @a pc. - * - * @param[in,out] pc payment context we are processing - */ -static void -phase_return_response (struct PayContext *pc) -{ - GNUNET_assert (0 != pc->response_code); - /* We are *done* processing the request, just queue the response (!) */ - if (UINT_MAX == pc->response_code) - { - GNUNET_break (0); - pay_end (pc, - MHD_NO); /* hard error */ - return; - } - pay_end (pc, - MHD_queue_response (pc->connection, - pc->response_code, - pc->response)); -} - - -/** - * Return a response indicating failure for legal reasons. - * - * @param[in,out] pc payment context we are processing - */ -static void -phase_fail_for_legal_reasons (struct PayContext *pc) -{ - json_t *exchanges; - - GNUNET_assert (0 == pc->pay_transaction.pending); - GNUNET_assert (pc->batch_deposits.got_451); - exchanges = json_array (); - GNUNET_assert (NULL != exchanges); - for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) - { - struct ExchangeGroup *eg = pc->parse_pay.egs[i]; - - GNUNET_assert (NULL == eg->fo); - GNUNET_assert (NULL == eg->bdh); - if (! eg->got_451) - continue; - GNUNET_assert ( - 0 == - json_array_append_new ( - exchanges, - json_string (eg->exchange_url))); - } - pay_end (pc, - TALER_MHD_REPLY_JSON_PACK ( - pc->connection, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED), - GNUNET_JSON_pack_array_steal ("exchange_base_urls", - exchanges))); -} - - -/** - * Do database transaction for a completed batch deposit. - * - * @param eg group that completed - * @param dr response from the server - * @return transaction status - */ -static enum GNUNET_DB_QueryStatus -batch_deposit_transaction (const struct ExchangeGroup *eg, - const struct - TALER_EXCHANGE_PostBatchDepositResponse *dr) -{ - const struct PayContext *pc = eg->pc; - enum GNUNET_DB_QueryStatus qs; - struct TALER_Amount total_without_fees; - uint64_t b_dep_serial; - uint32_t off = 0; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &total_without_fees)); - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - struct TALER_Amount amount_without_fees; - - /* might want to group deposits by batch more explicitly ... */ - if (0 != strcmp (eg->exchange_url, - dc->exchange_url)) - continue; - if (dc->found_in_db) - continue; - GNUNET_assert (0 <= - TALER_amount_subtract (&amount_without_fees, - &dc->cdd.amount, - &dc->deposit_fee)); - GNUNET_assert (0 <= - TALER_amount_add (&total_without_fees, - &total_without_fees, - &amount_without_fees)); - } - qs = TMH_db->insert_deposit_confirmation ( - TMH_db->cls, - pc->hc->instance->settings.id, - dr->details.ok.deposit_timestamp, - &pc->check_contract.h_contract_terms, - eg->exchange_url, - pc->check_contract.contract_terms->wire_deadline, - &total_without_fees, - &eg->wire_fee, - &pc->check_contract.wm->h_wire, - dr->details.ok.exchange_sig, - dr->details.ok.exchange_pub, - &b_dep_serial); - if (qs <= 0) - return qs; /* Entire batch already known or failure, we're done */ - - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - /* might want to group deposits by batch more explicitly ... */ - if (0 != strcmp (eg->exchange_url, - dc->exchange_url)) - continue; - if (dc->found_in_db) - continue; - /* FIXME-#9457: We might want to check if the order was fully paid concurrently - by some other wallet here, and if so, issue an auto-refund. Right now, - it is possible to over-pay if two wallets literally make a concurrent - payment, as the earlier check for 'paid' is not in the same transaction - scope as this 'insert' operation. */ - qs = TMH_db->insert_deposit ( - TMH_db->cls, - off++, /* might want to group deposits by batch more explicitly ... */ - b_dep_serial, - &dc->cdd.coin_pub, - &dc->cdd.coin_sig, - &dc->cdd.amount, - &dc->deposit_fee, - &dc->refund_fee, - GNUNET_TIME_absolute_add ( - pc->check_contract.contract_terms->wire_deadline.abs_time, - GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES))); - if (qs < 0) - return qs; - GNUNET_break (qs > 0); - } - return qs; -} - - -/** - * Handle case where the batch deposit completed - * with a status of #MHD_HTTP_OK. - * - * @param eg group that completed - * @param dr response from the server - */ -static void -handle_batch_deposit_ok (struct ExchangeGroup *eg, - const struct TALER_EXCHANGE_PostBatchDepositResponse * - dr) -{ - struct PayContext *pc = eg->pc; - enum GNUNET_DB_QueryStatus qs - = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - - /* store result to DB */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Storing successful payment %s (%s) at instance `%s'\n", - pc->hc->infix, - GNUNET_h2s (&pc->check_contract.h_contract_terms.hash), - pc->hc->instance->settings.id); - for (unsigned int r = 0; r<MAX_RETRIES; r++) - { - TMH_db->preflight (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "batch-deposit-insert-confirmation")) - { - resume_pay_with_response ( - pc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_GENERIC_DB_START_FAILED), - TMH_pack_exchange_reply (&dr->hr))); - return; - } - qs = batch_deposit_transaction (eg, - dr); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - continue; - } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "batch_deposit_transaction"); - TMH_db->rollback (TMH_db->cls); - return; - } - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - continue; - } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "insert_deposit"); - } - break; /* DB transaction succeeded */ - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - resume_pay_with_error (pc, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "insert_deposit"); - return; - } - - /* Transaction is done, mark affected coins as complete as well. */ - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - if (0 != strcmp (eg->exchange_url, - pc->parse_pay.dc[i].exchange_url)) - continue; - if (dc->found_in_db) - continue; - dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */ - pc->pay_transaction.pending--; - } -} - - -/** - * Notify taler-merchant-kyccheck that we got a KYC - * rule violation notification and should start to - * check our KYC status. - * - * @param eg exchange group we were notified for - */ -static void -notify_kyc_required (const struct ExchangeGroup *eg) -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) - }; - char *hws; - char *extra; - - hws = GNUNET_STRINGS_data_to_string_alloc ( - &eg->pc->check_contract.contract_terms->h_wire, - sizeof (eg->pc->check_contract.contract_terms->h_wire)); - GNUNET_asprintf (&extra, - "%s %s", - hws, - eg->exchange_url); - GNUNET_free (hws); - TMH_db->event_notify (TMH_db->cls, - &es, - extra, - strlen (extra) + 1); - GNUNET_free (extra); -} - - -/** - * Callback to handle a batch deposit permission's response. - * - * @param cls a `struct ExchangeGroup` - * @param dr HTTP response code details - */ -static void -batch_deposit_cb ( - void *cls, - const struct TALER_EXCHANGE_PostBatchDepositResponse *dr) -{ - struct ExchangeGroup *eg = cls; - struct PayContext *pc = eg->pc; - - eg->bdh = NULL; - pc->batch_deposits.pending_at_eg--; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Batch deposit completed with status %u\n", - dr->hr.http_status); - GNUNET_assert (GNUNET_YES == pc->suspended); - switch (dr->hr.http_status) - { - case MHD_HTTP_OK: - handle_batch_deposit_ok (eg, - dr); - if ( (GNUNET_YES == pc->suspended) && - (0 == pc->batch_deposits.pending_at_eg) ) - { - pc->phase = PP_COMPUTE_MONEY_POTS; - pay_resume (pc); - } - return; - case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: - notify_kyc_required (eg); - eg->got_451 = true; - pc->batch_deposits.got_451 = true; - /* update pc->pay_transaction.pending */ - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - if (0 != strcmp (eg->exchange_url, - pc->parse_pay.dc[i].exchange_url)) - continue; - if (dc->found_in_db) - continue; - pc->pay_transaction.pending--; - } - if (0 == pc->batch_deposits.pending_at_eg) - { - pc->phase = PP_COMPUTE_MONEY_POTS; - pay_resume (pc); - } - return; - default: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Deposit operation failed with HTTP code %u/%d\n", - dr->hr.http_status, - (int) dr->hr.ec); - /* Transaction failed */ - if (5 == dr->hr.http_status / 100) - { - /* internal server error at exchange */ - resume_pay_with_response (pc, - MHD_HTTP_BAD_GATEWAY, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), - TMH_pack_exchange_reply (&dr->hr))); - return; - } - if (NULL == dr->hr.reply) - { - /* We can't do anything meaningful here, the exchange did something wrong */ - resume_pay_with_response ( - pc, - MHD_HTTP_BAD_GATEWAY, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED), - TMH_pack_exchange_reply (&dr->hr))); - return; - } - - /* Forward error, adding the "exchange_url" for which the - error was being generated */ - if (TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS == dr->hr.ec) - { - resume_pay_with_response ( - pc, - MHD_HTTP_CONFLICT, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS), - TMH_pack_exchange_reply (&dr->hr), - GNUNET_JSON_pack_string ("exchange_url", - eg->exchange_url))); - return; - } - resume_pay_with_response ( - pc, - MHD_HTTP_BAD_GATEWAY, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), - TMH_pack_exchange_reply (&dr->hr), - GNUNET_JSON_pack_string ("exchange_url", - eg->exchange_url))); - return; - } /* end switch */ -} - - -/** - * Force re-downloading keys for @a eg. - * - * @param[in,out] eg group to re-download keys for - */ -static void -force_keys (struct ExchangeGroup *eg); - - -/** - * Function called with the result of our exchange keys lookup. - * - * @param cls the `struct ExchangeGroup` - * @param keys the keys of the exchange - * @param exchange representation of the exchange - */ -static void -process_pay_with_keys ( - void *cls, - struct TALER_EXCHANGE_Keys *keys, - struct TMH_Exchange *exchange) -{ - struct ExchangeGroup *eg = cls; - struct PayContext *pc = eg->pc; - struct TMH_HandlerContext *hc = pc->hc; - unsigned int group_size; - struct TALER_Amount max_amount; - enum TMH_ExchangeStatus es; - - eg->fo = NULL; - pc->batch_deposits.pending_at_eg--; - GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing payment with keys from exchange %s\n", - eg->exchange_url); - GNUNET_assert (GNUNET_YES == pc->suspended); - if (NULL == keys) - { - GNUNET_break_op (0); - resume_pay_with_error ( - pc, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, - NULL); - return; - } - if (! TMH_EXCHANGES_is_below_limit (keys, - TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION, - &eg->total)) - { - GNUNET_break_op (0); - resume_pay_with_error ( - pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, - eg->exchange_url); - return; - } - - max_amount = eg->total; - es = TMH_exchange_check_debit ( - pc->hc->instance->settings.id, - exchange, - pc->check_contract.wm, - &max_amount); - if ( (TMH_ES_OK != es) && - (TMH_ES_RETRY_OK != es) ) - { - if (eg->tried_force_keys || - (0 == (TMH_ES_RETRY_OK & es)) ) - { - GNUNET_break_op (0); - resume_pay_with_error ( - pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED, - NULL); - return; - } - force_keys (eg); - return; - } - if (-1 == - TALER_amount_cmp (&max_amount, - &eg->total)) - { - /* max_amount < eg->total */ - GNUNET_break_op (0); - resume_pay_with_error ( - pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, - eg->exchange_url); - return; - } - - if (GNUNET_OK != - TMH_EXCHANGES_lookup_wire_fee (exchange, - pc->check_contract.wm->wire_method, - &eg->wire_fee)) - { - if (eg->tried_force_keys) - { - GNUNET_break_op (0); - resume_pay_with_error ( - pc, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED, - pc->check_contract.wm->wire_method); - return; - } - force_keys (eg); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got wire data for %s\n", - eg->exchange_url); - - /* Initiate /batch-deposit operation for all coins of - the current exchange (!) */ - group_size = 0; - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - const struct TALER_EXCHANGE_DenomPublicKey *denom_details; - bool is_age_restricted_denom = false; - - if (0 != strcmp (eg->exchange_url, - pc->parse_pay.dc[i].exchange_url)) - continue; - if (dc->found_in_db) - continue; - - denom_details - = TALER_EXCHANGE_get_denomination_key_by_hash (keys, - &dc->cdd.h_denom_pub); - if (NULL == denom_details) - { - if (eg->tried_force_keys) - { - GNUNET_break_op (0); - resume_pay_with_response ( - pc, - MHD_HTTP_BAD_REQUEST, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &dc->cdd.h_denom_pub), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ( - "exchange_keys", - TALER_EXCHANGE_keys_to_json (keys))))); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Missing denomination %s from exchange %s, updating keys\n", - GNUNET_h2s (&dc->cdd.h_denom_pub.hash), - eg->exchange_url); - force_keys (eg); - return; - } - dc->deposit_fee = denom_details->fees.deposit; - dc->refund_fee = denom_details->fees.refund; - - if (GNUNET_TIME_absolute_is_past ( - denom_details->expire_deposit.abs_time)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Denomination key offered by client has expired for deposits\n"); - resume_pay_with_response ( - pc, - MHD_HTTP_GONE, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &denom_details->h_key))); - return; - } - - /* Now that we have the details about the denomination, we can verify age - * restriction requirements, if applicable. Note that denominations with an - * age_mask equal to zero always pass the age verification. */ - is_age_restricted_denom = (0 != denom_details->key.age_mask.bits); - - if (is_age_restricted_denom && - (0 < pc->check_contract.contract_terms->minimum_age)) - { - /* Minimum age given and restricted coin provided: We need to verify the - * minimum age */ - unsigned int code = 0; - - if (dc->no_age_commitment) - { - GNUNET_break_op (0); - code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING; - goto AGE_FAIL; - } - dc->age_commitment.mask = denom_details->key.age_mask; - if (((int) (dc->age_commitment.num + 1)) != - __builtin_popcount (dc->age_commitment.mask.bits)) - { - GNUNET_break_op (0); - code = - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH; - goto AGE_FAIL; - } - if (GNUNET_OK != - TALER_age_commitment_verify ( - &dc->age_commitment, - pc->check_contract.contract_terms->minimum_age, - &dc->minimum_age_sig)) - code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; -AGE_FAIL: - if (0 < code) - { - GNUNET_break_op (0); - TALER_age_commitment_free (&dc->age_commitment); - resume_pay_with_response ( - pc, - MHD_HTTP_BAD_REQUEST, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec (code), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &denom_details->h_key))); - return; - } - - /* Age restriction successfully verified! - * Calculate the hash of the age commitment. */ - TALER_age_commitment_hash (&dc->age_commitment, - &dc->cdd.h_age_commitment); - TALER_age_commitment_free (&dc->age_commitment); - } - else if (is_age_restricted_denom && - dc->no_h_age_commitment) - { - /* The contract did not ask for a minimum_age but the client paid - * with a coin that has age restriction enabled. We lack the hash - * of the age commitment in this case in order to verify the coin - * and to deposit it with the exchange. */ - GNUNET_break_op (0); - resume_pay_with_response ( - pc, - MHD_HTTP_BAD_REQUEST, - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING), - GNUNET_JSON_pack_data_auto ("h_denom_pub", - &denom_details->h_key))); - return; - } - group_size++; - } - - if (0 == group_size) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Group size zero, %u batch transactions remain pending\n", - pc->batch_deposits.pending_at_eg); - if (0 == pc->batch_deposits.pending_at_eg) - { - pc->phase = PP_COMPUTE_MONEY_POTS; - pay_resume (pc); - return; - } - return; - } - if (group_size > TALER_MAX_COINS) - group_size = TALER_MAX_COINS; - { - struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; - struct TALER_EXCHANGE_DepositContractDetail dcd = { - .wire_deadline = pc->check_contract.contract_terms->wire_deadline, - .merchant_payto_uri = pc->check_contract.wm->payto_uri, - .extra_wire_subject_metadata = pc->check_contract.wm-> - extra_wire_subject_metadata, - .wire_salt = pc->check_contract.wm->wire_salt, - .h_contract_terms = pc->check_contract.h_contract_terms, - .wallet_data_hash = pc->parse_wallet_data.h_wallet_data, - .wallet_timestamp = pc->check_contract.contract_terms->timestamp, - .merchant_pub = hc->instance->merchant_pub, - .refund_deadline = pc->check_contract.contract_terms->refund_deadline - }; - enum TALER_ErrorCode ec; - size_t off = 0; - - TALER_merchant_contract_sign (&pc->check_contract.h_contract_terms, - &pc->hc->instance->merchant_priv, - &dcd.merchant_sig); - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - if (dc->found_in_db) - continue; - if (0 != strcmp (dc->exchange_url, - eg->exchange_url)) - continue; - cdds[off++] = dc->cdd; - if (off >= group_size) - break; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Initiating batch deposit with %u coins\n", - group_size); - /* Note: the coin signatures over the wallet_data_hash are - checked inside of this call */ - eg->bdh = TALER_EXCHANGE_post_batch_deposit_create ( - TMH_curl_ctx, - eg->exchange_url, - keys, - &dcd, - group_size, - cdds, - &ec); - if (NULL == eg->bdh) - { - /* Signature was invalid or some other constraint was not satisfied. If - the exchange was unavailable, we'd get that information in the - callback. */ - GNUNET_break_op (0); - resume_pay_with_response ( - pc, - TALER_ErrorCode_get_http_status_safe (ec), - TALER_MHD_MAKE_JSON_PACK ( - TALER_JSON_pack_ec (ec), - GNUNET_JSON_pack_string ("exchange_url", - eg->exchange_url))); - return; - } - pc->batch_deposits.pending_at_eg++; - if (TMH_force_audit) - TALER_EXCHANGE_post_batch_deposit_force_dc (eg->bdh); - TALER_EXCHANGE_post_batch_deposit_start (eg->bdh, - &batch_deposit_cb, - eg); - } -} - - -static void -force_keys (struct ExchangeGroup *eg) -{ - struct PayContext *pc = eg->pc; - - eg->tried_force_keys = true; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Forcing /keys download (once)\n"); - eg->fo = TMH_EXCHANGES_keys4exchange ( - eg->exchange_url, - true, - &process_pay_with_keys, - eg); - if (NULL == eg->fo) - { - GNUNET_break_op (0); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, - eg->exchange_url); - return; - } - pc->batch_deposits.pending_at_eg++; -} - - -/** - * Handle a timeout for the processing of the pay request. - * - * @param cls our `struct PayContext` - */ -static void -handle_pay_timeout (void *cls) -{ - struct PayContext *pc = cls; - - pc->batch_deposits.timeout_task = NULL; - GNUNET_assert (GNUNET_YES == pc->suspended); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming pay with error after timeout\n"); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, - NULL); -} - - -/** - * Compute the timeout for a /pay request based on the number of coins - * involved. - * - * @param num_coins number of coins - * @returns timeout for the /pay request - */ -static struct GNUNET_TIME_Relative -get_pay_timeout (unsigned int num_coins) -{ - struct GNUNET_TIME_Relative t; - - /* FIXME-Performance-Optimization: Do some benchmarking to come up with a - * better timeout. We've increased this value so the wallet integration - * test passes again on my (Florian) machine. - */ - t = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, - 15 * (1 + (num_coins / 5))); - - return t; -} - - -/** - * Start batch deposits for all exchanges involved - * in this payment. - * - * @param[in,out] pc payment context we are processing - */ -static void -phase_batch_deposits (struct PayContext *pc) -{ - for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) - { - struct ExchangeGroup *eg = pc->parse_pay.egs[i]; - bool have_coins = false; - - for (size_t j = 0; j<pc->parse_pay.coins_cnt; j++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[j]; - - if (0 != strcmp (eg->exchange_url, - dc->exchange_url)) - continue; - if (dc->found_in_db) - continue; - have_coins = true; - break; - } - if (! have_coins) - continue; /* no coins left to deposit at this exchange */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Getting /keys for %s\n", - eg->exchange_url); - eg->fo = TMH_EXCHANGES_keys4exchange ( - eg->exchange_url, - false, - &process_pay_with_keys, - eg); - if (NULL == eg->fo) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, - eg->exchange_url)); - return; - } - pc->batch_deposits.pending_at_eg++; - } - if (0 == pc->batch_deposits.pending_at_eg) - { - pc->phase = PP_COMPUTE_MONEY_POTS; - pay_resume (pc); - return; - } - /* Suspend while we interact with the exchange */ - MHD_suspend_connection (pc->connection); - pc->suspended = GNUNET_YES; - GNUNET_assert (NULL == pc->batch_deposits.timeout_task); - pc->batch_deposits.timeout_task - = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->parse_pay.coins_cnt), - &handle_pay_timeout, - pc); -} - - -/** - * Build JSON array of blindly signed token envelopes, - * to be used in the response to the wallet. - * - * @param[in,out] pc payment context to use - */ -static json_t * -build_token_sigs (struct PayContext *pc) -{ - json_t *token_sigs; - - if (0 == pc->output_tokens_len) - return NULL; - token_sigs = json_array (); - GNUNET_assert (NULL != token_sigs); - for (unsigned int i = 0; i < pc->output_tokens_len; i++) - { - GNUNET_assert (0 == - json_array_append_new ( - token_sigs, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_blinded_sig ( - "blind_sig", - pc->output_tokens[i].sig.signature) - ))); - } - return token_sigs; -} - - -/** - * Generate response (payment successful) - * - * @param[in,out] pc payment context where the payment was successful - */ -static void -phase_success_response (struct PayContext *pc) -{ - struct TALER_MerchantSignatureP sig; - char *pos_confirmation; - - /* Sign on our end (as the payment did go through, even if it may - have been refunded already) */ - TALER_merchant_pay_sign (&pc->check_contract.h_contract_terms, - &pc->hc->instance->merchant_priv, - &sig); - /* Build the response */ - pos_confirmation = (NULL == pc->check_contract.pos_key) - ? NULL - : TALER_build_pos_confirmation (pc->check_contract.pos_key, - pc->check_contract.pos_alg, - &pc->validate_tokens.brutto, - pc->check_contract.contract_terms->timestamp - ); - pay_end (pc, - TALER_MHD_REPLY_JSON_PACK ( - pc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("pos_confirmation", - pos_confirmation)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ("token_sigs", - build_token_sigs (pc))), - GNUNET_JSON_pack_data_auto ("sig", - &sig))); - GNUNET_free (pos_confirmation); -} - - -/** - * Use database to notify other clients about the - * payment being completed. - * - * @param[in,out] pc context to trigger notification for - */ -static void -phase_payment_notification (struct PayContext *pc) -{ - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), - .merchant_pub = pc->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about payment of order %s\n", - pc->order_id); - GNUNET_CRYPTO_hash (pc->order_id, - strlen (pc->order_id), - &pay_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &pay_eh.header, - NULL, - 0); - } - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), - .merchant_pub = pc->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about status change of order %s\n", - pc->order_id); - GNUNET_CRYPTO_hash (pc->order_id, - strlen (pc->order_id), - &pay_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &pay_eh.header, - NULL, - 0); - } - if ( (NULL != pc->parse_pay.session_id) && - (NULL != pc->check_contract.contract_terms->fulfillment_url) ) - { - struct TMH_SessionEventP session_eh = { - .header.size = htons (sizeof (session_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), - .merchant_pub = pc->hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about session change to %s for %s\n", - pc->parse_pay.session_id, - pc->check_contract.contract_terms->fulfillment_url); - GNUNET_CRYPTO_hash (pc->parse_pay.session_id, - strlen (pc->parse_pay.session_id), - &session_eh.h_session_id); - GNUNET_CRYPTO_hash (pc->check_contract.contract_terms->fulfillment_url, - strlen (pc->check_contract.contract_terms-> - fulfillment_url), - &session_eh.h_fulfillment_url); - TMH_db->event_notify (TMH_db->cls, - &session_eh.header, - NULL, - 0); - } - pc->phase = PP_SUCCESS_RESPONSE; -} - - -/** - * Phase to write all outputs to our database so we do - * not re-request them in case the client re-plays the - * request. - * - * @param[in,out] pc payment context - */ -static void -phase_final_output_token_processing (struct PayContext *pc) -{ - if (0 == pc->output_tokens_len) - { - pc->phase++; - return; - } - for (unsigned int retry = 0; retry < MAX_RETRIES; retry++) - { - enum GNUNET_DB_QueryStatus qs; - - TMH_db->preflight (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "insert_order_blinded_sigs")) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "start insert_order_blinded_sigs_failed"); - pc->phase++; - return; - } -#ifdef HAVE_DONAU_DONAU_SERVICE_H - if (pc->parse_wallet_data.num_bkps > 0) - { - qs = TMH_db->update_donau_instance_receipts_amount ( - TMH_db->cls, - &pc->parse_wallet_data.donau_instance_serial, - &pc->parse_wallet_data.charity_receipts_to_date); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_db->rollback (TMH_db->cls); - GNUNET_break (0); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - continue; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* weird for an update */ - GNUNET_break (0); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } -#endif - for (unsigned int i = 0; - i < pc->output_tokens_len; - i++) - { - qs = TMH_db->insert_order_blinded_sigs ( - TMH_db->cls, - pc->order_id, - i, - &pc->output_tokens[i].h_issue.hash, - pc->output_tokens[i].sig.signature); - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_db->rollback (TMH_db->cls); - pc->phase++; - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - goto OUTER; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* weird for an update */ - GNUNET_break (0); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } /* for i */ - qs = TMH_db->commit (TMH_db->cls); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_db->rollback (TMH_db->cls); - pc->phase++; - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - continue; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - pc->phase++; - return; /* success */ - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - pc->phase++; - return; /* success */ - } - GNUNET_break (0); - pc->phase++; - return; /* strange */ -OUTER: - } /* for retry */ - TMH_db->rollback (TMH_db->cls); - pc->phase++; - /* We continue anyway, as there is not much we can - do here: the Donau *did* issue us the receipts; - also, we'll eventually ask the Donau for the - balance and get the correct one. Plus, we were - paid by the client, so it's technically all still - OK. If the request fails anyway, the wallet will - most likely replay the request and then hopefully - we will succeed the next time */ -} - - -#ifdef HAVE_DONAU_DONAU_SERVICE_H - -/** - * Add donation receipt outputs to the output_tokens. - * - * Note that under the current (odd, bad) libdonau - * API *we* are responsible for freeing blinded_sigs, - * so we truly own that array! - * - * @param[in,out] pc payment context - * @param num_blinded_sigs number of signatures received - * @param blinded_sigs blinded signatures from Donau - * @return #GNUNET_OK on success, - * #GNUNET_SYSERR on failure (state machine was - * in that case already advanced) - */ -static enum GNUNET_GenericReturnValue -add_donation_receipt_outputs ( - struct PayContext *pc, - size_t num_blinded_sigs, - struct DONAU_BlindedDonationUnitSignature *blinded_sigs) -{ - int donau_output_index = pc->validate_tokens.donau_output_index; - - GNUNET_assert (pc->parse_wallet_data.num_bkps == - num_blinded_sigs); - - GNUNET_assert (donau_output_index >= 0); - - for (unsigned int i = 0; i<pc->output_tokens_len; i++) - { - struct SignedOutputToken *sot - = &pc->output_tokens[i]; - - /* Only look at actual donau tokens. */ - if (sot->output_index != donau_output_index) - continue; - - sot->sig.signature = GNUNET_CRYPTO_blind_sig_incref (blinded_sigs[i]. - blinded_sig); - sot->h_issue.hash = pc->parse_wallet_data.bkps[i].h_donation_unit_pub.hash; - } - return GNUNET_OK; -} - - -/** - * Callback to handle the result of a batch issue request. - * - * @param cls our `struct PayContext` - * @param resp the response from Donau - */ -static void -merchant_donau_issue_receipt_cb ( - void *cls, - const struct DONAU_BatchIssueResponse *resp) -{ - struct PayContext *pc = cls; - /* Donau replies asynchronously, so we expect the PayContext - * to be suspended. */ - GNUNET_assert (GNUNET_YES == pc->suspended); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Donau responded with status=%u, ec=%u", - resp->hr.http_status, - resp->hr.ec); - switch (resp->hr.http_status) - { - case 0: - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Donau batch issue request from merchant-httpd failed (http_status==0)"); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_GENERIC_DONAU_INVALID_RESPONSE, - resp->hr.hint); - return; - case MHD_HTTP_OK: - case MHD_HTTP_CREATED: - if (TALER_EC_NONE != resp->hr.ec) - { - /* Most probably, it is just some small flaw from - * donau so no point in failing, yet we have to display it */ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Donau signalled error %u despite HTTP %u", - resp->hr.ec, - resp->hr.http_status); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Donau accepted donation receipts with total_issued=%s", - TALER_amount2s (&resp->details.ok.issued_amount)); - if (GNUNET_OK != - add_donation_receipt_outputs (pc, - resp->details.ok.num_blinded_sigs, - resp->details.ok.blinded_sigs)) - return; /* state machine was already advanced */ - pc->phase = PP_FINAL_OUTPUT_TOKEN_PROCESSING; - pay_resume (pc); - return; - - case MHD_HTTP_BAD_REQUEST: - case MHD_HTTP_FORBIDDEN: - case MHD_HTTP_NOT_FOUND: - case MHD_HTTP_INTERNAL_SERVER_ERROR: - default: /* make sure that everything except 200/201 will end up here*/ - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Donau replied with HTTP %u (ec=%u)", - resp->hr.http_status, - resp->hr.ec); - resume_pay_with_error (pc, - TALER_EC_MERCHANT_GENERIC_DONAU_INVALID_RESPONSE, - resp->hr.hint); - return; - } -} - - -/** - * Parse a bkp encoded in JSON. - * - * @param[out] bkp where to return the result - * @param bkp_key_obj json to parse - * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if @a bkp_key_obj - * is malformed. - */ -static enum GNUNET_GenericReturnValue -merchant_parse_json_bkp (struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkp, - const json_t *bkp_key_obj) -{ - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("h_donation_unit_pub", - &bkp->h_donation_unit_pub), - DONAU_JSON_spec_blinded_donation_identifier ("blinded_udi", - &bkp->blinded_udi), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (bkp_key_obj, - spec, - NULL, - NULL)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Generate a donation signature for the bkp and charity. - * - * @param[in,out] pc payment context containing the charity and bkps - */ -static void -phase_request_donation_receipt (struct PayContext *pc) -{ - if ( (NULL == pc->parse_wallet_data.donau.donau_url) || - (0 == pc->parse_wallet_data.num_bkps) ) - { - pc->phase++; - return; - } - pc->donau_receipt.birh = - DONAU_charity_issue_receipt ( - TMH_curl_ctx, - pc->parse_wallet_data.donau.donau_url, - &pc->parse_wallet_data.charity_priv, - pc->parse_wallet_data.charity_id, - pc->parse_wallet_data.donau.donation_year, - pc->parse_wallet_data.num_bkps, - pc->parse_wallet_data.bkps, - &merchant_donau_issue_receipt_cb, - pc); - if (NULL == pc->donau_receipt.birh) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to create Donau receipt request"); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR, - "Donau request creation error")); - return; - } - MHD_suspend_connection (pc->connection); - pc->suspended = GNUNET_YES; -} - - -#endif - - -/** - * Increment the money pot @a pot_id in @a pc by @a increment. - * - * @param[in,out] pc context to update - * @param pot_id money pot to increment - * @param increment amount to add - */ -static void -increment_pot (struct PayContext *pc, - uint64_t pot_id, - const struct TALER_Amount *increment) -{ - for (unsigned int i = 0; i<pc->compute_money_pots.num_pots; i++) - { - if (pot_id == pc->compute_money_pots.pots[i]) - { - struct TALER_Amount *p; - - p = &pc->compute_money_pots.increments[i]; - GNUNET_assert (0 <= - TALER_amount_add (p, - p, - increment)); - return; - } - } - GNUNET_array_append (pc->compute_money_pots.pots, - pc->compute_money_pots.num_pots, - pot_id); - pc->compute_money_pots.num_pots--; /* do not increment twice... */ - GNUNET_array_append (pc->compute_money_pots.increments, - pc->compute_money_pots.num_pots, - *increment); -} - - -/** - * Compute the total changes to money pots in preparation - * for the #PP_PAY_TRANSACTION phase. - * - * @param[in,out] pc payment context to transact - */ -static void -phase_compute_money_pots (struct PayContext *pc) -{ - const struct TALER_MERCHANT_Contract *contract - = pc->check_contract.contract_terms; - struct TALER_Amount assigned; - - if (0 == pc->parse_pay.coins_cnt) - { - /* Did not pay with any coins, so no currency/amount involved, - hence no money pot update possible. */ - pc->phase++; - return; - } - - if (pc->compute_money_pots.pots_computed) - { - pc->phase++; - return; - } - /* reset, in case this phase is run a 2nd time */ - GNUNET_free (pc->compute_money_pots.pots); - GNUNET_free (pc->compute_money_pots.increments); - pc->compute_money_pots.num_pots = 0; - - TALER_amount_set_zero (pc->parse_pay.dc[0].cdd.amount.currency, - &assigned); - GNUNET_assert (NULL != contract); - for (size_t i = 0; i<contract->products_len; i++) - { - const struct TALER_MERCHANT_ProductSold *product - = &contract->products[i]; - const struct TALER_Amount *price = NULL; - - /* find price in the right currency */ - for (unsigned int j = 0; j<product->prices_length; j++) - { - if (GNUNET_OK == - TALER_amount_cmp_currency (&assigned, - &product->prices[j])) - { - price = &product->prices[j]; - break; - } - } - if (NULL == price) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Product `%s' has no price given in `%s'.\n", - product->product_id, - assigned.currency); - continue; - } - if (0 != product->product_money_pot) - { - GNUNET_assert (0 <= - TALER_amount_add (&assigned, - &assigned, - price)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Contributing to product money pot %llu increment of %s\n", - (unsigned long long) product->product_money_pot, - TALER_amount2s (price)); - increment_pot (pc, - product->product_money_pot, - price); - } - } - - { - /* Compute what is left from the order total and account for that. - Also sanity-check and handle the case where the overall order - is below that of the sum of the products. */ - struct TALER_Amount left; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order brutto is %s\n", - TALER_amount2s (&pc->validate_tokens.brutto)); - if (0 > - TALER_amount_subtract (&left, - &pc->validate_tokens.brutto, - &assigned)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Total order brutto amount below sum from products, skipping per-product money pots\n"); - GNUNET_free (pc->compute_money_pots.pots); - GNUNET_free (pc->compute_money_pots.increments); - pc->compute_money_pots.num_pots = 0; - left = pc->validate_tokens.brutto; - } - - if ( (! TALER_amount_is_zero (&left)) && - (0 != contract->default_money_pot) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Computing money pot %llu increment as %s\n", - (unsigned long long) contract->default_money_pot, - TALER_amount2s (&left)); - increment_pot (pc, - contract->default_money_pot, - &left); - } - } - pc->compute_money_pots.pots_computed = true; - pc->phase++; -} - - -/** - * Function called with information about a coin that was deposited. - * - * @param cls closure - * @param exchange_url exchange where @a coin_pub was deposited - * @param coin_pub public key of the coin - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param refund_fee fee the exchange will charge for refunding this coin - */ -static void -check_coin_paid (void *cls, - const char *exchange_url, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee) -{ - struct PayContext *pc = cls; - - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - if (dc->found_in_db) - continue; /* processed earlier, skip "expensive" memcmp() */ - /* Get matching coin from results*/ - if ( (0 != GNUNET_memcmp (coin_pub, - &dc->cdd.coin_pub)) || - (0 != - strcmp (exchange_url, - dc->exchange_url)) || - (GNUNET_OK != - TALER_amount_cmp_currency (amount_with_fee, - &dc->cdd.amount)) || - (0 != TALER_amount_cmp (amount_with_fee, - &dc->cdd.amount)) ) - continue; /* does not match, skip */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Deposit of coin `%s' already in our DB.\n", - TALER_B2S (coin_pub)); - if ( (GNUNET_OK != - TALER_amount_cmp_currency (&pc->pay_transaction.total_paid, - amount_with_fee)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&pc->pay_transaction.total_fees_paid, - deposit_fee)) ) - { - GNUNET_break_op (0); - pc->pay_transaction.deposit_currency_mismatch = true; - break; - } - GNUNET_assert (0 <= - TALER_amount_add (&pc->pay_transaction.total_paid, - &pc->pay_transaction.total_paid, - amount_with_fee)); - GNUNET_assert (0 <= - TALER_amount_add (&pc->pay_transaction.total_fees_paid, - &pc->pay_transaction.total_fees_paid, - deposit_fee)); - dc->deposit_fee = *deposit_fee; - dc->refund_fee = *refund_fee; - dc->cdd.amount = *amount_with_fee; - dc->found_in_db = true; - pc->pay_transaction.pending--; - } -} - - -/** - * Function called with information about a refund. Check if this coin was - * claimed by the wallet for the transaction, and if so add the refunded - * amount to the pc's "total_refunded" amount. - * - * @param cls closure with a `struct PayContext` - * @param coin_pub public coin from which the refund comes from - * @param refund_amount refund amount which is being taken from @a coin_pub - */ -static void -check_coin_refunded (void *cls, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *refund_amount) -{ - struct PayContext *pc = cls; - - /* We look at refunds here that apply to the coins - that the customer is currently trying to pay us with. - - Such refunds are not "normal" refunds, but abort-pay refunds, which are - given in the case that the wallet aborts the payment. - In the case the wallet then decides to complete the payment *after* doing - an abort-pay refund (an unusual but possible case), we need - to make sure that existing refunds are accounted for. */ - - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - /* Get matching coins from results. */ - if (0 != GNUNET_memcmp (coin_pub, - &dc->cdd.coin_pub)) - continue; - if (GNUNET_OK != - TALER_amount_cmp_currency (&pc->pay_transaction.total_refunded, - refund_amount)) - { - GNUNET_break (0); - pc->pay_transaction.refund_currency_mismatch = true; - break; - } - GNUNET_assert (0 <= - TALER_amount_add (&pc->pay_transaction.total_refunded, - &pc->pay_transaction.total_refunded, - refund_amount)); - break; - } -} - - -/** - * Check whether the amount paid is sufficient to cover the price. - * - * @param pc payment context to check - * @return true if the payment is sufficient, false if it is - * insufficient - */ -static bool -check_payment_sufficient (struct PayContext *pc) -{ - struct TALER_Amount acc_fee; - struct TALER_Amount acc_amount; - struct TALER_Amount final_amount; - struct TALER_Amount total_wire_fee; - struct TALER_Amount total_needed; - - if (0 == pc->parse_pay.coins_cnt) - return TALER_amount_is_zero (&pc->validate_tokens.brutto); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &total_wire_fee)); - for (unsigned int i = 0; i < pc->parse_pay.num_exchanges; i++) - { - if (GNUNET_OK != - TALER_amount_cmp_currency (&total_wire_fee, - &pc->parse_pay.egs[i]->wire_fee)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - total_wire_fee.currency)); - return false; - } - if (0 > - TALER_amount_add (&total_wire_fee, - &total_wire_fee, - &pc->parse_pay.egs[i]->wire_fee)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, - "could not add exchange wire fee to total")); - return false; - } - } - - /** - * This loops calculates what are the deposit fee / total - * amount with fee / and wire fee, for all the coins. - */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &acc_fee)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &acc_amount)); - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - GNUNET_assert (dc->found_in_db); - if ( (GNUNET_OK != - TALER_amount_cmp_currency (&acc_fee, - &dc->deposit_fee)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&acc_amount, - &dc->cdd.amount)) ) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - dc->deposit_fee.currency)); - return false; - } - if ( (0 > - TALER_amount_add (&acc_fee, - &dc->deposit_fee, - &acc_fee)) || - (0 > - TALER_amount_add (&acc_amount, - &dc->cdd.amount, - &acc_amount)) ) - { - GNUNET_break (0); - /* Overflow in these amounts? Very strange. */ - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts")); - return false; - } - if (1 == - TALER_amount_cmp (&dc->deposit_fee, - &dc->cdd.amount)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, - "Deposit fees exceed coin's contribution")); - return false; - } - } /* end deposit loop */ - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Amount received from wallet: %s\n", - TALER_amount2s (&acc_amount)); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Deposit fee for all coins: %s\n", - TALER_amount2s (&acc_fee)); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Total wire fee: %s\n", - TALER_amount2s (&total_wire_fee)); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Deposit fee limit for merchant: %s\n", - TALER_amount2s (&pc->validate_tokens.max_fee)); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Total refunded amount: %s\n", - TALER_amount2s (&pc->pay_transaction.total_refunded)); - - /* Now compare exchange wire fee compared to what we are willing to pay */ - if (GNUNET_YES != - TALER_amount_cmp_currency (&total_wire_fee, - &acc_fee)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - total_wire_fee.currency)); - return false; - } - - /* add wire fee to the total fees */ - if (0 > - TALER_amount_add (&acc_fee, - &acc_fee, - &total_wire_fee)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts")); - return false; - } - if (-1 == TALER_amount_cmp (&pc->validate_tokens.max_fee, - &acc_fee)) - { - /** - * Sum of fees of *all* the different exchanges of all the coins are - * higher than the fixed limit that the merchant is willing to pay. The - * difference must be paid by the customer. - */ - struct TALER_Amount excess_fee; - - /* compute fee amount to be covered by customer */ - GNUNET_assert (TALER_AAR_RESULT_POSITIVE == - TALER_amount_subtract (&excess_fee, - &acc_fee, - &pc->validate_tokens.max_fee)); - /* add that to the total */ - if (0 > - TALER_amount_add (&total_needed, - &excess_fee, - &pc->validate_tokens.brutto)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts")); - return false; - } - } - else - { - /* Fees are fully covered by the merchant, all we require - is that the total payment is not below the contract's amount */ - total_needed = pc->validate_tokens.brutto; - } - - /* Do not count refunds towards the payment */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subtracting total refunds from paid amount: %s\n", - TALER_amount2s (&pc->pay_transaction.total_refunded)); - if (0 > - TALER_amount_subtract (&final_amount, - &acc_amount, - &pc->pay_transaction.total_refunded)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS, - "refunded amount exceeds total payments")); - return false; - } - - if (-1 == TALER_amount_cmp (&final_amount, - &total_needed)) - { - /* acc_amount < total_needed */ - if (-1 < TALER_amount_cmp (&acc_amount, - &total_needed)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_PAYMENT_REQUIRED, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED, - "contract not paid up due to refunds")); - return false; - } - if (-1 < TALER_amount_cmp (&acc_amount, - &pc->validate_tokens.brutto)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES, - "contract not paid up due to fees (client may have calculated them badly)")); - return false; - } - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, - "payment insufficient")); - return false; - } - return true; -} - - -/** - * Execute the DB transaction. If required (from - * soft/serialization errors), the transaction can be - * restarted here. - * - * @param[in,out] pc payment context to transact - */ -static void -phase_execute_pay_transaction (struct PayContext *pc) -{ - struct TMH_HandlerContext *hc = pc->hc; - const char *instance_id = hc->instance->settings.id; - - if (pc->batch_deposits.got_451) - { - pc->phase = PP_FAIL_LEGAL_REASONS; - return; - } - /* Avoid re-trying transactions on soft errors forever! */ - if (pc->pay_transaction.retry_counter++ > MAX_RETRIES) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL)); - return; - } - - /* Initialize some amount accumulators - (used in check_coin_paid(), check_coin_refunded() - and check_payment_sufficient()). */ - GNUNET_break (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &pc->pay_transaction.total_paid)); - GNUNET_break (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &pc->pay_transaction.total_fees_paid)); - GNUNET_break (GNUNET_OK == - TALER_amount_set_zero (pc->validate_tokens.brutto.currency, - &pc->pay_transaction.total_refunded)); - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - pc->parse_pay.dc[i].found_in_db = false; - pc->pay_transaction.pending = pc->parse_pay.coins_cnt; - - /* First, try to see if we have all we need already done */ - TMH_db->preflight (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "run pay")) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL)); - return; - } - - for (size_t i = 0; i<pc->parse_pay.tokens_cnt; i++) - { - struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; - enum GNUNET_DB_QueryStatus qs; - - /* Insert used token into database, the unique constraint will - case an error if this token was used before. */ - qs = TMH_db->insert_spent_token (TMH_db->cls, - &pc->check_contract.h_contract_terms, - &tuc->h_issue, - &tuc->pub, - &tuc->sig, - &tuc->unblinded_sig); - - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - return; /* do it again */ - case GNUNET_DB_STATUS_HARD_ERROR: - /* Always report on hard error as well to enable diagnostics */ - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert used token")); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* UNIQUE constraint violation, meaning this token was already used. */ - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID, - NULL)); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* Good, proceed! */ - break; - } - } /* for all tokens */ - - { - enum GNUNET_DB_QueryStatus qs; - - /* Check if some of these coins already succeeded for _this_ contract. */ - qs = TMH_db->lookup_deposits (TMH_db->cls, - instance_id, - &pc->check_contract.h_contract_terms, - &check_coin_paid, - pc); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return; /* do it again */ - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup deposits")); - return; - } - if (pc->pay_transaction.deposit_currency_mismatch) - { - TMH_db->rollback (TMH_db->cls); - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - pc->validate_tokens.brutto.currency)); - return; - } - } - - { - enum GNUNET_DB_QueryStatus qs; - - /* Check if we refunded some of the coins */ - qs = TMH_db->lookup_refunds (TMH_db->cls, - instance_id, - &pc->check_contract.h_contract_terms, - &check_coin_refunded, - pc); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return; /* do it again */ - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup refunds")); - return; - } - if (pc->pay_transaction.refund_currency_mismatch) - { - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "refund currency in database does not match order currency")); - return; - } - } - - /* Check if there are coins that still need to be processed */ - if (0 != pc->pay_transaction.pending) - { - /* we made no DB changes, so we can just rollback */ - TMH_db->rollback (TMH_db->cls); - /* Ok, we need to first go to the network to process more coins. - We that interaction in *tiny* transactions (hence the rollback - above). */ - pc->phase = PP_BATCH_DEPOSITS; - return; - } - - /* 0 == pc->pay_transaction.pending: all coins processed, let's see if that was enough */ - if (! check_payment_sufficient (pc)) - { - /* check_payment_sufficient() will have queued an error already. - We need to still abort the transaction. */ - TMH_db->rollback (TMH_db->cls); - return; - } - /* Payment succeeded, save in database */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' (%s) was fully paid\n", - pc->order_id, - GNUNET_h2s (&pc->check_contract.h_contract_terms.hash)); - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->mark_contract_paid (TMH_db->cls, - instance_id, - &pc->check_contract.h_contract_terms, - pc->parse_pay.session_id, - pc->parse_wallet_data.choice_index); - if (qs < 0) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return; /* do it again */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "mark contract paid")); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Marked contract paid returned %d\n", - (int) qs); - - if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && - (0 < pc->compute_money_pots.num_pots) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Incrementing %u money pots by %s\n", - pc->compute_money_pots.num_pots, - TALER_amount2s (&pc->compute_money_pots.increments[0])); - qs = TMH_db->increment_money_pots (TMH_db->cls, - instance_id, - pc->compute_money_pots.num_pots, - pc->compute_money_pots.pots, - pc->compute_money_pots.increments); - switch (qs) - { - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - return; /* do it again */ - case GNUNET_DB_STATUS_HARD_ERROR: - /* Always report on hard error as well to enable diagnostics */ - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "increment_money_pots")); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* strange */ - GNUNET_break (0); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* Good, proceed! */ - break; - } - - } - - - } - - - { - const struct TALER_MERCHANT_ContractChoice *choice = - &pc->check_contract.contract_terms->details.v1 - .choices[pc->parse_wallet_data.choice_index]; - - for (size_t i = 0; i<pc->output_tokens_len; i++) - { - unsigned int output_index; - enum TALER_MERCHANT_ContractOutputType type; - - output_index = pc->output_tokens[i].output_index; - GNUNET_assert (output_index < choice->outputs_len); - type = choice->outputs[output_index].type; - - switch (type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - /* Well, good luck getting here */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "invalid output type")); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - /* We skip output tokens of donation receipts here, as they are handled in the - * phase_final_output_token_processing() callback from donau */ - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - struct SignedOutputToken *output = - &pc->output_tokens[i]; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->insert_issued_token (TMH_db->cls, - &pc->check_contract.h_contract_terms, - &output->h_issue, - &output->sig); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_db->rollback (TMH_db->cls); - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert output token")); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* Serialization failure, retry */ - TMH_db->rollback (TMH_db->cls); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* UNIQUE constraint violation, meaning this token was already used. */ - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "duplicate output token")); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - break; - } - } - } - - TMH_notify_order_change (hc->instance, - TMH_OSF_CLAIMED | TMH_OSF_PAID, - pc->check_contract.contract_terms->timestamp, - pc->check_contract.order_serial); - { - enum GNUNET_DB_QueryStatus qs; - json_t *jhook; - - jhook = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_object_incref ("contract_terms", - pc->check_contract.contract_terms_json), - GNUNET_JSON_pack_string ("order_id", - pc->order_id) - ); - GNUNET_assert (NULL != jhook); - qs = TMH_trigger_webhook (pc->hc->instance->settings.id, - "pay", - jhook); - json_decref (jhook); - if (qs < 0) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return; /* do it again */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "failed to trigger webhooks")); - return; - } - } - { - enum GNUNET_DB_QueryStatus qs; - - /* Now commit! */ - qs = TMH_db->commit (TMH_db->cls); - if (0 > qs) - { - /* commit failed */ - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return; /* do it again */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL)); - return; - } - } - pc->phase++; -} - - -/** - * Ensures that the expected number of tokens for a @e key - * are provided as inputs and have valid signatures. - * - * @param[in,out] pc payment context we are processing - * @param family family the tokens should be from - * @param index number of the input we are handling - * @param expected_num number of tokens expected - * @return #GNUNET_YES on success - */ -static enum GNUNET_GenericReturnValue -find_valid_input_tokens ( - struct PayContext *pc, - const struct TALER_MERCHANT_ContractTokenFamily *family, - unsigned int index, - unsigned int expected_num) -{ - unsigned int num_validated = 0; - struct GNUNET_TIME_Timestamp now - = GNUNET_TIME_timestamp_get (); - const struct TALER_MERCHANT_ContractTokenFamilyKey *kig = NULL; - - for (unsigned int j = 0; j < expected_num; j++) - { - struct TokenUseConfirmation *tuc - = &pc->parse_pay.tokens[index + j]; - const struct TALER_MERCHANT_ContractTokenFamilyKey *key = NULL; - - for (unsigned int i = 0; i<family->keys_len; i++) - { - const struct TALER_MERCHANT_ContractTokenFamilyKey *ki - = &family->keys[i]; - - if (0 == - GNUNET_memcmp (&ki->pub.public_key->pub_key_hash, - &tuc->h_issue.hash)) - { - if (GNUNET_TIME_timestamp_cmp (ki->valid_after, - >, - now) || - GNUNET_TIME_timestamp_cmp (ki->valid_before, - <=, - now)) - { - /* We have a match, but not in the current validity period */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Public key %s currently not valid\n", - GNUNET_h2s (&ki->pub.public_key->pub_key_hash)); - kig = ki; - continue; - } - key = ki; - break; - } - } - if (NULL == key) - { - if (NULL != kig) - { - char start_str[128]; - char end_str[128]; - char emsg[350]; - - GNUNET_snprintf (start_str, - sizeof (start_str), - "%s", - GNUNET_STRINGS_timestamp_to_string (kig->valid_after)); - GNUNET_snprintf (end_str, - sizeof (end_str), - "%s", - GNUNET_STRINGS_timestamp_to_string (kig->valid_before)) - ; - /* FIXME: use more specific EC */ - GNUNET_snprintf (emsg, - sizeof (emsg), - "Token is only valid from %s to %s", - start_str, - end_str); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_GONE, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, - emsg)); - return GNUNET_NO; - } - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Input token supplied for public key %s that is not acceptable\n", - GNUNET_h2s (&tuc->h_issue.hash)); - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN, - NULL)); - return GNUNET_NO; - } - if (GNUNET_OK != - TALER_token_issue_verify (&tuc->pub, - &key->pub, - &tuc->unblinded_sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Input token for public key with valid_after " - "`%s' has invalid issue signature\n", - GNUNET_TIME_timestamp2s (key->valid_after)); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID, - NULL)); - return GNUNET_NO; - } - - if (GNUNET_OK != - TALER_wallet_token_use_verify (&pc->check_contract.h_contract_terms, - &pc->parse_wallet_data.h_wallet_data, - &tuc->pub, - &tuc->sig)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Input token for public key with valid_before " - "`%s' has invalid use signature\n", - GNUNET_TIME_timestamp2s (key->valid_before)); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID, - NULL)); - return GNUNET_NO; - } - - num_validated++; - } - - if (num_validated != expected_num) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Expected %d tokens for family %s, but found %d\n", - expected_num, - family->slug, - num_validated); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH, - NULL)); - return GNUNET_NO; - } - return GNUNET_YES; -} - - -/** - * Check if an output token of the given @a tfk is mandatory, or if - * wallets are allowed to simply not support it and still proceed. - * - * @param tfk token family kind to check - * @return true if such outputs are mandatory and wallets must supply - * the corresponding blinded input - */ -/* FIXME: this function belongs into a lower-level lib! */ -static bool -test_tfk_mandatory (enum TALER_MERCHANTDB_TokenFamilyKind tfk) -{ - switch (tfk) - { - case TALER_MERCHANTDB_TFK_Discount: - return false; - case TALER_MERCHANTDB_TFK_Subscription: - return true; - } - GNUNET_break (0); - return false; -} - - -/** - * Sign the tokens provided by the wallet for a particular @a key. - * - * @param[in,out] pc reference for payment we are processing - * @param key token family data - * @param priv private key to use to sign with - * @param mandatory true if the token must exist, if false - * and the client did not provide an envelope, that's OK and - * we just also skimp on the signature - * @param index offset in the token envelope array (from other families) - * @param expected_num number of tokens of this type that we should create - * @return #GNUNET_NO on failure - * #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -sign_token_envelopes ( - struct PayContext *pc, - const struct TALER_MERCHANT_ContractTokenFamilyKey *key, - const struct TALER_TokenIssuePrivateKey *priv, - bool mandatory, - unsigned int index, - unsigned int expected_num) -{ - unsigned int num_signed = 0; - - for (unsigned int j = 0; j<expected_num; j++) - { - unsigned int pos = index + j; - const struct TokenEnvelope *env - = &pc->parse_wallet_data.token_envelopes[pos]; - struct SignedOutputToken *output - = &pc->output_tokens[pos]; - - if ( (pos >= pc->parse_wallet_data.token_envelopes_cnt) || - (pos >= pc->output_tokens_len) ) - { - GNUNET_assert (0); /* this should not happen */ - return GNUNET_NO; - } - if (NULL == env->blinded_token.blinded_pub) - { - if (! mandatory) - continue; - - /* mandatory token families require a token envelope. */ - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Token envelope for mandatory token family missing")); - return GNUNET_NO; - } - TALER_token_issue_sign (priv, - &env->blinded_token, - &output->sig); - output->h_issue.hash - = key->pub.public_key->pub_key_hash; - num_signed++; - } - - if (mandatory && - (num_signed != expected_num) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Expected %d token envelopes for public key with valid_after " - "'%s', but found %d\n", - expected_num, - GNUNET_TIME_timestamp2s (key->valid_after), - num_signed); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH, - NULL)); - return GNUNET_NO; - } - - return GNUNET_OK; -} - - -/** - * Find the family entry for the family of the given @a slug - * in @a pc. - * - * @param[in] pc payment context to search - * @param slug slug to search for - * @return NULL if @a slug was not found - */ -static const struct TALER_MERCHANT_ContractTokenFamily * -find_family (const struct PayContext *pc, - const char *slug) -{ - for (unsigned int i = 0; - i < pc->check_contract.contract_terms->details.v1.token_authorities_len; - i++) - { - const struct TALER_MERCHANT_ContractTokenFamily *tfi - = &pc->check_contract.contract_terms->details.v1.token_authorities[i]; - - if (0 == strcmp (tfi->slug, - slug)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Token family %s found with %u keys\n", - slug, - tfi->keys_len); - return tfi; - } - } - return NULL; -} - - -/** - * Handle contract output of type TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN. - * Looks up the token family, loads the matching private key, - * and signs the corresponding token envelopes from the wallet. - * - * @param pc context for the pay request - * @param output contract output we need to process - * @param output_index index of this output in the contract's outputs array - * @return #GNUNET_OK on success, #GNUNET_NO if an error was encountered - */ -static enum GNUNET_GenericReturnValue -handle_output_token (struct PayContext *pc, - const struct TALER_MERCHANT_ContractOutput *output, - unsigned int output_index) -{ - const struct TALER_MERCHANT_ContractTokenFamily *family; - struct TALER_MERCHANT_ContractTokenFamilyKey *key; - struct TALER_MERCHANTDB_TokenFamilyKeyDetails details; - enum GNUNET_DB_QueryStatus qs; - bool mandatory; - - /* Locate token family in the contract. - This should ever fail as this invariant should - have been checked when the contract was created. */ - family = find_family (pc, - output->details.token.token_family_slug); - if (NULL == family) - { - /* This "should never happen", so treat it as an internal error */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "token family not found in order")); - return GNUNET_SYSERR; - } - - /* Check the key_index field from the output. */ - if (output->details.token.key_index >= family->keys_len) - { - /* Also "should never happen", contract was presumably validated on insert */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "key index invalid for token family")); - return GNUNET_SYSERR; - } - - /* Pick the correct key inside that family. */ - key = &family->keys[output->details.token.key_index]; - - /* Fetch the private key from the DB for the merchant instance and - * this particular family/time interval. */ - qs = TMH_db->lookup_token_family_key ( - TMH_db->cls, - pc->hc->instance->settings.id, - family->slug, - pc->check_contract.contract_terms->timestamp, - pc->check_contract.contract_terms->pay_deadline, - &details); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Database error looking up token-family key for %s\n", - family->slug); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL)); - return GNUNET_NO; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log ( - GNUNET_ERROR_TYPE_ERROR, - "Token-family key for %s not found at [%llu,%llu]\n", - family->slug, - (unsigned long long) - pc->check_contract.contract_terms->timestamp.abs_time.abs_value_us, - (unsigned long long) - pc->check_contract.contract_terms->pay_deadline.abs_time.abs_value_us - ); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN, - family->slug)); - return GNUNET_NO; - - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - GNUNET_assert (NULL != details.priv.private_key); - GNUNET_free (details.token_family.slug); - GNUNET_free (details.token_family.name); - GNUNET_free (details.token_family.description); - json_decref (details.token_family.description_i18n); - GNUNET_CRYPTO_blind_sign_pub_decref (details.pub.public_key); - GNUNET_free (details.token_family.cipher_spec); - - /* Depending on the token family, decide if the token envelope - * is mandatory or optional. (Simplified logic here: adapt as needed.) */ - mandatory = test_tfk_mandatory (details.token_family.kind); - /* Actually sign the number of token envelopes specified in 'count'. - * 'output_index' is the offset into the parse_wallet_data arrays. */ - if (GNUNET_OK != - sign_token_envelopes (pc, - key, - &details.priv, - mandatory, - output_index, - output->details.token.count)) - { - /* sign_token_envelopes() already queued up an error via pay_end() */ - GNUNET_break_op (0); - return GNUNET_NO; - } - GNUNET_CRYPTO_blind_sign_priv_decref (details.priv.private_key); - return GNUNET_OK; -} - - -#ifdef HAVE_DONAU_DONAU_SERVICE_H -/** - * Handle checks for contract output of type TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT. - * For now, this does nothing and simply returns #GNUNET_OK. - * - * @param pc context for the pay request - * @param output the contract output describing the donation receipt requirement - * @return #GNUNET_OK on success, - * #GNUNET_NO if an error was already queued - */ -static enum GNUNET_GenericReturnValue -handle_output_donation_receipt ( - struct PayContext *pc, - const struct TALER_MERCHANT_ContractOutput *output) -{ - enum GNUNET_GenericReturnValue ret; - - ret = DONAU_get_donation_amount_from_bkps ( - pc->parse_wallet_data.donau_keys, - pc->parse_wallet_data.bkps, - pc->parse_wallet_data.num_bkps, - pc->parse_wallet_data.donau.donation_year, - &pc->parse_wallet_data.donation_amount); - switch (ret) - { - case GNUNET_SYSERR: - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL)); - return GNUNET_NO; - case GNUNET_NO: - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inconsistent bkps / donau keys")); - return GNUNET_NO; - case GNUNET_OK: - break; - } - - if (GNUNET_OK != - TALER_amount_cmp_currency (&pc->parse_wallet_data.donation_amount, - &output->details.donation_receipt.amount)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - output->details.donation_receipt.amount.currency)); - return GNUNET_NO; - } - - if (0 != - TALER_amount_cmp (&pc->parse_wallet_data.donation_amount, - &output->details.donation_receipt.amount)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Wallet amount: %s\n", - TALER_amount2s (&pc->parse_wallet_data.donation_amount)); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Donation receipt amount: %s\n", - TALER_amount2s (&output->details.donation_receipt.amount)); - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DONATION_AMOUNT_MISMATCH, - "donation amount mismatch")); - return GNUNET_NO; - } - { - struct TALER_Amount receipts_to_date; - - if (0 > - TALER_amount_add (&receipts_to_date, - &pc->parse_wallet_data.charity_receipts_to_date, - &pc->parse_wallet_data.donation_amount)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "adding donation amount")); - return GNUNET_NO; - } - - if (1 == - TALER_amount_cmp (&receipts_to_date, - &pc->parse_wallet_data.charity_max_per_year)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DONATION_AMOUNT_MISMATCH, - "donation limit exceeded")); - return GNUNET_NO; - } - pc->parse_wallet_data.charity_receipts_to_date = receipts_to_date; - } - return GNUNET_OK; -} - - -#endif /* HAVE_DONAU_DONAU_SERVICE_H */ - - -/** - * Count tokens produced by an output. - * - * @param pc pay context - * @param output output to consider - * @returns number of output tokens - */ -static unsigned int -count_output_tokens (const struct PayContext *pc, - const struct TALER_MERCHANT_ContractOutput *output) -{ - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - return output->details.token.count; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: -#ifdef HAVE_DONAU_DONAU_SERVICE_H - return pc->parse_wallet_data.num_bkps; -#else - return 0; -#endif - } - /* Not reached. */ - GNUNET_assert (0); -} - - -/** - * Validate tokens and token envelopes. First, we check if all tokens listed - * in the 'inputs' array of the selected choice are present in the 'tokens' - * array of the request. Then, we validate the signatures of each provided - * token. - * - * @param[in,out] pc context we use to handle the payment - */ -static void -phase_validate_tokens (struct PayContext *pc) -{ - /* We haven't seen a donau output yet. */ - pc->validate_tokens.donau_output_index = -1; - - switch (pc->check_contract.contract_terms->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - /* No tokens to validate */ - pc->phase = PP_COMPUTE_MONEY_POTS; - pc->validate_tokens.max_fee - = pc->check_contract.contract_terms->details.v0.max_fee; - pc->validate_tokens.brutto - = pc->check_contract.contract_terms->details.v0.brutto; - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - { - const struct TALER_MERCHANT_ContractChoice *selected - = &pc->check_contract.contract_terms->details.v1.choices[ - pc->parse_wallet_data.choice_index]; - unsigned int output_off; - unsigned int cnt; - - pc->validate_tokens.max_fee = selected->max_fee; - pc->validate_tokens.brutto = selected->amount; - - for (unsigned int i = 0; i<selected->inputs_len; i++) - { - const struct TALER_MERCHANT_ContractInput *input - = &selected->inputs[i]; - const struct TALER_MERCHANT_ContractTokenFamily *family; - - switch (input->type) - { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "input token type not valid")); - return; -#if FUTURE - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_COIN: - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_MERCHANT_GENERIC_FEATURE_NOT_AVAILABLE, - "token type not yet supported")); - return; -#endif - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - family = find_family (pc, - input->details.token.token_family_slug); - if (NULL == family) - { - /* this should never happen, since the choices and - token families are validated on insert. */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "token family not found in order")); - return; - } - if (GNUNET_NO == - find_valid_input_tokens (pc, - family, - i, - input->details.token.count)) - { - /* Error is already scheduled from find_valid_input_token. */ - return; - } - } - } - - /* calculate pc->output_tokens_len */ - output_off = 0; - for (unsigned int i = 0; i<selected->outputs_len; i++) - { - const struct TALER_MERCHANT_ContractOutput *output - = &selected->outputs[i]; - - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - cnt = output->details.token.count; - if (output_off + cnt < output_off) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "output token counter overflow")); - return; - } - output_off += cnt; - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - /* check that this output type appears at most once */ - if (pc->validate_tokens.donau_output_index >= 0) - { - /* This should have been prevented when the - contract was initially created */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "two donau output sets in same contract")); - return; - } - pc->validate_tokens.donau_output_index = i; -#ifdef HAVE_DONAU_DONAU_SERVICE_H - if (output_off + pc->parse_wallet_data.num_bkps < output_off) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "output token counter overflow")); - return; - } - output_off += pc->parse_wallet_data.num_bkps; -#endif - break; - } - } - - - pc->output_tokens_len = output_off; - pc->output_tokens - = GNUNET_new_array (pc->output_tokens_len, - struct SignedOutputToken); - - /* calculate pc->output_tokens[].output_index */ - output_off = 0; - for (unsigned int i = 0; i<selected->outputs_len; i++) - { - const struct TALER_MERCHANT_ContractOutput *output - = &selected->outputs[i]; - cnt = count_output_tokens (pc, - output); - for (unsigned int j = 0; j<cnt; j++) - pc->output_tokens[output_off + j].output_index = i; - output_off += cnt; - } - - /* compute non-donau outputs */ - output_off = 0; - for (unsigned int i = 0; i<selected->outputs_len; i++) - { - const struct TALER_MERCHANT_ContractOutput *output - = &selected->outputs[i]; - - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - cnt = output->details.token.count; - GNUNET_assert (output_off + cnt - <= pc->output_tokens_len); - if (GNUNET_OK != - handle_output_token (pc, - output, - output_off)) - { - /* Error is already scheduled from handle_output_token. */ - return; - } - output_off += cnt; - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: -#ifndef HAVE_DONAU_DONAU_SERVICE_H - /* We checked at parse time, and - wallet didn't want donau, so OK! */ - return; -#else - if ( (0 != pc->parse_wallet_data.num_bkps) && - (GNUNET_OK != - handle_output_donation_receipt (pc, - output)) ) - { - /* Error is already scheduled from handle_output_donation_receipt. */ - return; - } - output_off += pc->parse_wallet_data.num_bkps; - continue; -#endif - } /* switch on output token */ - } /* for all output token types */ - } /* case contract v1 */ - break; - } /* switch on contract type */ - - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - const struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - if (GNUNET_OK != - TALER_amount_cmp_currency (&dc->cdd.amount, - &pc->validate_tokens.brutto)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - pc->validate_tokens.brutto.currency)); - return; - } - } - - pc->phase = PP_COMPUTE_MONEY_POTS; -} - - -/** - * Function called with information about a coin that was deposited. - * Checks if this coin is in our list of deposits as well. - * - * @param cls closure with our `struct PayContext *` - * @param deposit_serial which deposit operation is this about - * @param exchange_url URL of the exchange that issued the coin - * @param h_wire hash of merchant's wire details - * @param deposit_timestamp when was the deposit made - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param coin_pub public key of the coin - */ -static void -deposit_paid_check ( - void *cls, - uint64_t deposit_serial, - const char *exchange_url, - const struct TALER_MerchantWireHashP *h_wire, - struct GNUNET_TIME_Timestamp deposit_timestamp, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub) -{ - struct PayContext *pc = cls; - - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dci = &pc->parse_pay.dc[i]; - - if ( (0 == - GNUNET_memcmp (&dci->cdd.coin_pub, - coin_pub)) && - (0 == - strcmp (dci->exchange_url, - exchange_url)) && - (GNUNET_YES == - TALER_amount_cmp_currency (&dci->cdd.amount, - amount_with_fee)) && - (0 == - TALER_amount_cmp (&dci->cdd.amount, - amount_with_fee)) ) - { - dci->matched_in_db = true; - break; - } - } -} - - -/** - * Function called with information about a token that was spent. - * FIXME: Replace this with a more specific function for this cb - * - * @param cls closure with `struct PayContext *` - * @param spent_token_serial "serial" of the spent token unused - * @param h_contract_terms hash of the contract terms unused - * @param h_issue_pub hash of the token issue public key unused - * @param use_pub public key of the token - * @param use_sig signature of the token - * @param issue_sig signature of the token issue - */ -static void -input_tokens_paid_check ( - void *cls, - uint64_t spent_token_serial, - const struct TALER_PrivateContractHashP *h_contract_terms, - const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, - const struct TALER_TokenUsePublicKeyP *use_pub, - const struct TALER_TokenUseSignatureP *use_sig, - const struct TALER_TokenIssueSignature *issue_sig) -{ - struct PayContext *pc = cls; - - for (size_t i = 0; i<pc->parse_pay.tokens_cnt; i++) - { - struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; - - if ( (0 == - GNUNET_memcmp (&tuc->pub, - use_pub)) && - (0 == - GNUNET_memcmp (&tuc->sig, - use_sig)) && - (0 == - GNUNET_memcmp (&tuc->unblinded_sig, - issue_sig)) ) - { - tuc->found_in_db = true; - break; - } - } -} - - -/** - * Small helper function to append an output token signature from db - * - * @param cls closure with `struct PayContext *` - * @param h_issue hash of the token - * @param sig signature of the token - */ -static void -append_output_token_sig (void *cls, - struct GNUNET_HashCode *h_issue, - struct GNUNET_CRYPTO_BlindedSignature *sig) -{ - struct PayContext *pc = cls; - struct TALER_MERCHANT_ContractChoice *choice; - const struct TALER_MERCHANT_ContractOutput *output; - struct SignedOutputToken out; - unsigned int cnt; - - GNUNET_assert (TALER_MERCHANT_CONTRACT_VERSION_1 == - pc->check_contract.contract_terms->version); - choice = &pc->check_contract.contract_terms->details.v1 - .choices[pc->parse_wallet_data.choice_index]; - output = &choice->outputs[pc->output_index_gen]; - cnt = count_output_tokens (pc, - output); - out.output_index = pc->output_index_gen; - out.h_issue.hash = *h_issue; - out.sig.signature = sig; - GNUNET_CRYPTO_blind_sig_incref (sig); - GNUNET_array_append (pc->output_tokens, - pc->output_tokens_len, - out); - /* Go to next output once we've output all tokens for the current one. */ - pc->output_token_cnt++; - if (pc->output_token_cnt >= cnt) - { - pc->output_token_cnt = 0; - pc->output_index_gen++; - } -} - - -/** - * Handle case where contract was already paid. Either decides - * the payment is idempotent, or refunds the excess payment. - * - * @param[in,out] pc context we use to handle the payment - */ -static void -phase_contract_paid (struct PayContext *pc) -{ - json_t *refunds; - bool unmatched = false; - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_deposits_by_order (TMH_db->cls, - pc->check_contract.order_serial, - &deposit_paid_check, - pc); - /* Since orders with choices can have a price of zero, - 0 is also a valid query state */ - if (qs < 0) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_deposits_by_order")); - return; - } - } - for (size_t i = 0; - i<pc->parse_pay.coins_cnt && ! unmatched; - i++) - { - struct DepositConfirmation *dci = &pc->parse_pay.dc[i]; - - if (! dci->matched_in_db) - unmatched = true; - } - /* Check if provided input tokens match token in the database */ - { - enum GNUNET_DB_QueryStatus qs; - - /* FIXME-Optimization: Maybe use h_contract instead of order_serial here? */ - qs = TMH_db->lookup_spent_tokens_by_order (TMH_db->cls, - pc->check_contract.order_serial, - &input_tokens_paid_check, - pc); - - if (qs < 0) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_spent_tokens_by_order")); - return; - } - } - for (size_t i = 0; i<pc->parse_pay.tokens_cnt && ! unmatched; i++) - { - struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; - - if (! tuc->found_in_db) - unmatched = true; - } - - /* In this part we are fetching token_sigs related output */ - if (! unmatched) - { - /* Everything fine, idempotent request, generate response immediately */ - enum GNUNET_DB_QueryStatus qs; - - pc->output_index_gen = 0; - qs = TMH_db->select_order_blinded_sigs ( - TMH_db->cls, - pc->order_id, - &append_output_token_sig, - pc); - if (0 > qs) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_order_blinded_sigs")); - return; - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Idempotent pay request for order `%s', signing again\n", - pc->order_id); - pc->phase = PP_SUCCESS_RESPONSE; - return; - } - /* Conflict, double-payment detected! */ - /* FIXME-#8674: What should we do with input tokens? - Currently there is no refund for tokens. */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client attempted to pay extra for already paid order `%s'\n", - pc->order_id); - refunds = json_array (); - GNUNET_assert (NULL != refunds); - for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dci = &pc->parse_pay.dc[i]; - struct TALER_MerchantSignatureP merchant_sig; - - if (dci->matched_in_db) - continue; - TALER_merchant_refund_sign (&dci->cdd.coin_pub, - &pc->check_contract.h_contract_terms, - 0, /* rtransaction id */ - &dci->cdd.amount, - &pc->hc->instance->merchant_priv, - &merchant_sig); - GNUNET_assert ( - 0 == - json_array_append_new ( - refunds, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ( - "coin_pub", - &dci->cdd.coin_pub), - GNUNET_JSON_pack_data_auto ( - "merchant_sig", - &merchant_sig), - TALER_JSON_pack_amount ("amount", - &dci->cdd.amount), - GNUNET_JSON_pack_uint64 ("rtransaction_id", - 0)))); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Generating JSON response with code %d\n", - (int) TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID); - pay_end (pc, - TALER_MHD_REPLY_JSON_PACK ( - pc->connection, - MHD_HTTP_CONFLICT, - TALER_MHD_PACK_EC ( - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID), - GNUNET_JSON_pack_array_steal ("refunds", - refunds))); -} - - -/** - * Check the database state for the given order. - * Schedules an error response in the connection on failure. - * - * @param[in,out] pc context we use to handle the payment - */ -static void -phase_check_contract (struct PayContext *pc) -{ - /* obtain contract terms */ - enum GNUNET_DB_QueryStatus qs; - bool paid = false; - - if (NULL != pc->check_contract.contract_terms_json) - { - json_decref (pc->check_contract.contract_terms_json); - pc->check_contract.contract_terms_json = NULL; - } - if (NULL != pc->check_contract.contract_terms) - { - TALER_MERCHANT_contract_free (pc->check_contract.contract_terms); - pc->check_contract.contract_terms = NULL; - } - qs = TMH_db->lookup_contract_terms2 (TMH_db->cls, - pc->hc->instance->settings.id, - pc->order_id, - &pc->check_contract.contract_terms_json, - &pc->check_contract.order_serial, - &paid, - NULL, - &pc->check_contract.pos_key, - &pc->check_contract.pos_alg); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "contract terms")); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - pc->order_id)); - return; - } - /* hash contract (needed later) */ -#if DEBUG - json_dumpf (pc->check_contract.contract_terms_json, - stderr, - JSON_INDENT (2)); -#endif - if (GNUNET_OK != - TALER_JSON_contract_hash (pc->check_contract.contract_terms_json, - &pc->check_contract.h_contract_terms)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - NULL)); - return; - } - - /* Parse the contract terms even for paid orders, - as later phases need it. */ - - pc->check_contract.contract_terms = TALER_MERCHANT_contract_parse ( - pc->check_contract.contract_terms_json, - true); - - if (NULL == pc->check_contract.contract_terms) - { - /* invalid contract */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - pc->order_id)); - return; - } - - if (paid) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' paid, checking for double-payment\n", - pc->order_id); - pc->phase = PP_CONTRACT_PAID; - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling payment for order `%s' with contract hash `%s'\n", - pc->order_id, - GNUNET_h2s (&pc->check_contract.h_contract_terms.hash)); - - /* Check fundamentals */ - { - switch (pc->check_contract.contract_terms->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - { - if (pc->parse_wallet_data.choice_index > 0) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, - "contract terms v0 has no choices")); - return; - } - } - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - { - if (pc->parse_wallet_data.choice_index < 0) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' has non-empty choices array but" - "request is missing 'choice_index' field\n", - pc->order_id); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING, - NULL)); - return; - } - if (pc->parse_wallet_data.choice_index >= - pc->check_contract.contract_terms->details.v1.choices_len) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' has choices array with %u elements but " - "request has 'choice_index' field with value %d\n", - pc->order_id, - pc->check_contract.contract_terms->details.v1.choices_len, - pc->parse_wallet_data.choice_index); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, - NULL)); - return; - } - } - break; - default: - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "contract 'version' in database not supported by this backend") - ); - return; - } - } - - if (GNUNET_TIME_timestamp_cmp (pc->check_contract.contract_terms-> - wire_deadline, - <, - pc->check_contract.contract_terms-> - refund_deadline)) - { - /* This should already have been checked when creating the order! */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, - NULL)); - return; - } - if (GNUNET_TIME_absolute_is_past (pc->check_contract.contract_terms-> - pay_deadline.abs_time)) - { - /* too late */ - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_GONE, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, - NULL)); - return; - } - -/* Make sure wire method (still) exists for this instance */ - { - struct TMH_WireMethod *wm; - - wm = pc->hc->instance->wm_head; - while (0 != GNUNET_memcmp (&pc->check_contract.contract_terms->h_wire, - &wm->h_wire)) - wm = wm->next; - if (NULL == wm) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, - NULL)); - return; - } - pc->check_contract.wm = wm; - } - pc->phase = PP_VALIDATE_TOKENS; -} - - -/** - * Try to parse the wallet_data object of the pay request into - * the given context. Schedules an error response in the connection - * on failure. - * - * @param[in,out] pc context we use to handle the payment - */ -static void -phase_parse_wallet_data (struct PayContext *pc) -{ - const json_t *tokens_evs; - const json_t *donau_obj; - - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_int16 ("choice_index", - &pc->parse_wallet_data.choice_index), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("tokens_evs", - &tokens_evs), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("donau", - &donau_obj), - NULL), - GNUNET_JSON_spec_end () - }; - - pc->parse_wallet_data.choice_index = -1; - if (NULL == pc->parse_pay.wallet_data) - { - pc->phase = PP_CHECK_CONTRACT; - return; - } - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (pc->connection, - pc->parse_pay.wallet_data, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - } - - pc->parse_wallet_data.token_envelopes_cnt - = json_array_size (tokens_evs); - if (pc->parse_wallet_data.token_envelopes_cnt > - MAX_TOKEN_ALLOWED_OUTPUTS) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "'tokens_evs' array too long")); - return; - } - pc->parse_wallet_data.token_envelopes - = GNUNET_new_array (pc->parse_wallet_data.token_envelopes_cnt, - struct TokenEnvelope); - - { - unsigned int tokens_ev_index; - json_t *token_ev; - - json_array_foreach (tokens_evs, - tokens_ev_index, - token_ev) - { - struct TokenEnvelope *ev - = &pc->parse_wallet_data.token_envelopes[tokens_ev_index]; - struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_token_envelope (NULL, - &ev->blinded_token), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - if (json_is_null (token_ev)) - continue; - res = TALER_MHD_parse_json_data (pc->connection, - token_ev, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - - for (unsigned int j = 0; j<tokens_ev_index; j++) - { - if (0 == - GNUNET_memcmp (ev->blinded_token.blinded_pub, - pc->parse_wallet_data.token_envelopes[j]. - blinded_token.blinded_pub)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "duplicate token envelope in list")); - return; - } - } - } - } - -#ifdef HAVE_DONAU_DONAU_SERVICE_H - if (NULL != donau_obj) - { - const char *donau_url_tmp; - const json_t *budikeypairs; - json_t *donau_keys_json; - - /* Fetching and checking that all 3 are present in some way */ - struct GNUNET_JSON_Specification dspec[] = { - GNUNET_JSON_spec_string ("url", - &donau_url_tmp), - GNUNET_JSON_spec_uint64 ("year", - &pc->parse_wallet_data.donau.donation_year), - GNUNET_JSON_spec_array_const ("budikeypairs", - &budikeypairs), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (pc->connection, - donau_obj, - dspec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - - /* Check if the needed data is present for the given donau URL */ - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_order_charity ( - TMH_db->cls, - pc->hc->instance->settings.id, - donau_url_tmp, - &pc->parse_wallet_data.charity_id, - &pc->parse_wallet_data.charity_priv, - &pc->parse_wallet_data.charity_max_per_year, - &pc->parse_wallet_data.charity_receipts_to_date, - &donau_keys_json, - &pc->parse_wallet_data.donau_instance_serial); - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_order_charity")); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - TMH_db->rollback (TMH_db->cls); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_DONAU_CHARITY_UNKNOWN, - donau_url_tmp)); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - pc->parse_wallet_data.donau.donau_url = - GNUNET_strdup (donau_url_tmp); - break; - } - } - - { - pc->parse_wallet_data.donau_keys = - DONAU_keys_from_json (donau_keys_json); - json_decref (donau_keys_json); - if (NULL == pc->parse_wallet_data.donau_keys) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Invalid donau_keys")); - return; - } - } - - /* Stage to parse the budikeypairs from json to struct */ - if (0 != json_array_size (budikeypairs)) - { - size_t num_bkps = json_array_size (budikeypairs); - struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps = - GNUNET_new_array (num_bkps, - struct DONAU_BlindedUniqueDonorIdentifierKeyPair); - - /* Change to json for each*/ - for (size_t i = 0; i < num_bkps; i++) - { - const json_t *bkp_obj = json_array_get (budikeypairs, - i); - if (GNUNET_SYSERR == - merchant_parse_json_bkp (&bkps[i], - bkp_obj)) - { - GNUNET_break_op (0); - for (size_t j = 0; i < j; j++) - GNUNET_CRYPTO_blinded_message_decref ( - bkps[j].blinded_udi.blinded_message); - GNUNET_free (bkps); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Failed to parse budikeypairs")); - return; - } - } - - pc->parse_wallet_data.num_bkps = num_bkps; - pc->parse_wallet_data.bkps = bkps; - } - } -#else - /* Donau not compiled in: reject request if a donau object was given. */ - if (NULL != donau_obj) - { - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_MERCHANT_GENERIC_DONAU_NOT_CONFIGURED, - "donau support disabled")); - return; - } -#endif /* HAVE_DONAU_DONAU_SERVICE_H */ - - TALER_json_hash (pc->parse_pay.wallet_data, - &pc->parse_wallet_data.h_wallet_data); - - pc->phase = PP_CHECK_CONTRACT; -} - - -/** - * Try to parse the pay request into the given pay context. - * Schedules an error response in the connection on failure. - * - * @param[in,out] pc context we use to handle the payment - */ -static void -phase_parse_pay (struct PayContext *pc) -{ - const char *session_id = NULL; - const json_t *coins; - const json_t *tokens; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("coins", - &coins), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("session_id", - &session_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("wallet_data", - &pc->parse_pay.wallet_data), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("tokens", - &tokens), - NULL), - GNUNET_JSON_spec_end () - }; - -#if DEBUG - { - char *dump = json_dumps (pc->hc->request_body, - JSON_INDENT (2) - | JSON_ENCODE_ANY - | JSON_SORT_KEYS); - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "POST /orders/%s/pay – request body follows:\n%s\n", - pc->order_id, - dump); - - free (dump); - - } -#endif /* DEBUG */ - - GNUNET_assert (PP_PARSE_PAY == pc->phase); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (pc->connection, - pc->hc->request_body, - spec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - } - - /* copy session ID (if set) */ - if (NULL != session_id) - { - pc->parse_pay.session_id = GNUNET_strdup (session_id); - } - else - { - /* use empty string as default if client didn't specify it */ - pc->parse_pay.session_id = GNUNET_strdup (""); - } - - pc->parse_pay.coins_cnt = json_array_size (coins); - if (pc->parse_pay.coins_cnt > MAX_COIN_ALLOWED_COINS) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "'coins' array too long")); - return; - } - /* note: 1 coin = 1 deposit confirmation expected */ - pc->parse_pay.dc = GNUNET_new_array (pc->parse_pay.coins_cnt, - struct DepositConfirmation); - - /* This loop populates the array 'dc' in 'pc' */ - { - unsigned int coins_index; - json_t *coin; - - json_array_foreach (coins, coins_index, coin) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[coins_index]; - const char *exchange_url; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto ("coin_sig", - &dc->cdd.coin_sig), - GNUNET_JSON_spec_fixed_auto ("coin_pub", - &dc->cdd.coin_pub), - TALER_JSON_spec_denom_sig ("ub_sig", - &dc->cdd.denom_sig), - GNUNET_JSON_spec_fixed_auto ("h_denom", - &dc->cdd.h_denom_pub), - TALER_JSON_spec_amount_any ("contribution", - &dc->cdd.amount), - TALER_JSON_spec_web_url ("exchange_url", - &exchange_url), - /* if a minimum age was required, the minimum_age_sig and - * age_commitment must be provided */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", - &dc->minimum_age_sig), - &dc->no_minimum_age_sig), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_age_commitment ("age_commitment", - &dc->age_commitment), - &dc->no_age_commitment), - /* if minimum age was not required, but coin with age restriction set - * was used, h_age_commitment must be provided. */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("h_age_commitment", - &dc->cdd.h_age_commitment), - &dc->no_h_age_commitment), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - struct ExchangeGroup *eg = NULL; - - res = TALER_MHD_parse_json_data (pc->connection, - coin, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - for (unsigned int j = 0; j<coins_index; j++) - { - if (0 == - GNUNET_memcmp (&dc->cdd.coin_pub, - &pc->parse_pay.dc[j].cdd.coin_pub)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "duplicate coin in list")); - return; - } - } - - dc->exchange_url = GNUNET_strdup (exchange_url); - dc->index = coins_index; - dc->pc = pc; - - /* Check the consistency of the (potential) age restriction - * information. */ - if (dc->no_age_commitment != dc->no_minimum_age_sig) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inconsistent: 'age_commitment' vs. 'minimum_age_sig'" - )); - return; - } - - /* Setup exchange group */ - for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) - { - if (0 == - strcmp (pc->parse_pay.egs[i]->exchange_url, - exchange_url)) - { - eg = pc->parse_pay.egs[i]; - break; - } - } - if (NULL == eg) - { - eg = GNUNET_new (struct ExchangeGroup); - eg->pc = pc; - eg->exchange_url = dc->exchange_url; - eg->total = dc->cdd.amount; - GNUNET_array_append (pc->parse_pay.egs, - pc->parse_pay.num_exchanges, - eg); - } - else - { - if (0 > - TALER_amount_add (&eg->total, - &eg->total, - &dc->cdd.amount)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, - "Overflow adding up amounts")); - return; - } - } - } - } - - pc->parse_pay.tokens_cnt = json_array_size (tokens); - if (pc->parse_pay.tokens_cnt > MAX_TOKEN_ALLOWED_INPUTS) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "'tokens' array too long")); - return; - } - - pc->parse_pay.tokens = GNUNET_new_array (pc->parse_pay.tokens_cnt, - struct TokenUseConfirmation); - - /* This loop populates the array 'tokens' in 'pc' */ - { - unsigned int tokens_index; - json_t *token; - - json_array_foreach (tokens, tokens_index, token) - { - struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[tokens_index]; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_fixed_auto ("token_sig", - &tuc->sig), - GNUNET_JSON_spec_fixed_auto ("token_pub", - &tuc->pub), - GNUNET_JSON_spec_fixed_auto ("h_issue", - &tuc->h_issue), - TALER_JSON_spec_token_issue_sig ("ub_sig", - &tuc->unblinded_sig), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (pc->connection, - token, - ispec); - if (GNUNET_YES != res) - { - GNUNET_break_op (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - - for (unsigned int j = 0; j<tokens_index; j++) - { - if (0 == - GNUNET_memcmp (&tuc->pub, - &pc->parse_pay.tokens[j].pub)) - { - GNUNET_break_op (0); - pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "duplicate token in list")); - return; - } - } - } - } - - pc->phase = PP_PARSE_WALLET_DATA; -} - - -/** - * Custom cleanup routine for a `struct PayContext`. - * - * @param cls the `struct PayContext` to clean up. - */ -static void -pay_context_cleanup (void *cls) -{ - struct PayContext *pc = cls; - - if (NULL != pc->batch_deposits.timeout_task) - { - GNUNET_SCHEDULER_cancel (pc->batch_deposits.timeout_task); - pc->batch_deposits.timeout_task = NULL; - } - if (NULL != pc->check_contract.contract_terms_json) - { - json_decref (pc->check_contract.contract_terms_json); - pc->check_contract.contract_terms_json = NULL; - } - for (unsigned int i = 0; i<pc->parse_pay.coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; - - TALER_denom_sig_free (&dc->cdd.denom_sig); - GNUNET_free (dc->exchange_url); - } - GNUNET_free (pc->parse_pay.dc); - for (unsigned int i = 0; i<pc->parse_pay.tokens_cnt; i++) - { - struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; - - TALER_token_issue_sig_free (&tuc->unblinded_sig); - } - GNUNET_free (pc->parse_pay.tokens); - for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) - { - struct ExchangeGroup *eg = pc->parse_pay.egs[i]; - - if (NULL != eg->fo) - TMH_EXCHANGES_keys4exchange_cancel (eg->fo); - GNUNET_free (eg); - } - GNUNET_free (pc->parse_pay.egs); - if (NULL != pc->check_contract.contract_terms) - { - TALER_MERCHANT_contract_free (pc->check_contract.contract_terms); - pc->check_contract.contract_terms = NULL; - } - if (NULL != pc->response) - { - MHD_destroy_response (pc->response); - pc->response = NULL; - } - GNUNET_free (pc->parse_pay.session_id); - GNUNET_CONTAINER_DLL_remove (pc_head, - pc_tail, - pc); - GNUNET_free (pc->check_contract.pos_key); - GNUNET_free (pc->compute_money_pots.pots); - GNUNET_free (pc->compute_money_pots.increments); -#ifdef HAVE_DONAU_DONAU_SERVICE_H - if (NULL != pc->parse_wallet_data.bkps) - { - for (size_t i = 0; i < pc->parse_wallet_data.num_bkps; i++) - GNUNET_CRYPTO_blinded_message_decref ( - pc->parse_wallet_data.bkps[i].blinded_udi.blinded_message); - GNUNET_array_grow (pc->parse_wallet_data.bkps, - pc->parse_wallet_data.num_bkps, - 0); - } - if (NULL != pc->parse_wallet_data.donau_keys) - { - DONAU_keys_decref (pc->parse_wallet_data.donau_keys); - pc->parse_wallet_data.donau_keys = NULL; - } - GNUNET_free (pc->parse_wallet_data.donau.donau_url); -#endif - for (unsigned int i = 0; i<pc->parse_wallet_data.token_envelopes_cnt; i++) - { - struct TokenEnvelope *ev - = &pc->parse_wallet_data.token_envelopes[i]; - - GNUNET_CRYPTO_blinded_message_decref (ev->blinded_token.blinded_pub); - } - GNUNET_free (pc->parse_wallet_data.token_envelopes); - if (NULL != pc->output_tokens) - { - for (unsigned int i = 0; i<pc->output_tokens_len; i++) - if (NULL != pc->output_tokens[i].sig.signature) - GNUNET_CRYPTO_blinded_sig_decref (pc->output_tokens[i].sig.signature); - GNUNET_free (pc->output_tokens); - pc->output_tokens = NULL; - } - GNUNET_free (pc); -} - - -MHD_RESULT -TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct PayContext *pc = hc->ctx; - - GNUNET_assert (NULL != hc->infix); - if (NULL == pc) - { - pc = GNUNET_new (struct PayContext); - pc->connection = connection; - pc->hc = hc; - pc->order_id = hc->infix; - hc->ctx = pc; - hc->cc = &pay_context_cleanup; - GNUNET_CONTAINER_DLL_insert (pc_head, - pc_tail, - pc); - } - while (1) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing /pay in phase %d\n", - (int) pc->phase); - switch (pc->phase) - { - case PP_PARSE_PAY: - phase_parse_pay (pc); - break; - case PP_PARSE_WALLET_DATA: - phase_parse_wallet_data (pc); - break; - case PP_CHECK_CONTRACT: - phase_check_contract (pc); - break; - case PP_VALIDATE_TOKENS: - phase_validate_tokens (pc); - break; - case PP_CONTRACT_PAID: - phase_contract_paid (pc); - break; - case PP_COMPUTE_MONEY_POTS: - phase_compute_money_pots (pc); - break; - case PP_PAY_TRANSACTION: - phase_execute_pay_transaction (pc); - break; - case PP_REQUEST_DONATION_RECEIPT: -#ifdef HAVE_DONAU_DONAU_SERVICE_H - phase_request_donation_receipt (pc); -#else - pc->phase++; -#endif - break; - case PP_FINAL_OUTPUT_TOKEN_PROCESSING: - phase_final_output_token_processing (pc); - break; - case PP_PAYMENT_NOTIFICATION: - phase_payment_notification (pc); - break; - case PP_SUCCESS_RESPONSE: - phase_success_response (pc); - break; - case PP_BATCH_DEPOSITS: - phase_batch_deposits (pc); - break; - case PP_RETURN_RESPONSE: - phase_return_response (pc); - break; - case PP_FAIL_LEGAL_REASONS: - phase_fail_for_legal_reasons (pc); - break; - case PP_END_YES: - return MHD_YES; - case PP_END_NO: - return MHD_NO; - default: - /* should not be reachable */ - GNUNET_assert (0); - return MHD_NO; - } - switch (pc->suspended) - { - case GNUNET_SYSERR: - /* during shutdown, we don't generate any more replies */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing /pay ends due to shutdown in phase %d\n", - (int) pc->phase); - return MHD_NO; - case GNUNET_NO: - /* continue to next phase */ - break; - case GNUNET_YES: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing /pay suspended in phase %d\n", - (int) pc->phase); - return MHD_YES; - } - } - /* impossible to get here */ - GNUNET_assert (0); - return MHD_YES; -} - - -/* end of taler-merchant-httpd_post-orders-ID-pay.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.h b/src/backend/taler-merchant-httpd_post-orders-ID-pay.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 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/> -*/ -/** - * @file taler-merchant-httpd_post-orders-ID-pay.h - * @brief headers for POST /orders/$ID/pay handler - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAY_H -#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAY_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Force all pay contexts to be resumed as we are about - * to shut down MHD. - */ -void -TMH_force_pc_resume (void); - - -/** - * Process payment for a claimed order. - * - * @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_post_orders_ID_pay (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c @@ -1,845 +0,0 @@ -/* - This file is part of TALER - (C) 2020-2022 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-httpd_post-orders-ID-refund.c - * @brief handling of POST /orders/$ID/refund requests - * @author Jonathan Buchanan - */ -#include "taler/platform.h" -#include <taler/taler_dbevents.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_post-orders-ID-refund.h" - - -/** - * Information we keep for each coin to be refunded. - */ -struct CoinRefund -{ - - /** - * Kept in a DLL. - */ - struct CoinRefund *next; - - /** - * Kept in a DLL. - */ - struct CoinRefund *prev; - - /** - * Request to connect to the target exchange. - */ - struct TMH_EXCHANGES_KeysOperation *fo; - - /** - * Handle for the refund operation with the exchange. - */ - struct TALER_EXCHANGE_PostCoinsRefundHandle *rh; - - /** - * Request this operation is part of. - */ - struct PostRefundData *prd; - - /** - * URL of the exchange for this @e coin_pub. - */ - char *exchange_url; - - /** - * Fully reply from the exchange, only possibly set if - * we got a JSON reply and a non-#MHD_HTTP_OK error code - */ - json_t *exchange_reply; - - /** - * When did the merchant grant the refund. To be used to group events - * in the wallet. - */ - struct GNUNET_TIME_Timestamp execution_time; - - /** - * Coin to refund. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Refund transaction ID to use. - */ - uint64_t rtransaction_id; - - /** - * Unique serial number identifying the refund. - */ - uint64_t refund_serial; - - /** - * Amount to refund. - */ - struct TALER_Amount refund_amount; - - /** - * Public key of the exchange affirming the refund. - */ - struct TALER_ExchangePublicKeyP exchange_pub; - - /** - * Signature of the exchange affirming the refund. - */ - struct TALER_ExchangeSignatureP exchange_sig; - - /** - * HTTP status from the exchange, #MHD_HTTP_OK if - * @a exchange_pub and @a exchange_sig are valid. - */ - unsigned int exchange_status; - - /** - * HTTP error code from the exchange. - */ - enum TALER_ErrorCode exchange_code; - -}; - - -/** - * Context for the operation. - */ -struct PostRefundData -{ - - /** - * Hashed version of contract terms. All zeros if not provided. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * DLL of (suspended) requests. - */ - struct PostRefundData *next; - - /** - * DLL of (suspended) requests. - */ - struct PostRefundData *prev; - - /** - * Refunds for this order. Head of DLL. - */ - struct CoinRefund *cr_head; - - /** - * Refunds for this order. Tail of DLL. - */ - struct CoinRefund *cr_tail; - - /** - * Context of the request. - */ - struct TMH_HandlerContext *hc; - - /** - * Entry in the #resume_timeout_heap for this check payment, if we are - * suspended. - */ - struct TMH_SuspendedConnection sc; - - /** - * order ID for the payment - */ - const char *order_id; - - /** - * Where to get the contract - */ - const char *contract_url; - - /** - * fulfillment URL of the contract (valid as long as - * @e contract_terms is valid). - */ - const char *fulfillment_url; - - /** - * session of the client - */ - const char *session_id; - - /** - * Contract terms of the payment we are checking. NULL when they - * are not (yet) known. - */ - json_t *contract_terms; - - /** - * Total refunds granted for this payment. Only initialized - * if @e refunded is set to true. - */ - struct TALER_Amount refund_amount; - - /** - * Did we suspend @a connection and are thus in - * the #prd_head DLL (#GNUNET_YES). Set to - * #GNUNET_NO if we are not suspended, and to - * #GNUNET_SYSERR if we should close the connection - * without a response due to shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * Return code: #TALER_EC_NONE if successful. - */ - enum TALER_ErrorCode ec; - - /** - * HTTP status to use for the reply, 0 if not yet known. - */ - unsigned int http_status; - - /** - * Set to true if we are dealing with an unclaimed order - * (and thus @e h_contract_terms is not set, and certain - * DB queries will not work). - */ - bool unclaimed; - - /** - * Set to true if this payment has been refunded and - * @e refund_amount is initialized. - */ - bool refunded; - - /** - * Set to true if a refund is still available for the - * wallet for this payment. - */ - bool refund_available; - - /** - * Set to true if the client requested HTML, otherwise - * we generate JSON. - */ - bool generate_html; - -}; - - -/** - * Head of DLL of (suspended) requests. - */ -static struct PostRefundData *prd_head; - -/** - * Tail of DLL of (suspended) requests. - */ -static struct PostRefundData *prd_tail; - - -/** - * Function called when we are done processing a refund request. - * Frees memory associated with @a ctx. - * - * @param ctx a `struct PostRefundData` - */ -static void -refund_cleanup (void *ctx) -{ - struct PostRefundData *prd = ctx; - struct CoinRefund *cr; - - while (NULL != (cr = prd->cr_head)) - { - GNUNET_CONTAINER_DLL_remove (prd->cr_head, - prd->cr_tail, - cr); - json_decref (cr->exchange_reply); - GNUNET_free (cr->exchange_url); - if (NULL != cr->fo) - { - TMH_EXCHANGES_keys4exchange_cancel (cr->fo); - cr->fo = NULL; - } - if (NULL != cr->rh) - { - TALER_EXCHANGE_post_coins_refund_cancel (cr->rh); - cr->rh = NULL; - } - GNUNET_free (cr); - } - json_decref (prd->contract_terms); - GNUNET_free (prd); -} - - -/** - * Force resuming all suspended order lookups, needed during shutdown. - */ -void -TMH_force_wallet_refund_order_resume (void) -{ - struct PostRefundData *prd; - - while (NULL != (prd = prd_head)) - { - GNUNET_CONTAINER_DLL_remove (prd_head, - prd_tail, - prd); - GNUNET_assert (GNUNET_YES == prd->suspended); - prd->suspended = GNUNET_SYSERR; - MHD_resume_connection (prd->sc.con); - } -} - - -/** - * Check if @a prd has exchange requests still pending. - * - * @param prd state to check - * @return true if activities are still pending - */ -static bool -exchange_operations_pending (struct PostRefundData *prd) -{ - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - if ( (NULL != cr->fo) || - (NULL != cr->rh) ) - return true; - } - return false; -} - - -/** - * Check if @a prd is ready to be resumed, and if so, do it. - * - * @param prd refund request to be possibly ready - */ -static void -check_resume_prd (struct PostRefundData *prd) -{ - if ( (TALER_EC_NONE == prd->ec) && - exchange_operations_pending (prd) ) - return; - GNUNET_CONTAINER_DLL_remove (prd_head, - prd_tail, - prd); - GNUNET_assert (prd->suspended); - prd->suspended = GNUNET_NO; - MHD_resume_connection (prd->sc.con); - TALER_MHD_daemon_trigger (); -} - - -/** - * Notify applications waiting for a client to obtain - * a refund. - * - * @param prd refund request with the change - */ -static void -notify_refund_obtained (struct PostRefundData *prd) -{ - struct TMH_OrderPayEventP refund_eh = { - .header.size = htons (sizeof (refund_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_REFUND_OBTAINED), - .merchant_pub = prd->hc->instance->merchant_pub - }; - - GNUNET_CRYPTO_hash (prd->order_id, - strlen (prd->order_id), - &refund_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &refund_eh.header, - NULL, - 0); -} - - -/** - * Callbacks of this type are used to serve the result of submitting a - * refund request to an exchange. - * - * @param cls a `struct CoinRefund` - * @param rr response data - */ -static void -refund_cb (void *cls, - const struct TALER_EXCHANGE_PostCoinsRefundResponse *rr) -{ - struct CoinRefund *cr = cls; - const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr; - - cr->rh = NULL; - cr->exchange_status = hr->http_status; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Exchange refund status for coin %s is %u\n", - TALER_B2S (&cr->coin_pub), - hr->http_status); - switch (hr->http_status) - { - case MHD_HTTP_OK: - { - enum GNUNET_DB_QueryStatus qs; - - cr->exchange_pub = rr->details.ok.exchange_pub; - cr->exchange_sig = rr->details.ok.exchange_sig; - qs = TMH_db->insert_refund_proof (TMH_db->cls, - cr->refund_serial, - &rr->details.ok.exchange_sig, - &rr->details.ok.exchange_pub); - if (0 >= qs) - { - /* generally, this is relatively harmless for the merchant, but let's at - least log this. */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to persist exchange response to /refund in database: %d\n", - qs); - } - else - { - notify_refund_obtained (cr->prd); - } - } - break; - default: - cr->exchange_code = hr->ec; - cr->exchange_reply = json_incref ((json_t*) hr->reply); - break; - } - check_resume_prd (cr->prd); -} - - -/** - * Function called with the result of a - * #TMH_EXCHANGES_keys4exchange() - * operation. - * - * @param cls a `struct CoinRefund *` - * @param keys keys of exchange, NULL on error - * @param exchange representation of the exchange - */ -static void -exchange_found_cb (void *cls, - struct TALER_EXCHANGE_Keys *keys, - struct TMH_Exchange *exchange) -{ - struct CoinRefund *cr = cls; - struct PostRefundData *prd = cr->prd; - - (void) exchange; - cr->fo = NULL; - if (NULL == keys) - { - prd->http_status = MHD_HTTP_GATEWAY_TIMEOUT; - prd->ec = TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT; - check_resume_prd (prd); - return; - } - cr->rh = TALER_EXCHANGE_post_coins_refund_create ( - TMH_curl_ctx, - cr->exchange_url, - keys, - &cr->refund_amount, - &prd->h_contract_terms, - &cr->coin_pub, - cr->rtransaction_id, - &prd->hc->instance->merchant_priv); - GNUNET_assert (NULL != cr->rh); - GNUNET_assert (TALER_EC_NONE == - TALER_EXCHANGE_post_coins_refund_start (cr->rh, - &refund_cb, - cr)); -} - - -/** - * Function called with information about a refund. - * It is responsible for summing up the refund amount. - * - * @param cls closure - * @param refund_serial unique serial number of the refund - * @param timestamp time of the refund (for grouping of refunds in the wallet UI) - * @param coin_pub public coin from which the refund comes from - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param rtransaction_id identificator of the refund - * @param reason human-readable explanation of the refund - * @param refund_amount refund amount which is being taken from @a coin_pub - * @param pending true if the this refund was not yet processed by the wallet/exchange - */ -static void -process_refunds_cb (void *cls, - uint64_t refund_serial, - struct GNUNET_TIME_Timestamp timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - uint64_t rtransaction_id, - const char *reason, - const struct TALER_Amount *refund_amount, - bool pending) -{ - struct PostRefundData *prd = cls; - struct CoinRefund *cr; - - for (cr = prd->cr_head; - NULL != cr; - cr = cr->next) - if (cr->refund_serial == refund_serial) - return; - /* already known */ - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Found refund of %s for coin %s with reason `%s' in database\n", - TALER_amount2s (refund_amount), - TALER_B2S (coin_pub), - reason); - cr = GNUNET_new (struct CoinRefund); - cr->refund_serial = refund_serial; - cr->exchange_url = GNUNET_strdup (exchange_url); - cr->prd = prd; - cr->coin_pub = *coin_pub; - cr->rtransaction_id = rtransaction_id; - cr->refund_amount = *refund_amount; - cr->execution_time = timestamp; - GNUNET_CONTAINER_DLL_insert (prd->cr_head, - prd->cr_tail, - cr); - if (prd->refunded) - { - GNUNET_assert (0 <= - TALER_amount_add (&prd->refund_amount, - &prd->refund_amount, - refund_amount)); - return; - } - prd->refund_amount = *refund_amount; - prd->refunded = true; - prd->refund_available |= pending; -} - - -/** - * Obtain refunds for an order. - * - * @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_post_orders_ID_refund (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct PostRefundData *prd = hc->ctx; - enum GNUNET_DB_QueryStatus qs; - - if (NULL == prd) - { - prd = GNUNET_new (struct PostRefundData); - prd->sc.con = connection; - prd->hc = hc; - prd->order_id = hc->infix; - hc->ctx = prd; - hc->cc = &refund_cleanup; - { - enum GNUNET_GenericReturnValue res; - - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("h_contract", - &prd->h_contract_terms), - GNUNET_JSON_spec_end () - }; - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - TMH_db->preflight (TMH_db->cls); - { - json_t *contract_terms; - uint64_t order_serial; - - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &contract_terms, - &order_serial, - NULL); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "contract terms"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - hc->infix); - } - { - struct TALER_PrivateContractHashP h_contract_terms; - - if (GNUNET_OK != - TALER_JSON_contract_hash (contract_terms, - &h_contract_terms)) - { - GNUNET_break (0); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - NULL); - } - json_decref (contract_terms); - if (0 != GNUNET_memcmp (&h_contract_terms, - &prd->h_contract_terms)) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); - } - } - } - } - if (GNUNET_SYSERR == prd->suspended) - return MHD_NO; /* we are in shutdown */ - - if (TALER_EC_NONE != prd->ec) - { - GNUNET_break (0 != prd->http_status); - /* kill pending coin refund operations immediately, just to be - extra sure they don't modify 'prd' after we already created - a reply (this might not be needed, but feels safer). */ - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - if (NULL != cr->fo) - { - TMH_EXCHANGES_keys4exchange_cancel (cr->fo); - cr->fo = NULL; - } - if (NULL != cr->rh) - { - TALER_EXCHANGE_post_coins_refund_cancel (cr->rh); - cr->rh = NULL; - } - } - return TALER_MHD_reply_with_error (connection, - prd->http_status, - prd->ec, - NULL); - } - - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - hc->instance->settings.id, - &prd->h_contract_terms, - &process_refunds_cb, - prd); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "detailed refunds"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "no coins found that could be refunded"); - } - - /* Now launch exchange interactions, unless we already have the - response in the database! */ - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - qs = TMH_db->lookup_refund_proof (TMH_db->cls, - cr->refund_serial, - &cr->exchange_sig, - &cr->exchange_pub); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "refund proof"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - if (NULL == cr->exchange_reply) - { - /* We need to talk to the exchange */ - cr->fo = TMH_EXCHANGES_keys4exchange (cr->exchange_url, - false, - &exchange_found_cb, - cr); - if (NULL == cr->fo) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, - cr->exchange_url); - } - } - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* We got a reply earlier, set status accordingly */ - cr->exchange_status = MHD_HTTP_OK; - break; - } - } - - /* Check if there are still exchange operations pending */ - if (exchange_operations_pending (prd)) - { - if (GNUNET_NO == prd->suspended) - { - prd->suspended = GNUNET_YES; - MHD_suspend_connection (connection); - GNUNET_CONTAINER_DLL_insert (prd_head, - prd_tail, - prd); - } - return MHD_YES; /* we're still talking to the exchange */ - } - - { - json_t *ra; - - ra = json_array (); - GNUNET_assert (NULL != ra); - for (struct CoinRefund *cr = prd->cr_head; - NULL != cr; - cr = cr->next) - { - json_t *refund; - - if (MHD_HTTP_OK != cr->exchange_status) - { - if (NULL == cr->exchange_reply) - { - refund = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "failure"), - GNUNET_JSON_pack_uint64 ("exchange_status", - cr->exchange_status), - GNUNET_JSON_pack_uint64 ("rtransaction_id", - cr->rtransaction_id), - GNUNET_JSON_pack_data_auto ("coin_pub", - &cr->coin_pub), - TALER_JSON_pack_amount ("refund_amount", - &cr->refund_amount), - GNUNET_JSON_pack_timestamp ("execution_time", - cr->execution_time)); - } - else - { - refund = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "failure"), - GNUNET_JSON_pack_uint64 ("exchange_status", - cr->exchange_status), - GNUNET_JSON_pack_uint64 ("exchange_code", - cr->exchange_code), - GNUNET_JSON_pack_object_incref ("exchange_reply", - cr->exchange_reply), - GNUNET_JSON_pack_uint64 ("rtransaction_id", - cr->rtransaction_id), - GNUNET_JSON_pack_data_auto ("coin_pub", - &cr->coin_pub), - TALER_JSON_pack_amount ("refund_amount", - &cr->refund_amount), - GNUNET_JSON_pack_timestamp ("execution_time", - cr->execution_time)); - } - } - else - { - refund = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "success"), - GNUNET_JSON_pack_uint64 ("exchange_status", - cr->exchange_status), - GNUNET_JSON_pack_data_auto ("exchange_sig", - &cr->exchange_sig), - GNUNET_JSON_pack_data_auto ("exchange_pub", - &cr->exchange_pub), - GNUNET_JSON_pack_uint64 ("rtransaction_id", - cr->rtransaction_id), - GNUNET_JSON_pack_data_auto ("coin_pub", - &cr->coin_pub), - TALER_JSON_pack_amount ("refund_amount", - &cr->refund_amount), - GNUNET_JSON_pack_timestamp ("execution_time", - cr->execution_time)); - } - GNUNET_assert ( - 0 == - json_array_append_new (ra, - refund)); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - TALER_JSON_pack_amount ("refund_amount", - &prd->refund_amount), - GNUNET_JSON_pack_array_steal ("refunds", - ra), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &hc->instance->merchant_pub)); - } - - return MHD_YES; -} - - -/* end of taler-merchant-httpd_post-orders-ID-refund.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.h b/src/backend/taler-merchant-httpd_post-orders-ID-refund.h @@ -1,48 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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/> -*/ -/** - * @file taler-merchant-httpd_post-orders-ID-refund.h - * @brief headers for POST /orders/$ID/refund handler - * @author Jonathan Buchanan - */ -#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_REFUND_H -#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_REFUND_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Obtain refunds for an order. - * - * @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_post_orders_ID_refund (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Force resuming all suspended order lookups, needed during shutdown. - */ -void -TMH_force_wallet_refund_order_resume (void); - - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-unclaim.c b/src/backend/taler-merchant-httpd_post-orders-ID-unclaim.c @@ -1,122 +0,0 @@ -/* - This file is part of TALER - (C) 2026 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-httpd_post-orders-ID-unclaim.c - * @brief headers for POST /orders/$ID/unclaim handler - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_signatures.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler-merchant-httpd_post-orders-ID-unclaim.h" - - -MHD_RESULT -TMH_post_orders_ID_unclaim (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *order_id = hc->infix; - struct GNUNET_CRYPTO_EddsaPublicKey nonce; - struct GNUNET_CRYPTO_EddsaSignature nsig; - struct GNUNET_HashCode h_contract; - enum GNUNET_DB_QueryStatus qs; - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("unclaim_sig", - &nsig), - GNUNET_JSON_spec_fixed_auto ("nonce", - &nonce), - GNUNET_JSON_spec_fixed_auto ("h_contract", - &h_contract), - GNUNET_JSON_spec_end () - }; - 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; - } - } - - if (GNUNET_OK != - TALER_wallet_order_unclaim_verify (&h_contract, - &nonce, - &nsig)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unclaim_sig"); - } - TMH_db->preflight (TMH_db->cls); - qs = TMH_db->insert_unclaim_signature (TMH_db->cls, - hc->instance->settings.id, - &hc->instance->merchant_pub, - order_id, - &nonce, - &h_contract, - &nsig); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "insert_unclaim_signature"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "insert_unclaim_signature"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND, - order_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* Good! return signature (below) */ - } - - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_post-orders-ID-unclaim.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-unclaim.h b/src/backend/taler-merchant-httpd_post-orders-ID-unclaim.h @@ -1,40 +0,0 @@ -/* - This file is part of TALER - (C) 2026 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/> -*/ -/** - * @file taler-merchant-httpd_post-orders-ID-unclaim.h - * @brief headers for POST /orders/$ID/unclaim handler - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_POST_ORDERS_ID_UNCLAIM_H -#define TALER_MERCHANT_HTTPD_POST_ORDERS_ID_UNCLAIM_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - -/** - * Manage a POST /orders/$ID/unclaim request. Allows the client to - * unclaim an order. - * - * @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_post_orders_ID_unclaim (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c @@ -0,0 +1,1044 @@ +/* + This file is part of TALER + (C) 2014-2021 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-httpd_post-orders-ORDER_ID-abort.c + * @brief handling of POST /orders/$ID/abort requests + * @author Marcello Stanisci + * @author Christian Grothoff + * @author Florian Dold + */ +#include "taler/platform.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_get-exchanges.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-abort.h" + + +/** + * How long to wait before giving up processing with the exchange? + */ +#define ABORT_GENERIC_TIMEOUT (GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, \ + 30)) + +/** + * How often do we retry the (complex!) database transaction? + */ +#define MAX_RETRIES 5 + +/** + * Information we keep for an individual call to the /abort handler. + */ +struct AbortContext; + +/** + * Information kept during a /abort request for each coin. + */ +struct RefundDetails +{ + + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature from the exchange confirming the refund. + * Set if we were successful (status 200). + */ + struct TALER_ExchangeSignatureP exchange_sig; + + /** + * Public key used for @e exchange_sig. + * Set if we were successful (status 200). + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Reference to the main AbortContext + */ + struct AbortContext *ac; + + /** + * Handle to the refund operation we are performing for + * this coin, NULL after the operation is done. + */ + struct TALER_EXCHANGE_PostCoinsRefundHandle *rh; + + /** + * URL of the exchange that issued this coin. + */ + char *exchange_url; + + /** + * Body of the response from the exchange. Note that the body returned MUST + * be freed (if non-NULL). + */ + json_t *exchange_reply; + + /** + * Amount this coin contributes to the total purchase price. + * This amount includes the deposit fee. + */ + struct TALER_Amount amount_with_fee; + + /** + * Offset of this coin into the `rd` array of all coins in the + * @e ac. + */ + unsigned int index; + + /** + * HTTP status returned by the exchange (if any). + */ + unsigned int http_status; + + /** + * Did we try to process this refund yet? + */ + bool processed; + + /** + * Did we find the deposit in our own database? + */ + bool found_deposit; +}; + + +/** + * Information we keep for an individual call to the /abort handler. + */ +struct AbortContext +{ + + /** + * Hashed contract terms (according to client). + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Context for our operation. + */ + struct TMH_HandlerContext *hc; + + /** + * Stored in a DLL. + */ + struct AbortContext *next; + + /** + * Stored in a DLL. + */ + struct AbortContext *prev; + + /** + * Array with @e coins_cnt coins we are despositing. + */ + struct RefundDetails *rd; + + /** + * MHD connection to return to + */ + struct MHD_Connection *connection; + + /** + * Task called when the (suspended) processing for + * the /abort request times out. + * Happens when we don't get a response from the exchange. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Response to return, NULL if we don't have one yet. + */ + struct MHD_Response *response; + + /** + * Handle to the exchange that we are doing the abortment with. + * (initially NULL while @e fo is trying to find a exchange). + */ + struct TALER_EXCHANGE_Handle *mh; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_KeysOperation *fo; + + /** + * URL of the exchange used for the last @e fo. + */ + const char *current_exchange; + + /** + * Number of coins this abort is for. Length of the @e rd array. + */ + size_t coins_cnt; + + /** + * How often have we retried the 'main' transaction? + */ + unsigned int retry_counter; + + /** + * Number of transactions still pending. Initially set to + * @e coins_cnt, decremented on each transaction that + * successfully finished. + */ + size_t pending; + + /** + * Number of transactions still pending for the currently selected + * exchange. Initially set to the number of coins started at the + * exchange, decremented on each transaction that successfully + * finished. Once it hits zero, we pick the next exchange. + */ + size_t pending_at_ce; + + /** + * HTTP status code to use for the reply, i.e 200 for "OK". + * Special value UINT_MAX is used to indicate hard errors + * (no reply, return #MHD_NO). + */ + unsigned int response_code; + + /** + * #GNUNET_NO if the @e connection was not suspended, + * #GNUNET_YES if the @e connection was suspended, + * #GNUNET_SYSERR if @e connection was resumed to as + * part of #MH_force_ac_resume during shutdown. + */ + int suspended; + +}; + + +/** + * Head of active abort context DLL. + */ +static struct AbortContext *ac_head; + +/** + * Tail of active abort context DLL. + */ +static struct AbortContext *ac_tail; + + +/** + * Abort all pending /deposit operations. + * + * @param ac abort context to abort + */ +static void +abort_refunds (struct AbortContext *ac) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Aborting pending /deposit operations\n"); + for (size_t i = 0; i<ac->coins_cnt; i++) + { + struct RefundDetails *rdi = &ac->rd[i]; + + if (NULL != rdi->rh) + { + TALER_EXCHANGE_post_coins_refund_cancel (rdi->rh); + rdi->rh = NULL; + } + } +} + + +void +TMH_force_ac_resume () +{ + for (struct AbortContext *ac = ac_head; + NULL != ac; + ac = ac->next) + { + abort_refunds (ac); + if (NULL != ac->timeout_task) + { + GNUNET_SCHEDULER_cancel (ac->timeout_task); + ac->timeout_task = NULL; + } + if (GNUNET_YES == ac->suspended) + { + ac->suspended = GNUNET_SYSERR; + MHD_resume_connection (ac->connection); + } + } +} + + +/** + * Resume the given abort context and send the given response. + * Stores the response in the @a ac and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param ac abortment context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_abort_with_response (struct AbortContext *ac, + unsigned int response_code, + struct MHD_Response *response) +{ + abort_refunds (ac); + ac->response_code = response_code; + ac->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /abort handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != ac->timeout_task) + { + GNUNET_SCHEDULER_cancel (ac->timeout_task); + ac->timeout_task = NULL; + } + GNUNET_assert (GNUNET_YES == ac->suspended); + ac->suspended = GNUNET_NO; + MHD_resume_connection (ac->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Resume abortment processing with an error. + * + * @param ac operation to resume + * @param http_status http status code to return + * @param ec taler error code to return + * @param msg human readable error message + */ +static void +resume_abort_with_error (struct AbortContext *ac, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *msg) +{ + resume_abort_with_response (ac, + http_status, + TALER_MHD_make_error (ec, + msg)); +} + + +/** + * Generate a response that indicates abortment success. + * + * @param ac abortment context + */ +static void +generate_success_response (struct AbortContext *ac) +{ + json_t *refunds; + unsigned int hc = MHD_HTTP_OK; + + refunds = json_array (); + if (NULL == refunds) + { + GNUNET_break (0); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, + "could not create JSON array"); + return; + } + for (size_t i = 0; i<ac->coins_cnt; i++) + { + struct RefundDetails *rdi = &ac->rd[i]; + json_t *detail; + + if (rdi->found_deposit) + { + if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) && + (MHD_HTTP_NOT_FOUND != rdi->http_status) && + (MHD_HTTP_GONE != rdi->http_status) ) || + (0 == rdi->http_status) || + (NULL == rdi->exchange_reply) ) + { + hc = MHD_HTTP_BAD_GATEWAY; + } + } + if (! rdi->found_deposit) + { + detail = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "undeposited")); + } + else + { + if (MHD_HTTP_OK != rdi->http_status) + { + detail = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "failure"), + GNUNET_JSON_pack_uint64 ("exchange_status", + rdi->http_status), + GNUNET_JSON_pack_uint64 ("exchange_code", + (NULL != rdi->exchange_reply) + ? TALER_JSON_get_error_code ( + rdi->exchange_reply) + : TALER_EC_GENERIC_INVALID_RESPONSE), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("exchange_reply", + rdi->exchange_reply))); + } + else + { + detail = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "success"), + GNUNET_JSON_pack_uint64 ("exchange_status", + rdi->http_status), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &rdi->exchange_sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &rdi->exchange_pub)); + } + } + GNUNET_assert (0 == + json_array_append_new (refunds, + detail)); + } + + /* Resume and send back the response. */ + resume_abort_with_response ( + ac, + hc, + TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("refunds", + refunds))); +} + + +/** + * Custom cleanup routine for a `struct AbortContext`. + * + * @param cls the `struct AbortContext` to clean up. + */ +static void +abort_context_cleanup (void *cls) +{ + struct AbortContext *ac = cls; + + if (NULL != ac->timeout_task) + { + GNUNET_SCHEDULER_cancel (ac->timeout_task); + ac->timeout_task = NULL; + } + abort_refunds (ac); + for (size_t i = 0; i<ac->coins_cnt; i++) + { + struct RefundDetails *rdi = &ac->rd[i]; + + if (NULL != rdi->exchange_reply) + { + json_decref (rdi->exchange_reply); + rdi->exchange_reply = NULL; + } + GNUNET_free (rdi->exchange_url); + } + GNUNET_free (ac->rd); + if (NULL != ac->fo) + { + TMH_EXCHANGES_keys4exchange_cancel (ac->fo); + ac->fo = NULL; + } + if (NULL != ac->response) + { + MHD_destroy_response (ac->response); + ac->response = NULL; + } + GNUNET_CONTAINER_DLL_remove (ac_head, + ac_tail, + ac); + GNUNET_free (ac); +} + + +/** + * Find the exchange we need to talk to for the next + * pending deposit permission. + * + * @param ac abortment context we are processing + */ +static void +find_next_exchange (struct AbortContext *ac); + + +/** + * Function called with the result from the exchange (to be + * passed back to the wallet). + * + * @param cls closure + * @param rr response data + */ +static void +refund_cb (void *cls, + const struct TALER_EXCHANGE_PostCoinsRefundResponse *rr) +{ + struct RefundDetails *rd = cls; + const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr; + struct AbortContext *ac = rd->ac; + + rd->rh = NULL; + rd->http_status = hr->http_status; + rd->exchange_reply = json_incref ((json_t*) hr->reply); + if (MHD_HTTP_OK == hr->http_status) + { + rd->exchange_pub = rr->details.ok.exchange_pub; + rd->exchange_sig = rr->details.ok.exchange_sig; + } + ac->pending_at_ce--; + if (0 == ac->pending_at_ce) + find_next_exchange (ac); +} + + +/** + * Function called with the result of our exchange lookup. + * + * @param cls the `struct AbortContext` + * @param keys keys of the exchange + * @param exchange representation of the exchange + */ +static void +process_abort_with_exchange (void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct AbortContext *ac = cls; + + (void) exchange; + ac->fo = NULL; + GNUNET_assert (GNUNET_YES == ac->suspended); + if (NULL == keys) + { + resume_abort_with_response ( + ac, + MHD_HTTP_GATEWAY_TIMEOUT, + TALER_MHD_make_error ( + TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, + NULL)); + return; + } + /* Initiate refund operation for all coins of + the current exchange (!) */ + GNUNET_assert (0 == ac->pending_at_ce); + for (size_t i = 0; i<ac->coins_cnt; i++) + { + struct RefundDetails *rdi = &ac->rd[i]; + + if (rdi->processed) + continue; + GNUNET_assert (NULL == rdi->rh); + if (0 != strcmp (rdi->exchange_url, + ac->current_exchange)) + continue; + rdi->processed = true; + ac->pending--; + if (! rdi->found_deposit) + { + /* Coin wasn't even deposited yet, we do not need to refund it. */ + continue; + } + rdi->rh = TALER_EXCHANGE_post_coins_refund_create ( + TMH_curl_ctx, + ac->current_exchange, + keys, + &rdi->amount_with_fee, + &ac->h_contract_terms, + &rdi->coin_pub, + 0, /* rtransaction_id */ + &ac->hc->instance->merchant_priv); + if (NULL == rdi->rh) + { + GNUNET_break_op (0); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED, + "Failed to start refund with exchange"); + return; + } + GNUNET_assert (TALER_EC_NONE == + TALER_EXCHANGE_post_coins_refund_start (rdi->rh, + &refund_cb, + rdi)); + ac->pending_at_ce++; + } + /* Still continue if no coins for this exchange were deposited. */ + if (0 == ac->pending_at_ce) + find_next_exchange (ac); +} + + +/** + * Begin of the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @param ac abortment context to transact + */ +static void +begin_transaction (struct AbortContext *ac); + + +/** + * Find the exchange we need to talk to for the next + * pending deposit permission. + * + * @param ac abortment context we are processing + */ +static void +find_next_exchange (struct AbortContext *ac) +{ + for (size_t i = 0; i<ac->coins_cnt; i++) + { + struct RefundDetails *rdi = &ac->rd[i]; + + if (! rdi->processed) + { + ac->current_exchange = rdi->exchange_url; + ac->fo = TMH_EXCHANGES_keys4exchange (ac->current_exchange, + false, + &process_abort_with_exchange, + ac); + if (NULL == ac->fo) + { + /* strange, should have happened on pay! */ + GNUNET_break (0); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, + ac->current_exchange); + return; + } + return; + } + } + ac->current_exchange = NULL; + GNUNET_assert (0 == ac->pending); + /* We are done with all the HTTP requests, go back and try + the 'big' database transaction! (It should work now!) */ + begin_transaction (ac); +} + + +/** + * Function called with information about a coin that was deposited. + * + * @param cls closure + * @param exchange_url exchange where @a coin_pub was deposited + * @param coin_pub public key of the coin + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param refund_fee fee the exchange will charge for refunding this coin + */ +static void +refund_coins (void *cls, + const char *exchange_url, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_Amount *refund_fee) +{ + struct AbortContext *ac = cls; + struct GNUNET_TIME_Timestamp now; + + (void) deposit_fee; + (void) refund_fee; + now = GNUNET_TIME_timestamp_get (); + for (size_t i = 0; i<ac->coins_cnt; i++) + { + struct RefundDetails *rdi = &ac->rd[i]; + enum GNUNET_DB_QueryStatus qs; + + if ( (0 != + GNUNET_memcmp (coin_pub, + &rdi->coin_pub)) || + (0 != + strcmp (exchange_url, + rdi->exchange_url)) ) + continue; /* not in request */ + rdi->found_deposit = true; + rdi->amount_with_fee = *amount_with_fee; + /* Store refund in DB */ + qs = TMH_db->refund_coin (TMH_db->cls, + ac->hc->instance->settings.id, + &ac->h_contract_terms, + now, + coin_pub, + /* justification */ + "incomplete abortment aborted"); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + begin_transaction (ac); + return; + } + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "refund_coin"); + return; + } + } /* for all coins */ +} + + +/** + * Begin of the DB transaction. If required (from soft/serialization errors), + * the transaction can be restarted here. + * + * @param ac abortment context to transact + */ +static void +begin_transaction (struct AbortContext *ac) +{ + enum GNUNET_DB_QueryStatus qs; + + /* Avoid re-trying transactions on soft errors forever! */ + if (ac->retry_counter++ > MAX_RETRIES) + { + GNUNET_break (0); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + return; + } + GNUNET_assert (GNUNET_YES == ac->suspended); + + /* First, try to see if we have all we need already done */ + TMH_db->preflight (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "run abort")) + { + GNUNET_break (0); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + return; + } + + /* check payment was indeed incomplete + (now that we are in the transaction scope!) */ + { + struct TALER_PrivateContractHashP h_contract_terms; + bool paid; + + qs = TMH_db->lookup_order_status (TMH_db->cls, + ac->hc->instance->settings.id, + ac->hc->infix, + &h_contract_terms, + &paid); + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_HARD_ERROR: + /* Always report on hard error to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + begin_transaction (ac); + return; + } + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "order status"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + TMH_db->rollback (TMH_db->cls); + resume_abort_with_error (ac, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND, + "Could not find contract"); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (paid) + { + /* Payment is complete, refuse to abort. */ + TMH_db->rollback (TMH_db->cls); + resume_abort_with_error (ac, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE, + "Payment was complete, refusing to abort"); + return; + } + } + if (0 != + GNUNET_memcmp (&ac->h_contract_terms, + &h_contract_terms)) + { + GNUNET_break_op (0); + TMH_db->rollback (TMH_db->cls); + resume_abort_with_error (ac, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH, + "Provided hash does not match order on file"); + return; + } + } + + /* Mark all deposits we have in our database for the order as refunded. */ + qs = TMH_db->lookup_deposits (TMH_db->cls, + ac->hc->instance->settings.id, + &ac->h_contract_terms, + &refund_coins, + ac); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + begin_transaction (ac); + return; + } + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "deposits"); + return; + } + + qs = TMH_db->commit (TMH_db->cls); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + begin_transaction (ac); + return; + } + resume_abort_with_error (ac, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + return; + } + + /* At this point, the refund got correctly committed + into the database. Tell exchange about abort/refund. */ + if (ac->pending > 0) + { + find_next_exchange (ac); + return; + } + generate_success_response (ac); +} + + +/** + * Try to parse the abort request into the given abort context. + * Schedules an error response in the connection on failure. + * + * @param connection HTTP connection we are receiving abortment on + * @param hc context we use to handle the abortment + * @param ac state of the /abort call + * @return #GNUNET_OK on success, + * #GNUNET_NO on failure (response was queued with MHD) + * #GNUNET_SYSERR on hard error (MHD connection must be dropped) + */ +static enum GNUNET_GenericReturnValue +parse_abort (struct MHD_Connection *connection, + struct TMH_HandlerContext *hc, + struct AbortContext *ac) +{ + const json_t *coins; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("coins", + &coins), + GNUNET_JSON_spec_fixed_auto ("h_contract", + &ac->h_contract_terms), + + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + return res; + } + ac->coins_cnt = json_array_size (coins); + if (0 == ac->coins_cnt) + { + GNUNET_break_op (0); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY, + "coins")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + /* note: 1 coin = 1 deposit confirmation expected */ + ac->pending = ac->coins_cnt; + ac->rd = GNUNET_new_array (ac->coins_cnt, + struct RefundDetails); + /* This loop populates the array 'rd' in 'ac' */ + { + unsigned int coins_index; + json_t *coin; + json_array_foreach (coins, coins_index, coin) + { + struct RefundDetails *rd = &ac->rd[coins_index]; + const char *exchange_url; + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_web_url ("exchange_url", + &exchange_url), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &rd->coin_pub), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (connection, + coin, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + return res; + } + rd->exchange_url = GNUNET_strdup (exchange_url); + rd->index = coins_index; + rd->ac = ac; + } + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling /abort for order `%s' with contract hash `%s'\n", + ac->hc->infix, + GNUNET_h2s (&ac->h_contract_terms.hash)); + return GNUNET_OK; +} + + +/** + * Handle a timeout for the processing of the abort request. + * + * @param cls our `struct AbortContext` + */ +static void +handle_abort_timeout (void *cls) +{ + struct AbortContext *ac = cls; + + ac->timeout_task = NULL; + GNUNET_assert (GNUNET_YES == ac->suspended); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming abort with error after timeout\n"); + if (NULL != ac->fo) + { + TMH_EXCHANGES_keys4exchange_cancel (ac->fo); + ac->fo = NULL; + } + resume_abort_with_error (ac, + MHD_HTTP_GATEWAY_TIMEOUT, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, + NULL); +} + + +MHD_RESULT +TMH_post_orders_ID_abort (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct AbortContext *ac = hc->ctx; + + if (NULL == ac) + { + ac = GNUNET_new (struct AbortContext); + GNUNET_CONTAINER_DLL_insert (ac_head, + ac_tail, + ac); + ac->connection = connection; + ac->hc = hc; + hc->ctx = ac; + hc->cc = &abort_context_cleanup; + } + if (GNUNET_SYSERR == ac->suspended) + return MHD_NO; /* during shutdown, we don't generate any more replies */ + if (0 != ac->response_code) + { + MHD_RESULT res; + + /* We are *done* processing the request, + just queue the response (!) */ + if (UINT_MAX == ac->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + res = MHD_queue_response (connection, + ac->response_code, + ac->response); + MHD_destroy_response (ac->response); + ac->response = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /abort (%s).\n", + (unsigned int) ac->response_code, + res ? "OK" : "FAILED"); + return res; + } + { + enum GNUNET_GenericReturnValue ret; + + ret = parse_abort (connection, + hc, + ac); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + + /* Abort not finished, suspend while we interact with the exchange */ + GNUNET_assert (GNUNET_NO == ac->suspended); + MHD_suspend_connection (connection); + ac->suspended = GNUNET_YES; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending abort handling while working with the exchange\n"); + ac->timeout_task = GNUNET_SCHEDULER_add_delayed (ABORT_GENERIC_TIMEOUT, + &handle_abort_timeout, + ac); + begin_transaction (ac); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_post-orders-ORDER_ID-abort.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.h b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + (C) 2014-2020 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/> +*/ +/** + * @file taler-merchant-httpd_post-orders-ORDER_ID-abort.h + * @brief headers for POST /orders/$ID/abort handler + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_ABORT_H +#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_ABORT_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Force all abort contexts to be resumed as we are about + * to shut down MHD. + */ +void +TMH_force_ac_resume (void); + + +/** + * Abort payment for a claimed order. + * + * @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_post_orders_ID_abort (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-claim.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-claim.c @@ -0,0 +1,331 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2016, 2018, 2020 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-httpd_post-orders-ORDER_ID-claim.c + * @brief headers for POST /orders/$ID/claim handler + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_signatures.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-claim.h" + + +/** + * How often do we retry the database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Run transaction to claim @a order_id for @a nonce. + * + * @param hc handler context with information about instance to claim order at + * @param order_id order to claim + * @param nonce nonce to use for the claim + * @param claim_token the token that should be used to verify the claim + * @param[out] contract_terms set to the resulting contract terms + * (for any non-negative result; + * @return transaction status code + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if the order was claimed by a different + * nonce (@a contract_terms set to non-NULL) + * OR if the order is is unknown (@a contract_terms is NULL) + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the order was successfully claimed + */ +static enum GNUNET_DB_QueryStatus +claim_order (struct TMH_HandlerContext *hc, + const char *order_id, + const struct GNUNET_CRYPTO_EddsaPublicKey *nonce, + const struct TALER_ClaimTokenP *claim_token, + json_t **contract_terms) +{ + const char *instance_id = hc->instance->settings.id; + struct TALER_ClaimTokenP order_ct; + enum GNUNET_DB_QueryStatus qs; + uint64_t order_serial; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "claim order")) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + qs = TMH_db->lookup_contract_terms (TMH_db->cls, + instance_id, + order_id, + contract_terms, + &order_serial, + NULL); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + return qs; + } + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + /* We already have claimed contract terms for this order_id */ + struct GNUNET_CRYPTO_EddsaPublicKey stored_nonce; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("nonce", + &stored_nonce), + GNUNET_JSON_spec_end () + }; + + TMH_db->rollback (TMH_db->cls); + GNUNET_assert (NULL != *contract_terms); + + if (GNUNET_OK != + GNUNET_JSON_parse (*contract_terms, + spec, + NULL, + NULL)) + { + /* this should not be possible: contract_terms should always + have a nonce! */ + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + if (0 != + GNUNET_memcmp (&stored_nonce, + nonce)) + { + GNUNET_JSON_parse_free (spec); + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + GNUNET_JSON_parse_free (spec); + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + + GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs); + + /* Now we need to claim the order. */ + { + struct TALER_MerchantPostDataHashP unused; + struct GNUNET_TIME_Timestamp timestamp; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_timestamp ("timestamp", + &timestamp), + GNUNET_JSON_spec_end () + }; + + /* see if we have this order in our table of unclaimed orders */ + qs = TMH_db->lookup_order (TMH_db->cls, + instance_id, + order_id, + &order_ct, + &unused, + contract_terms); + if (0 >= qs) + { + TMH_db->rollback (TMH_db->cls); + return qs; + } + GNUNET_assert (NULL != *contract_terms); + if (GNUNET_OK != + GNUNET_JSON_parse (*contract_terms, + spec, + NULL, + NULL)) + { + /* this should not be possible: contract_terms should always + have a timestamp! */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + GNUNET_assert (0 == + json_object_set_new ( + *contract_terms, + "nonce", + GNUNET_JSON_from_data_auto (nonce))); + if (0 != GNUNET_memcmp_priv (&order_ct, + claim_token)) + { + TMH_db->rollback (TMH_db->cls); + json_decref (*contract_terms); + *contract_terms = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + qs = TMH_db->insert_contract_terms (TMH_db->cls, + instance_id, + order_id, + *contract_terms, + &order_serial); + if (0 >= qs) + { + TMH_db->rollback (TMH_db->cls); + json_decref (*contract_terms); + *contract_terms = NULL; + return qs; + } + // FIXME: unify notifications? or do we need both? + TMH_notify_order_change (TMH_lookup_instance (instance_id), + TMH_OSF_CLAIMED, + timestamp, + order_serial); + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about status change of order %s\n", + order_id); + GNUNET_CRYPTO_hash (order_id, + strlen (order_id), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } + qs = TMH_db->commit (TMH_db->cls); + if (0 > qs) + return qs; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } +} + + +MHD_RESULT +TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *order_id = hc->infix; + struct GNUNET_CRYPTO_EddsaPublicKey nonce; + enum GNUNET_DB_QueryStatus qs; + json_t *contract_terms; + struct TALER_ClaimTokenP claim_token = { 0 }; + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("token", + &claim_token), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + json_dumpf (hc->request_body, + stderr, + JSON_INDENT (2)); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + contract_terms = NULL; + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + TMH_db->preflight (TMH_db->cls); + qs = claim_order (hc, + order_id, + &nonce, + &claim_token, + &contract_terms); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (NULL == contract_terms) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND, + order_id); + /* already claimed! */ + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED, + order_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_assert (NULL != contract_terms); + break; /* Good! return signature (below) */ + } + + /* create contract signature */ + { + struct TALER_PrivateContractHashP hash; + struct TALER_MerchantSignatureP merchant_sig; + + /** + * Hash of the JSON contract in UTF-8 including 0-termination, + * using JSON_COMPACT | JSON_SORT_KEYS + */ + + if (GNUNET_OK != + TALER_JSON_contract_hash (contract_terms, + &hash)) + { + GNUNET_break (0); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + NULL); + } + + TALER_merchant_contract_sign (&hash, + &hc->instance->merchant_priv, + &merchant_sig); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_object_steal ("contract_terms", + contract_terms), + GNUNET_JSON_pack_data_auto ("sig", + &merchant_sig)); + } +} + + +/* end of taler-merchant-httpd_post-orders-ORDER_ID-claim.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-claim.h b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-claim.h @@ -0,0 +1,42 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2020 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/> +*/ +/** + * @file taler-merchant-httpd_post-orders-ORDER_ID-claim.h + * @brief headers for POST /orders/$ID/claim handler + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_POST_ORDERS_ID_CLAIM_H +#define TALER_MERCHANT_HTTPD_POST_ORDERS_ID_CLAIM_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + +/** + * Manage a POST /orders/$ID/claim request. Allows the client to + * claim the order (unless already claims) and creates the respective + * contract. Returns the contract terms. + * + * @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_post_orders_ID_claim (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-paid.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-paid.c @@ -0,0 +1,198 @@ +/* + This file is part of TALER + (C) 2014-2023 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-httpd_post-orders-ORDER_ID-paid.c + * @brief handling of POST /orders/$ID/paid requests + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <taler/taler_dbevents.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-paid.h" + + +/** + * Use database to notify other clients about the + * session being captured. + * + * @param hc http context + * @param session_id the captured session + * @param fulfillment_url the URL that is now paid for by @a session_id + */ +static void +trigger_session_notification (struct TMH_HandlerContext *hc, + const char *session_id, + const char *fulfillment_url) +{ + struct TMH_SessionEventP session_eh = { + .header.size = htons (sizeof (session_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), + .merchant_pub = hc->instance->merchant_pub + }; + + GNUNET_CRYPTO_hash (session_id, + strlen (session_id), + &session_eh.h_session_id); + GNUNET_CRYPTO_hash (fulfillment_url, + strlen (fulfillment_url), + &session_eh.h_fulfillment_url); + TMH_db->event_notify (TMH_db->cls, + &session_eh.header, + NULL, + 0); +} + + +MHD_RESULT +TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *order_id = hc->infix; + struct TALER_MerchantSignatureP merchant_sig; + const char *session_id; + struct TALER_PrivateContractHashP hct; + char *fulfillment_url; + enum GNUNET_DB_QueryStatus qs; + bool refunded; + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("sig", + &merchant_sig), + GNUNET_JSON_spec_fixed_auto ("h_contract", + &hct), + GNUNET_JSON_spec_string ("session_id", + &session_id), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + + if (GNUNET_OK != + TALER_merchant_pay_verify (&hct, + &hc->instance->merchant_pub, + &merchant_sig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID, + NULL); + } + + TMH_db->preflight (TMH_db->cls); + + qs = TMH_db->update_contract_session (TMH_db->cls, + hc->instance->settings.id, + &hct, + session_id, + &fulfillment_url, + &refunded); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unknown order id given: `%s'\n", + order_id); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_contract_session"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_contract_session"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* continued below */ + break; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Marking contract %s with %s/%s as paid\n", + order_id, + session_id, + fulfillment_url); + + /* Wake everybody up who waits for this fulfillment_url and session_id */ + if ( (NULL != fulfillment_url) && + (NULL != session_id) ) + trigger_session_notification (hc, + session_id, + fulfillment_url); + /*Trigger webhook */ + /*Commented out until its purpose is defined + { + enum GNUNET_DB_QueryStatus qs; + json_t *jhook; + + jhook = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("contract_terms", + contract_terms), + GNUNET_JSON_pack_string ("order_id", + order_id) + ); + GNUNET_assert (NULL != jhook); + qs = TMH_trigger_webhook (hc->instance->settings.id, + "paid", + jhook); + json_decref (jhook); + if (qs < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to init the webhook for contract %s with %s/%s as paid\n", + order_id, + session_id, + fulfillment_url); + } + }*/ + GNUNET_free (fulfillment_url); + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_bool ("refunded", + refunded)); +} + + +/* end of taler-merchant-httpd_post-orders-ORDER_ID-paid.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-paid.h b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-paid.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + (C) 2020 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/> +*/ +/** + * @file taler-merchant-httpd_post-orders-ORDER_ID-paid.h + * @brief headers for POST /orders/$ID/paid handler + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAID_H +#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAID_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Process proof of payment for a paid order. + * + * @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_post_orders_ID_paid (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c @@ -0,0 +1,5309 @@ +/* + This file is part of TALER + (C) 2014-2026 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-httpd_post-orders-ORDER_ID-pay.c + * @brief handling of POST /orders/$ID/pay requests + * @author Marcello Stanisci + * @author Christian Grothoff + * @author Florian Dold + */ +#include "taler/platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_db_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_time_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_get-exchanges.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-pay.h" +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler/taler_merchant_util.h" +#include "taler/taler_merchantdb_plugin.h" + +#ifdef HAVE_DONAU_DONAU_SERVICE_H +#include <donau/donau_service.h> +#include <donau/donau_util.h> +#include <donau/donau_json_lib.h> +#endif + +/** + * How often do we retry the (complex!) database transaction? + */ +#define MAX_RETRIES 5 + +/** + * Maximum number of coins that we allow per transaction. + * Note that the limit for each batch deposit request to + * the exchange is lower, so we may break a very large + * number of coins up into multiple smaller requests to + * the exchange. + */ +#define MAX_COIN_ALLOWED_COINS 1024 + +/** + * Maximum number of tokens that we allow as inputs per transaction + */ +#define MAX_TOKEN_ALLOWED_INPUTS 64 + +/** + * Maximum number of tokens that we allow as outputs per transaction + */ +#define MAX_TOKEN_ALLOWED_OUTPUTS 64 + +/** + * How often do we ask the exchange again about our + * KYC status? Very rarely, as if the user actively + * changes it, we should usually notice anyway. + */ +#define KYC_RETRY_FREQUENCY GNUNET_TIME_UNIT_WEEKS + +/** + * Information we keep for an individual call to the pay handler. + */ +struct PayContext; + + +/** + * Different phases of processing the /pay request. + */ +enum PayPhase +{ + /** + * Initial phase where the request is parsed. + */ + PP_PARSE_PAY = 0, + + /** + * Parse wallet data object from the pay request. + */ + PP_PARSE_WALLET_DATA, + + /** + * Check database state for the given order. + */ + PP_CHECK_CONTRACT, + + /** + * Validate provided tokens and token envelopes. + */ + PP_VALIDATE_TOKENS, + + /** + * Check if contract has been paid. + */ + PP_CONTRACT_PAID, + + /** + * Compute money pot changes. + */ + PP_COMPUTE_MONEY_POTS, + + /** + * Execute payment transaction. + */ + PP_PAY_TRANSACTION, + + /** + * Communicate with DONAU to generate a donation receipt from the donor BUDIs. + */ + PP_REQUEST_DONATION_RECEIPT, + + /** + * Process the donation receipt response from DONAU (save the donau_sigs to the db). + */ + PP_FINAL_OUTPUT_TOKEN_PROCESSING, + + /** + * Notify other processes about successful payment. + */ + PP_PAYMENT_NOTIFICATION, + + /** + * Create final success response. + */ + PP_SUCCESS_RESPONSE, + + /** + * Perform batch deposits with exchange(s). + */ + PP_BATCH_DEPOSITS, + + /** + * Return response in payment context. + */ + PP_RETURN_RESPONSE, + + /** + * An exchange denied a deposit, fail for + * legal reasons. + */ + PP_FAIL_LEGAL_REASONS, + + /** + * Return #MHD_YES to end processing. + */ + PP_END_YES, + + /** + * Return #MHD_NO to end processing. + */ + PP_END_NO +}; + + +/** + * Information kept during a pay request for each coin. + */ +struct DepositConfirmation +{ + + /** + * Reference to the main PayContext + */ + struct PayContext *pc; + + /** + * URL of the exchange that issued this coin. + */ + char *exchange_url; + + /** + * Details about the coin being deposited. + */ + struct TALER_EXCHANGE_CoinDepositDetail cdd; + + /** + * Fee charged by the exchange for the deposit operation of this coin. + */ + struct TALER_Amount deposit_fee; + + /** + * Fee charged by the exchange for the refund operation of this coin. + */ + struct TALER_Amount refund_fee; + + /** + * If a minimum age was required (i. e. pc->minimum_age is large enough), + * this is the signature of the minimum age (as a single uint8_t), using the + * private key to the corresponding age group. Might be all zeroes for no + * age attestation. + */ + struct TALER_AgeAttestationP minimum_age_sig; + + /** + * If a minimum age was required (i. e. pc->minimum_age is large enough), + * this is the age commitment (i. e. age mask and vector of EdDSA public + * keys, one per age group) that went into the mining of the coin. The + * SHA256 hash of the mask and the vector of public keys was bound to the + * key. + */ + struct TALER_AgeCommitment age_commitment; + + /** + * Age mask in the denomination that defines the age groups. Only + * applicable, if minimum age was required. + */ + struct TALER_AgeMask age_mask; + + /** + * Offset of this coin into the `dc` array of all coins in the + * @e pc. + */ + unsigned int index; + + /** + * true, if no field "age_commitment" was found in the JSON blob + */ + bool no_age_commitment; + + /** + * True, if no field "minimum_age_sig" was found in the JSON blob + */ + bool no_minimum_age_sig; + + /** + * true, if no field "h_age_commitment" was found in the JSON blob + */ + bool no_h_age_commitment; + + /** + * true if we found this coin in the database. + */ + bool found_in_db; + + /** + * true if we #deposit_paid_check() matched this coin in the database. + */ + bool matched_in_db; + +}; + +struct TokenUseConfirmation +{ + + /** + * Signature on the deposit request made using the token use private key. + */ + struct TALER_TokenUseSignatureP sig; + + /** + * Token use public key. This key was blindly signed by the merchant during + * the token issuance process. + */ + struct TALER_TokenUsePublicKeyP pub; + + /** + * Unblinded signature on the token use public key done by the merchant. + */ + struct TALER_TokenIssueSignature unblinded_sig; + + /** + * Hash of the token issue public key associated with this token. + * Note this is set in the validate_tokens phase. + */ + struct TALER_TokenIssuePublicKeyHashP h_issue; + + /** + * true if we found this token in the database. + */ + bool found_in_db; + +}; + + +/** + * Information about a token envelope. + */ +struct TokenEnvelope +{ + + /** + * Blinded token use public keys waiting to be signed. + */ + struct TALER_TokenEnvelope blinded_token; + +}; + + +/** + * (Blindly) signed token to be returned to the wallet. + */ +struct SignedOutputToken +{ + + /** + * Index of the output token that produced + * this blindly signed token. + */ + unsigned int output_index; + + /** + * Blinded token use public keys waiting to be signed. + */ + struct TALER_BlindedTokenIssueSignature sig; + + /** + * Hash of token issue public key. + */ + struct TALER_TokenIssuePublicKeyHashP h_issue; + +}; + + +/** + * Information kept during a pay request for each exchange. + */ +struct ExchangeGroup +{ + + /** + * Payment context this group is part of. + */ + struct PayContext *pc; + + /** + * Handle to the batch deposit operation we are performing for this + * exchange, NULL after the operation is done. + */ + struct TALER_EXCHANGE_PostBatchDepositHandle *bdh; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_KeysOperation *fo; + + /** + * URL of the exchange that issued this coin. Aliases + * the exchange URL of one of the coins, do not free! + */ + const char *exchange_url; + + /** + * Total deposit amount in this exchange group. + */ + struct TALER_Amount total; + + /** + * Wire fee that applies to this exchange for the + * given payment context's wire method. + */ + struct TALER_Amount wire_fee; + + /** + * true if we already tried a forced /keys download. + */ + bool tried_force_keys; + + /** + * Did this exchange deny the transaction for legal reasons? + */ + bool got_451; +}; + + +/** + * Information about donau, that can be fetched even + * if the merhchant doesn't support donau + */ +struct DonauData +{ + /** + * The user-selected Donau URL. + */ + char *donau_url; + + /** + * The donation year, as parsed from "year". + */ + uint64_t donation_year; + + /** + * The original BUDI key-pairs array from the donor + * to be used for the receipt creation. + */ + const json_t *budikeypairs; +}; + +/** + * Information we keep for an individual call to the /pay handler. + */ +struct PayContext +{ + + /** + * Stored in a DLL. + */ + struct PayContext *next; + + /** + * Stored in a DLL. + */ + struct PayContext *prev; + + /** + * MHD connection to return to + */ + struct MHD_Connection *connection; + + /** + * Details about the client's request. + */ + struct TMH_HandlerContext *hc; + + /** + * Transaction ID given in @e root. + */ + const char *order_id; + + /** + * Response to return, NULL if we don't have one yet. + */ + struct MHD_Response *response; + + /** + * Array with @e output_tokens_len signed tokens returned in + * the response to the wallet. + */ + struct SignedOutputToken *output_tokens; + + /** + * Number of output tokens to return in the response. + * Length of the @e output_tokens array. + */ + unsigned int output_tokens_len; + + /** + * Counter used to generate the output index in append_output_token_sig(). + */ + unsigned int output_index_gen; + + /** + * Counter used to generate the output index in append_output_token_sig(). + * + * Counts the generated tokens _within_ the current output_index_gen. + */ + unsigned int output_token_cnt; + + /** + * HTTP status code to use for the reply, i.e 200 for "OK". + * Special value UINT_MAX is used to indicate hard errors + * (no reply, return #MHD_NO). + */ + unsigned int response_code; + + /** + * Payment processing phase we are in. + */ + enum PayPhase phase; + + /** + * #GNUNET_NO if the @e connection was not suspended, + * #GNUNET_YES if the @e connection was suspended, + * #GNUNET_SYSERR if @e connection was resumed to as + * part of #MH_force_pc_resume during shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Results from the phase_parse_pay() + */ + struct + { + + /** + * Array with @e num_exchanges exchanges we are depositing + * coins into. + */ + struct ExchangeGroup **egs; + + /** + * Array with @e coins_cnt coins we are despositing. + */ + struct DepositConfirmation *dc; + + /** + * Array with @e tokens_cnt input tokens passed to this request. + */ + struct TokenUseConfirmation *tokens; + + /** + * Optional session id given in @e root. + * NULL if not given. + */ + char *session_id; + + /** + * Wallet data json object from the request. Containing additional + * wallet data such as the selected choice_index. + */ + const json_t *wallet_data; + + /** + * Number of coins this payment is made of. Length + * of the @e dc array. + */ + size_t coins_cnt; + + /** + * Number of input tokens passed to this request. Length + * of the @e tokens array. + */ + size_t tokens_cnt; + + /** + * Number of exchanges involved in the payment. Length + * of the @e eg array. + */ + unsigned int num_exchanges; + + } parse_pay; + + /** + * Results from the phase_wallet_data() + */ + struct + { + + /** + * Array with @e token_envelopes_cnt (blinded) token envelopes. + */ + struct TokenEnvelope *token_envelopes; + + /** + * Index of selected choice in the @e contract_terms choices array. + */ + int16_t choice_index; + + /** + * Number of token envelopes passed to this request. + * Length of the @e token_envelopes array. + */ + size_t token_envelopes_cnt; + + /** + * Hash of the canonicalized wallet data json object. + */ + struct GNUNET_HashCode h_wallet_data; + + /** + * Donau related information + */ + struct DonauData donau; + + /** + * Serial from the DB of the donau instance that we are using + */ + uint64_t donau_instance_serial; + +#ifdef HAVE_DONAU_DONAU_SERVICE_H + /** + * Number of the blinded key pairs @e bkps + */ + unsigned int num_bkps; + + /** + * Blinded key pairs received from the wallet + */ + struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps; + + /** + * The id of the charity as saved on the donau. + */ + uint64_t charity_id; + + /** + * Private key of the charity(related to the private key of the merchant). + */ + struct DONAU_CharityPrivateKeyP charity_priv; + + /** + * Maximum amount of donations that the charity can receive per year. + */ + struct TALER_Amount charity_max_per_year; + + /** + * Amount of donations that the charity has received so far this year. + */ + struct TALER_Amount charity_receipts_to_date; + + /** + * Donau keys, that we are using to get the information about the bkps. + */ + struct DONAU_Keys *donau_keys; + + /** + * Amount from BKPS + */ + struct TALER_Amount donation_amount; +#endif + + } parse_wallet_data; + + /** + * Results from the phase_check_contract() + */ + struct + { + + /** + * Hashed @e contract_terms. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * Our contract (or NULL if not available). + */ + json_t *contract_terms_json; + + /** + * Parsed contract terms, NULL when parsing failed. + */ + struct TALER_MERCHANT_Contract *contract_terms; + + /** + * What wire method (of the @e mi) was selected by the wallet? + * Set in #phase_parse_pay(). + */ + struct TMH_WireMethod *wm; + + /** + * Set to the POS key, if applicable for this order. + */ + char *pos_key; + + /** + * Serial number of this order in the database (set once we did the lookup). + */ + uint64_t order_serial; + + /** + * Algorithm chosen for generating the confirmation code. + */ + enum TALER_MerchantConfirmationAlgorithm pos_alg; + + } check_contract; + + /** + * Results from the phase_validate_tokens() + */ + struct + { + + /** + * Maximum fee the merchant is willing to pay, from @e root. + * Note that IF the total fee of the exchange is higher, that is + * acceptable to the merchant if the customer is willing to + * pay the difference + * (i.e. amount - max_fee <= actual_amount - actual_fee). + */ + struct TALER_Amount max_fee; + + /** + * Amount from @e root. This is the amount the merchant expects + * to make, minus @e max_fee. + */ + struct TALER_Amount brutto; + + /** + * Index of the donau output in the list of tokens. + * Set to -1 if no donau output exists. + */ + int donau_output_index; + + } validate_tokens; + + + struct + { + /** + * Length of the @a pots and @a increments arrays. + */ + unsigned int num_pots; + + /** + * Serial IDs of money pots to increment. + */ + uint64_t *pots; + + /** + * Increment for the respective money pot. + */ + struct TALER_Amount *increments; + + /** + * True if the money pots have already been computed. + */ + bool pots_computed; + + } compute_money_pots; + + /** + * Results from the phase_execute_pay_transaction() + */ + struct + { + + /** + * Considering all the coins with the "found_in_db" flag + * set, what is the total amount we were so far paid on + * this contract? + */ + struct TALER_Amount total_paid; + + /** + * Considering all the coins with the "found_in_db" flag + * set, what is the total amount we had to pay in deposit + * fees so far on this contract? + */ + struct TALER_Amount total_fees_paid; + + /** + * Considering all the coins with the "found_in_db" flag + * set, what is the total amount we already refunded? + */ + struct TALER_Amount total_refunded; + + /** + * Number of coin deposits pending. + */ + unsigned int pending; + + /** + * How often have we retried the 'main' transaction? + */ + unsigned int retry_counter; + + /** + * Set to true if the deposit currency of a coin + * does not match the contract currency. + */ + bool deposit_currency_mismatch; + + /** + * Set to true if the database contains a (bogus) + * refund for a different currency. + */ + bool refund_currency_mismatch; + + } pay_transaction; + + /** + * Results from the phase_batch_deposits() + */ + struct + { + + /** + * Task called when the (suspended) processing for + * the /pay request times out. + * Happens when we don't get a response from the exchange. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Number of batch transactions pending. + */ + unsigned int pending_at_eg; + + /** + * Did any exchange deny a deposit for legal reasons? + */ + bool got_451; + + } batch_deposits; + +#ifdef HAVE_DONAU_DONAU_SERVICE_H + /** + * Struct for #phase_request_donation_receipt() + */ + struct + { + /** + * Handler of the donau request + */ + struct DONAU_BatchIssueReceiptHandle *birh; + + } donau_receipt; +#endif +}; + + +/** + * Head of active pay context DLL. + */ +static struct PayContext *pc_head; + +/** + * Tail of active pay context DLL. + */ +static struct PayContext *pc_tail; + + +void +TMH_force_pc_resume () +{ + for (struct PayContext *pc = pc_head; + NULL != pc; + pc = pc->next) + { + if (NULL != pc->batch_deposits.timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->batch_deposits.timeout_task); + pc->batch_deposits.timeout_task = NULL; + } + if (GNUNET_YES == pc->suspended) + { + pc->suspended = GNUNET_SYSERR; + MHD_resume_connection (pc->connection); + } + } +} + + +/** + * Resume payment processing. + * + * @param[in,out] pc payment process to resume + */ +static void +pay_resume (struct PayContext *pc) +{ + GNUNET_assert (GNUNET_YES == pc->suspended); + pc->suspended = GNUNET_NO; + MHD_resume_connection (pc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Resume the given pay context and send the given response. + * Stores the response in the @a pc and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param pc payment context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_pay_with_response (struct PayContext *pc, + unsigned int response_code, + struct MHD_Response *response) +{ + pc->response_code = response_code; + pc->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /pay handling. HTTP status for our reply is %u.\n", + response_code); + for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) + { + struct ExchangeGroup *eg = pc->parse_pay.egs[i]; + + if (NULL != eg->fo) + { + TMH_EXCHANGES_keys4exchange_cancel (eg->fo); + eg->fo = NULL; + pc->batch_deposits.pending_at_eg--; + } + if (NULL != eg->bdh) + { + TALER_EXCHANGE_post_batch_deposit_cancel (eg->bdh); + eg->bdh = NULL; + pc->batch_deposits.pending_at_eg--; + } + } + GNUNET_assert (0 == pc->batch_deposits.pending_at_eg); + if (NULL != pc->batch_deposits.timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->batch_deposits.timeout_task); + pc->batch_deposits.timeout_task = NULL; + } + pc->phase = PP_RETURN_RESPONSE; + pay_resume (pc); +} + + +/** + * Resume payment processing with an error. + * + * @param pc operation to resume + * @param ec taler error code to return + * @param msg human readable error message + */ +static void +resume_pay_with_error (struct PayContext *pc, + enum TALER_ErrorCode ec, + const char *msg) +{ + resume_pay_with_response ( + pc, + TALER_ErrorCode_get_http_status_safe (ec), + TALER_MHD_make_error (ec, + msg)); +} + + +/** + * Conclude payment processing for @a pc with the + * given @a res MHD status code. + * + * @param[in,out] pc payment context for final state transition + * @param res MHD return code to end with + */ +static void +pay_end (struct PayContext *pc, + MHD_RESULT res) +{ + pc->phase = (MHD_YES == res) + ? PP_END_YES + : PP_END_NO; +} + + +/** + * Return response stored in @a pc. + * + * @param[in,out] pc payment context we are processing + */ +static void +phase_return_response (struct PayContext *pc) +{ + GNUNET_assert (0 != pc->response_code); + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == pc->response_code) + { + GNUNET_break (0); + pay_end (pc, + MHD_NO); /* hard error */ + return; + } + pay_end (pc, + MHD_queue_response (pc->connection, + pc->response_code, + pc->response)); +} + + +/** + * Return a response indicating failure for legal reasons. + * + * @param[in,out] pc payment context we are processing + */ +static void +phase_fail_for_legal_reasons (struct PayContext *pc) +{ + json_t *exchanges; + + GNUNET_assert (0 == pc->pay_transaction.pending); + GNUNET_assert (pc->batch_deposits.got_451); + exchanges = json_array (); + GNUNET_assert (NULL != exchanges); + for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) + { + struct ExchangeGroup *eg = pc->parse_pay.egs[i]; + + GNUNET_assert (NULL == eg->fo); + GNUNET_assert (NULL == eg->bdh); + if (! eg->got_451) + continue; + GNUNET_assert ( + 0 == + json_array_append_new ( + exchanges, + json_string (eg->exchange_url))); + } + pay_end (pc, + TALER_MHD_REPLY_JSON_PACK ( + pc->connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED), + GNUNET_JSON_pack_array_steal ("exchange_base_urls", + exchanges))); +} + + +/** + * Do database transaction for a completed batch deposit. + * + * @param eg group that completed + * @param dr response from the server + * @return transaction status + */ +static enum GNUNET_DB_QueryStatus +batch_deposit_transaction (const struct ExchangeGroup *eg, + const struct + TALER_EXCHANGE_PostBatchDepositResponse *dr) +{ + const struct PayContext *pc = eg->pc; + enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount total_without_fees; + uint64_t b_dep_serial; + uint32_t off = 0; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &total_without_fees)); + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + struct TALER_Amount amount_without_fees; + + /* might want to group deposits by batch more explicitly ... */ + if (0 != strcmp (eg->exchange_url, + dc->exchange_url)) + continue; + if (dc->found_in_db) + continue; + GNUNET_assert (0 <= + TALER_amount_subtract (&amount_without_fees, + &dc->cdd.amount, + &dc->deposit_fee)); + GNUNET_assert (0 <= + TALER_amount_add (&total_without_fees, + &total_without_fees, + &amount_without_fees)); + } + qs = TMH_db->insert_deposit_confirmation ( + TMH_db->cls, + pc->hc->instance->settings.id, + dr->details.ok.deposit_timestamp, + &pc->check_contract.h_contract_terms, + eg->exchange_url, + pc->check_contract.contract_terms->wire_deadline, + &total_without_fees, + &eg->wire_fee, + &pc->check_contract.wm->h_wire, + dr->details.ok.exchange_sig, + dr->details.ok.exchange_pub, + &b_dep_serial); + if (qs <= 0) + return qs; /* Entire batch already known or failure, we're done */ + + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + /* might want to group deposits by batch more explicitly ... */ + if (0 != strcmp (eg->exchange_url, + dc->exchange_url)) + continue; + if (dc->found_in_db) + continue; + /* FIXME-#9457: We might want to check if the order was fully paid concurrently + by some other wallet here, and if so, issue an auto-refund. Right now, + it is possible to over-pay if two wallets literally make a concurrent + payment, as the earlier check for 'paid' is not in the same transaction + scope as this 'insert' operation. */ + qs = TMH_db->insert_deposit ( + TMH_db->cls, + off++, /* might want to group deposits by batch more explicitly ... */ + b_dep_serial, + &dc->cdd.coin_pub, + &dc->cdd.coin_sig, + &dc->cdd.amount, + &dc->deposit_fee, + &dc->refund_fee, + GNUNET_TIME_absolute_add ( + pc->check_contract.contract_terms->wire_deadline.abs_time, + GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES))); + if (qs < 0) + return qs; + GNUNET_break (qs > 0); + } + return qs; +} + + +/** + * Handle case where the batch deposit completed + * with a status of #MHD_HTTP_OK. + * + * @param eg group that completed + * @param dr response from the server + */ +static void +handle_batch_deposit_ok (struct ExchangeGroup *eg, + const struct TALER_EXCHANGE_PostBatchDepositResponse * + dr) +{ + struct PayContext *pc = eg->pc; + enum GNUNET_DB_QueryStatus qs + = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + + /* store result to DB */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Storing successful payment %s (%s) at instance `%s'\n", + pc->hc->infix, + GNUNET_h2s (&pc->check_contract.h_contract_terms.hash), + pc->hc->instance->settings.id); + for (unsigned int r = 0; r<MAX_RETRIES; r++) + { + TMH_db->preflight (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "batch-deposit-insert-confirmation")) + { + resume_pay_with_response ( + pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_GENERIC_DB_START_FAILED), + TMH_pack_exchange_reply (&dr->hr))); + return; + } + qs = batch_deposit_transaction (eg, + dr); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + TMH_db->rollback (TMH_db->cls); + continue; + } + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + resume_pay_with_error (pc, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "batch_deposit_transaction"); + TMH_db->rollback (TMH_db->cls); + return; + } + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + TMH_db->rollback (TMH_db->cls); + continue; + } + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + resume_pay_with_error (pc, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "insert_deposit"); + } + break; /* DB transaction succeeded */ + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + resume_pay_with_error (pc, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "insert_deposit"); + return; + } + + /* Transaction is done, mark affected coins as complete as well. */ + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + if (0 != strcmp (eg->exchange_url, + pc->parse_pay.dc[i].exchange_url)) + continue; + if (dc->found_in_db) + continue; + dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */ + pc->pay_transaction.pending--; + } +} + + +/** + * Notify taler-merchant-kyccheck that we got a KYC + * rule violation notification and should start to + * check our KYC status. + * + * @param eg exchange group we were notified for + */ +static void +notify_kyc_required (const struct ExchangeGroup *eg) +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) + }; + char *hws; + char *extra; + + hws = GNUNET_STRINGS_data_to_string_alloc ( + &eg->pc->check_contract.contract_terms->h_wire, + sizeof (eg->pc->check_contract.contract_terms->h_wire)); + GNUNET_asprintf (&extra, + "%s %s", + hws, + eg->exchange_url); + GNUNET_free (hws); + TMH_db->event_notify (TMH_db->cls, + &es, + extra, + strlen (extra) + 1); + GNUNET_free (extra); +} + + +/** + * Callback to handle a batch deposit permission's response. + * + * @param cls a `struct ExchangeGroup` + * @param dr HTTP response code details + */ +static void +batch_deposit_cb ( + void *cls, + const struct TALER_EXCHANGE_PostBatchDepositResponse *dr) +{ + struct ExchangeGroup *eg = cls; + struct PayContext *pc = eg->pc; + + eg->bdh = NULL; + pc->batch_deposits.pending_at_eg--; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Batch deposit completed with status %u\n", + dr->hr.http_status); + GNUNET_assert (GNUNET_YES == pc->suspended); + switch (dr->hr.http_status) + { + case MHD_HTTP_OK: + handle_batch_deposit_ok (eg, + dr); + if ( (GNUNET_YES == pc->suspended) && + (0 == pc->batch_deposits.pending_at_eg) ) + { + pc->phase = PP_COMPUTE_MONEY_POTS; + pay_resume (pc); + } + return; + case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: + notify_kyc_required (eg); + eg->got_451 = true; + pc->batch_deposits.got_451 = true; + /* update pc->pay_transaction.pending */ + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + if (0 != strcmp (eg->exchange_url, + pc->parse_pay.dc[i].exchange_url)) + continue; + if (dc->found_in_db) + continue; + pc->pay_transaction.pending--; + } + if (0 == pc->batch_deposits.pending_at_eg) + { + pc->phase = PP_COMPUTE_MONEY_POTS; + pay_resume (pc); + } + return; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Deposit operation failed with HTTP code %u/%d\n", + dr->hr.http_status, + (int) dr->hr.ec); + /* Transaction failed */ + if (5 == dr->hr.http_status / 100) + { + /* internal server error at exchange */ + resume_pay_with_response (pc, + MHD_HTTP_BAD_GATEWAY, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), + TMH_pack_exchange_reply (&dr->hr))); + return; + } + if (NULL == dr->hr.reply) + { + /* We can't do anything meaningful here, the exchange did something wrong */ + resume_pay_with_response ( + pc, + MHD_HTTP_BAD_GATEWAY, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED), + TMH_pack_exchange_reply (&dr->hr))); + return; + } + + /* Forward error, adding the "exchange_url" for which the + error was being generated */ + if (TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS == dr->hr.ec) + { + resume_pay_with_response ( + pc, + MHD_HTTP_CONFLICT, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS), + TMH_pack_exchange_reply (&dr->hr), + GNUNET_JSON_pack_string ("exchange_url", + eg->exchange_url))); + return; + } + resume_pay_with_response ( + pc, + MHD_HTTP_BAD_GATEWAY, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), + TMH_pack_exchange_reply (&dr->hr), + GNUNET_JSON_pack_string ("exchange_url", + eg->exchange_url))); + return; + } /* end switch */ +} + + +/** + * Force re-downloading keys for @a eg. + * + * @param[in,out] eg group to re-download keys for + */ +static void +force_keys (struct ExchangeGroup *eg); + + +/** + * Function called with the result of our exchange keys lookup. + * + * @param cls the `struct ExchangeGroup` + * @param keys the keys of the exchange + * @param exchange representation of the exchange + */ +static void +process_pay_with_keys ( + void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct ExchangeGroup *eg = cls; + struct PayContext *pc = eg->pc; + struct TMH_HandlerContext *hc = pc->hc; + unsigned int group_size; + struct TALER_Amount max_amount; + enum TMH_ExchangeStatus es; + + eg->fo = NULL; + pc->batch_deposits.pending_at_eg--; + GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing payment with keys from exchange %s\n", + eg->exchange_url); + GNUNET_assert (GNUNET_YES == pc->suspended); + if (NULL == keys) + { + GNUNET_break_op (0); + resume_pay_with_error ( + pc, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, + NULL); + return; + } + if (! TMH_EXCHANGES_is_below_limit (keys, + TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION, + &eg->total)) + { + GNUNET_break_op (0); + resume_pay_with_error ( + pc, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, + eg->exchange_url); + return; + } + + max_amount = eg->total; + es = TMH_exchange_check_debit ( + pc->hc->instance->settings.id, + exchange, + pc->check_contract.wm, + &max_amount); + if ( (TMH_ES_OK != es) && + (TMH_ES_RETRY_OK != es) ) + { + if (eg->tried_force_keys || + (0 == (TMH_ES_RETRY_OK & es)) ) + { + GNUNET_break_op (0); + resume_pay_with_error ( + pc, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED, + NULL); + return; + } + force_keys (eg); + return; + } + if (-1 == + TALER_amount_cmp (&max_amount, + &eg->total)) + { + /* max_amount < eg->total */ + GNUNET_break_op (0); + resume_pay_with_error ( + pc, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, + eg->exchange_url); + return; + } + + if (GNUNET_OK != + TMH_EXCHANGES_lookup_wire_fee (exchange, + pc->check_contract.wm->wire_method, + &eg->wire_fee)) + { + if (eg->tried_force_keys) + { + GNUNET_break_op (0); + resume_pay_with_error ( + pc, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED, + pc->check_contract.wm->wire_method); + return; + } + force_keys (eg); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got wire data for %s\n", + eg->exchange_url); + + /* Initiate /batch-deposit operation for all coins of + the current exchange (!) */ + group_size = 0; + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + const struct TALER_EXCHANGE_DenomPublicKey *denom_details; + bool is_age_restricted_denom = false; + + if (0 != strcmp (eg->exchange_url, + pc->parse_pay.dc[i].exchange_url)) + continue; + if (dc->found_in_db) + continue; + + denom_details + = TALER_EXCHANGE_get_denomination_key_by_hash (keys, + &dc->cdd.h_denom_pub); + if (NULL == denom_details) + { + if (eg->tried_force_keys) + { + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + MHD_HTTP_BAD_REQUEST, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &dc->cdd.h_denom_pub), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ( + "exchange_keys", + TALER_EXCHANGE_keys_to_json (keys))))); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Missing denomination %s from exchange %s, updating keys\n", + GNUNET_h2s (&dc->cdd.h_denom_pub.hash), + eg->exchange_url); + force_keys (eg); + return; + } + dc->deposit_fee = denom_details->fees.deposit; + dc->refund_fee = denom_details->fees.refund; + + if (GNUNET_TIME_absolute_is_past ( + denom_details->expire_deposit.abs_time)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Denomination key offered by client has expired for deposits\n"); + resume_pay_with_response ( + pc, + MHD_HTTP_GONE, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &denom_details->h_key))); + return; + } + + /* Now that we have the details about the denomination, we can verify age + * restriction requirements, if applicable. Note that denominations with an + * age_mask equal to zero always pass the age verification. */ + is_age_restricted_denom = (0 != denom_details->key.age_mask.bits); + + if (is_age_restricted_denom && + (0 < pc->check_contract.contract_terms->minimum_age)) + { + /* Minimum age given and restricted coin provided: We need to verify the + * minimum age */ + unsigned int code = 0; + + if (dc->no_age_commitment) + { + GNUNET_break_op (0); + code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING; + goto AGE_FAIL; + } + dc->age_commitment.mask = denom_details->key.age_mask; + if (((int) (dc->age_commitment.num + 1)) != + __builtin_popcount (dc->age_commitment.mask.bits)) + { + GNUNET_break_op (0); + code = + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH; + goto AGE_FAIL; + } + if (GNUNET_OK != + TALER_age_commitment_verify ( + &dc->age_commitment, + pc->check_contract.contract_terms->minimum_age, + &dc->minimum_age_sig)) + code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; +AGE_FAIL: + if (0 < code) + { + GNUNET_break_op (0); + TALER_age_commitment_free (&dc->age_commitment); + resume_pay_with_response ( + pc, + MHD_HTTP_BAD_REQUEST, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec (code), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &denom_details->h_key))); + return; + } + + /* Age restriction successfully verified! + * Calculate the hash of the age commitment. */ + TALER_age_commitment_hash (&dc->age_commitment, + &dc->cdd.h_age_commitment); + TALER_age_commitment_free (&dc->age_commitment); + } + else if (is_age_restricted_denom && + dc->no_h_age_commitment) + { + /* The contract did not ask for a minimum_age but the client paid + * with a coin that has age restriction enabled. We lack the hash + * of the age commitment in this case in order to verify the coin + * and to deposit it with the exchange. */ + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + MHD_HTTP_BAD_REQUEST, + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &denom_details->h_key))); + return; + } + group_size++; + } + + if (0 == group_size) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Group size zero, %u batch transactions remain pending\n", + pc->batch_deposits.pending_at_eg); + if (0 == pc->batch_deposits.pending_at_eg) + { + pc->phase = PP_COMPUTE_MONEY_POTS; + pay_resume (pc); + return; + } + return; + } + if (group_size > TALER_MAX_COINS) + group_size = TALER_MAX_COINS; + { + struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; + struct TALER_EXCHANGE_DepositContractDetail dcd = { + .wire_deadline = pc->check_contract.contract_terms->wire_deadline, + .merchant_payto_uri = pc->check_contract.wm->payto_uri, + .extra_wire_subject_metadata = pc->check_contract.wm-> + extra_wire_subject_metadata, + .wire_salt = pc->check_contract.wm->wire_salt, + .h_contract_terms = pc->check_contract.h_contract_terms, + .wallet_data_hash = pc->parse_wallet_data.h_wallet_data, + .wallet_timestamp = pc->check_contract.contract_terms->timestamp, + .merchant_pub = hc->instance->merchant_pub, + .refund_deadline = pc->check_contract.contract_terms->refund_deadline + }; + enum TALER_ErrorCode ec; + size_t off = 0; + + TALER_merchant_contract_sign (&pc->check_contract.h_contract_terms, + &pc->hc->instance->merchant_priv, + &dcd.merchant_sig); + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + if (dc->found_in_db) + continue; + if (0 != strcmp (dc->exchange_url, + eg->exchange_url)) + continue; + cdds[off++] = dc->cdd; + if (off >= group_size) + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Initiating batch deposit with %u coins\n", + group_size); + /* Note: the coin signatures over the wallet_data_hash are + checked inside of this call */ + eg->bdh = TALER_EXCHANGE_post_batch_deposit_create ( + TMH_curl_ctx, + eg->exchange_url, + keys, + &dcd, + group_size, + cdds, + &ec); + if (NULL == eg->bdh) + { + /* Signature was invalid or some other constraint was not satisfied. If + the exchange was unavailable, we'd get that information in the + callback. */ + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + TALER_ErrorCode_get_http_status_safe (ec), + TALER_MHD_MAKE_JSON_PACK ( + TALER_JSON_pack_ec (ec), + GNUNET_JSON_pack_string ("exchange_url", + eg->exchange_url))); + return; + } + pc->batch_deposits.pending_at_eg++; + if (TMH_force_audit) + TALER_EXCHANGE_post_batch_deposit_force_dc (eg->bdh); + TALER_EXCHANGE_post_batch_deposit_start (eg->bdh, + &batch_deposit_cb, + eg); + } +} + + +static void +force_keys (struct ExchangeGroup *eg) +{ + struct PayContext *pc = eg->pc; + + eg->tried_force_keys = true; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Forcing /keys download (once)\n"); + eg->fo = TMH_EXCHANGES_keys4exchange ( + eg->exchange_url, + true, + &process_pay_with_keys, + eg); + if (NULL == eg->fo) + { + GNUNET_break_op (0); + resume_pay_with_error (pc, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, + eg->exchange_url); + return; + } + pc->batch_deposits.pending_at_eg++; +} + + +/** + * Handle a timeout for the processing of the pay request. + * + * @param cls our `struct PayContext` + */ +static void +handle_pay_timeout (void *cls) +{ + struct PayContext *pc = cls; + + pc->batch_deposits.timeout_task = NULL; + GNUNET_assert (GNUNET_YES == pc->suspended); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming pay with error after timeout\n"); + resume_pay_with_error (pc, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, + NULL); +} + + +/** + * Compute the timeout for a /pay request based on the number of coins + * involved. + * + * @param num_coins number of coins + * @returns timeout for the /pay request + */ +static struct GNUNET_TIME_Relative +get_pay_timeout (unsigned int num_coins) +{ + struct GNUNET_TIME_Relative t; + + /* FIXME-Performance-Optimization: Do some benchmarking to come up with a + * better timeout. We've increased this value so the wallet integration + * test passes again on my (Florian) machine. + */ + t = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, + 15 * (1 + (num_coins / 5))); + + return t; +} + + +/** + * Start batch deposits for all exchanges involved + * in this payment. + * + * @param[in,out] pc payment context we are processing + */ +static void +phase_batch_deposits (struct PayContext *pc) +{ + for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) + { + struct ExchangeGroup *eg = pc->parse_pay.egs[i]; + bool have_coins = false; + + for (size_t j = 0; j<pc->parse_pay.coins_cnt; j++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[j]; + + if (0 != strcmp (eg->exchange_url, + dc->exchange_url)) + continue; + if (dc->found_in_db) + continue; + have_coins = true; + break; + } + if (! have_coins) + continue; /* no coins left to deposit at this exchange */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Getting /keys for %s\n", + eg->exchange_url); + eg->fo = TMH_EXCHANGES_keys4exchange ( + eg->exchange_url, + false, + &process_pay_with_keys, + eg); + if (NULL == eg->fo) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, + eg->exchange_url)); + return; + } + pc->batch_deposits.pending_at_eg++; + } + if (0 == pc->batch_deposits.pending_at_eg) + { + pc->phase = PP_COMPUTE_MONEY_POTS; + pay_resume (pc); + return; + } + /* Suspend while we interact with the exchange */ + MHD_suspend_connection (pc->connection); + pc->suspended = GNUNET_YES; + GNUNET_assert (NULL == pc->batch_deposits.timeout_task); + pc->batch_deposits.timeout_task + = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->parse_pay.coins_cnt), + &handle_pay_timeout, + pc); +} + + +/** + * Build JSON array of blindly signed token envelopes, + * to be used in the response to the wallet. + * + * @param[in,out] pc payment context to use + */ +static json_t * +build_token_sigs (struct PayContext *pc) +{ + json_t *token_sigs; + + if (0 == pc->output_tokens_len) + return NULL; + token_sigs = json_array (); + GNUNET_assert (NULL != token_sigs); + for (unsigned int i = 0; i < pc->output_tokens_len; i++) + { + GNUNET_assert (0 == + json_array_append_new ( + token_sigs, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_blinded_sig ( + "blind_sig", + pc->output_tokens[i].sig.signature) + ))); + } + return token_sigs; +} + + +/** + * Generate response (payment successful) + * + * @param[in,out] pc payment context where the payment was successful + */ +static void +phase_success_response (struct PayContext *pc) +{ + struct TALER_MerchantSignatureP sig; + char *pos_confirmation; + + /* Sign on our end (as the payment did go through, even if it may + have been refunded already) */ + TALER_merchant_pay_sign (&pc->check_contract.h_contract_terms, + &pc->hc->instance->merchant_priv, + &sig); + /* Build the response */ + pos_confirmation = (NULL == pc->check_contract.pos_key) + ? NULL + : TALER_build_pos_confirmation (pc->check_contract.pos_key, + pc->check_contract.pos_alg, + &pc->validate_tokens.brutto, + pc->check_contract.contract_terms->timestamp + ); + pay_end (pc, + TALER_MHD_REPLY_JSON_PACK ( + pc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("pos_confirmation", + pos_confirmation)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_steal ("token_sigs", + build_token_sigs (pc))), + GNUNET_JSON_pack_data_auto ("sig", + &sig))); + GNUNET_free (pos_confirmation); +} + + +/** + * Use database to notify other clients about the + * payment being completed. + * + * @param[in,out] pc context to trigger notification for + */ +static void +phase_payment_notification (struct PayContext *pc) +{ + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), + .merchant_pub = pc->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about payment of order %s\n", + pc->order_id); + GNUNET_CRYPTO_hash (pc->order_id, + strlen (pc->order_id), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = pc->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about status change of order %s\n", + pc->order_id); + GNUNET_CRYPTO_hash (pc->order_id, + strlen (pc->order_id), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } + if ( (NULL != pc->parse_pay.session_id) && + (NULL != pc->check_contract.contract_terms->fulfillment_url) ) + { + struct TMH_SessionEventP session_eh = { + .header.size = htons (sizeof (session_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), + .merchant_pub = pc->hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about session change to %s for %s\n", + pc->parse_pay.session_id, + pc->check_contract.contract_terms->fulfillment_url); + GNUNET_CRYPTO_hash (pc->parse_pay.session_id, + strlen (pc->parse_pay.session_id), + &session_eh.h_session_id); + GNUNET_CRYPTO_hash (pc->check_contract.contract_terms->fulfillment_url, + strlen (pc->check_contract.contract_terms-> + fulfillment_url), + &session_eh.h_fulfillment_url); + TMH_db->event_notify (TMH_db->cls, + &session_eh.header, + NULL, + 0); + } + pc->phase = PP_SUCCESS_RESPONSE; +} + + +/** + * Phase to write all outputs to our database so we do + * not re-request them in case the client re-plays the + * request. + * + * @param[in,out] pc payment context + */ +static void +phase_final_output_token_processing (struct PayContext *pc) +{ + if (0 == pc->output_tokens_len) + { + pc->phase++; + return; + } + for (unsigned int retry = 0; retry < MAX_RETRIES; retry++) + { + enum GNUNET_DB_QueryStatus qs; + + TMH_db->preflight (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "insert_order_blinded_sigs")) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "start insert_order_blinded_sigs_failed"); + pc->phase++; + return; + } +#ifdef HAVE_DONAU_DONAU_SERVICE_H + if (pc->parse_wallet_data.num_bkps > 0) + { + qs = TMH_db->update_donau_instance_receipts_amount ( + TMH_db->cls, + &pc->parse_wallet_data.donau_instance_serial, + &pc->parse_wallet_data.charity_receipts_to_date); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_db->rollback (TMH_db->cls); + GNUNET_break (0); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* weird for an update */ + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } +#endif + for (unsigned int i = 0; + i < pc->output_tokens_len; + i++) + { + qs = TMH_db->insert_order_blinded_sigs ( + TMH_db->cls, + pc->order_id, + i, + &pc->output_tokens[i].h_issue.hash, + pc->output_tokens[i].sig.signature); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_db->rollback (TMH_db->cls); + pc->phase++; + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + goto OUTER; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* weird for an update */ + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } /* for i */ + qs = TMH_db->commit (TMH_db->cls); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_db->rollback (TMH_db->cls); + pc->phase++; + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + pc->phase++; + return; /* success */ + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + pc->phase++; + return; /* success */ + } + GNUNET_break (0); + pc->phase++; + return; /* strange */ +OUTER: + } /* for retry */ + TMH_db->rollback (TMH_db->cls); + pc->phase++; + /* We continue anyway, as there is not much we can + do here: the Donau *did* issue us the receipts; + also, we'll eventually ask the Donau for the + balance and get the correct one. Plus, we were + paid by the client, so it's technically all still + OK. If the request fails anyway, the wallet will + most likely replay the request and then hopefully + we will succeed the next time */ +} + + +#ifdef HAVE_DONAU_DONAU_SERVICE_H + +/** + * Add donation receipt outputs to the output_tokens. + * + * Note that under the current (odd, bad) libdonau + * API *we* are responsible for freeing blinded_sigs, + * so we truly own that array! + * + * @param[in,out] pc payment context + * @param num_blinded_sigs number of signatures received + * @param blinded_sigs blinded signatures from Donau + * @return #GNUNET_OK on success, + * #GNUNET_SYSERR on failure (state machine was + * in that case already advanced) + */ +static enum GNUNET_GenericReturnValue +add_donation_receipt_outputs ( + struct PayContext *pc, + size_t num_blinded_sigs, + struct DONAU_BlindedDonationUnitSignature *blinded_sigs) +{ + int donau_output_index = pc->validate_tokens.donau_output_index; + + GNUNET_assert (pc->parse_wallet_data.num_bkps == + num_blinded_sigs); + + GNUNET_assert (donau_output_index >= 0); + + for (unsigned int i = 0; i<pc->output_tokens_len; i++) + { + struct SignedOutputToken *sot + = &pc->output_tokens[i]; + + /* Only look at actual donau tokens. */ + if (sot->output_index != donau_output_index) + continue; + + sot->sig.signature = GNUNET_CRYPTO_blind_sig_incref (blinded_sigs[i]. + blinded_sig); + sot->h_issue.hash = pc->parse_wallet_data.bkps[i].h_donation_unit_pub.hash; + } + return GNUNET_OK; +} + + +/** + * Callback to handle the result of a batch issue request. + * + * @param cls our `struct PayContext` + * @param resp the response from Donau + */ +static void +merchant_donau_issue_receipt_cb ( + void *cls, + const struct DONAU_BatchIssueResponse *resp) +{ + struct PayContext *pc = cls; + /* Donau replies asynchronously, so we expect the PayContext + * to be suspended. */ + GNUNET_assert (GNUNET_YES == pc->suspended); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Donau responded with status=%u, ec=%u", + resp->hr.http_status, + resp->hr.ec); + switch (resp->hr.http_status) + { + case 0: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Donau batch issue request from merchant-httpd failed (http_status==0)"); + resume_pay_with_error (pc, + TALER_EC_MERCHANT_GENERIC_DONAU_INVALID_RESPONSE, + resp->hr.hint); + return; + case MHD_HTTP_OK: + case MHD_HTTP_CREATED: + if (TALER_EC_NONE != resp->hr.ec) + { + /* Most probably, it is just some small flaw from + * donau so no point in failing, yet we have to display it */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Donau signalled error %u despite HTTP %u", + resp->hr.ec, + resp->hr.http_status); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Donau accepted donation receipts with total_issued=%s", + TALER_amount2s (&resp->details.ok.issued_amount)); + if (GNUNET_OK != + add_donation_receipt_outputs (pc, + resp->details.ok.num_blinded_sigs, + resp->details.ok.blinded_sigs)) + return; /* state machine was already advanced */ + pc->phase = PP_FINAL_OUTPUT_TOKEN_PROCESSING; + pay_resume (pc); + return; + + case MHD_HTTP_BAD_REQUEST: + case MHD_HTTP_FORBIDDEN: + case MHD_HTTP_NOT_FOUND: + case MHD_HTTP_INTERNAL_SERVER_ERROR: + default: /* make sure that everything except 200/201 will end up here*/ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Donau replied with HTTP %u (ec=%u)", + resp->hr.http_status, + resp->hr.ec); + resume_pay_with_error (pc, + TALER_EC_MERCHANT_GENERIC_DONAU_INVALID_RESPONSE, + resp->hr.hint); + return; + } +} + + +/** + * Parse a bkp encoded in JSON. + * + * @param[out] bkp where to return the result + * @param bkp_key_obj json to parse + * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if @a bkp_key_obj + * is malformed. + */ +static enum GNUNET_GenericReturnValue +merchant_parse_json_bkp (struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkp, + const json_t *bkp_key_obj) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_donation_unit_pub", + &bkp->h_donation_unit_pub), + DONAU_JSON_spec_blinded_donation_identifier ("blinded_udi", + &bkp->blinded_udi), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (bkp_key_obj, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Generate a donation signature for the bkp and charity. + * + * @param[in,out] pc payment context containing the charity and bkps + */ +static void +phase_request_donation_receipt (struct PayContext *pc) +{ + if ( (NULL == pc->parse_wallet_data.donau.donau_url) || + (0 == pc->parse_wallet_data.num_bkps) ) + { + pc->phase++; + return; + } + pc->donau_receipt.birh = + DONAU_charity_issue_receipt ( + TMH_curl_ctx, + pc->parse_wallet_data.donau.donau_url, + &pc->parse_wallet_data.charity_priv, + pc->parse_wallet_data.charity_id, + pc->parse_wallet_data.donau.donation_year, + pc->parse_wallet_data.num_bkps, + pc->parse_wallet_data.bkps, + &merchant_donau_issue_receipt_cb, + pc); + if (NULL == pc->donau_receipt.birh) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to create Donau receipt request"); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR, + "Donau request creation error")); + return; + } + MHD_suspend_connection (pc->connection); + pc->suspended = GNUNET_YES; +} + + +#endif + + +/** + * Increment the money pot @a pot_id in @a pc by @a increment. + * + * @param[in,out] pc context to update + * @param pot_id money pot to increment + * @param increment amount to add + */ +static void +increment_pot (struct PayContext *pc, + uint64_t pot_id, + const struct TALER_Amount *increment) +{ + for (unsigned int i = 0; i<pc->compute_money_pots.num_pots; i++) + { + if (pot_id == pc->compute_money_pots.pots[i]) + { + struct TALER_Amount *p; + + p = &pc->compute_money_pots.increments[i]; + GNUNET_assert (0 <= + TALER_amount_add (p, + p, + increment)); + return; + } + } + GNUNET_array_append (pc->compute_money_pots.pots, + pc->compute_money_pots.num_pots, + pot_id); + pc->compute_money_pots.num_pots--; /* do not increment twice... */ + GNUNET_array_append (pc->compute_money_pots.increments, + pc->compute_money_pots.num_pots, + *increment); +} + + +/** + * Compute the total changes to money pots in preparation + * for the #PP_PAY_TRANSACTION phase. + * + * @param[in,out] pc payment context to transact + */ +static void +phase_compute_money_pots (struct PayContext *pc) +{ + const struct TALER_MERCHANT_Contract *contract + = pc->check_contract.contract_terms; + struct TALER_Amount assigned; + + if (0 == pc->parse_pay.coins_cnt) + { + /* Did not pay with any coins, so no currency/amount involved, + hence no money pot update possible. */ + pc->phase++; + return; + } + + if (pc->compute_money_pots.pots_computed) + { + pc->phase++; + return; + } + /* reset, in case this phase is run a 2nd time */ + GNUNET_free (pc->compute_money_pots.pots); + GNUNET_free (pc->compute_money_pots.increments); + pc->compute_money_pots.num_pots = 0; + + TALER_amount_set_zero (pc->parse_pay.dc[0].cdd.amount.currency, + &assigned); + GNUNET_assert (NULL != contract); + for (size_t i = 0; i<contract->products_len; i++) + { + const struct TALER_MERCHANT_ProductSold *product + = &contract->products[i]; + const struct TALER_Amount *price = NULL; + + /* find price in the right currency */ + for (unsigned int j = 0; j<product->prices_length; j++) + { + if (GNUNET_OK == + TALER_amount_cmp_currency (&assigned, + &product->prices[j])) + { + price = &product->prices[j]; + break; + } + } + if (NULL == price) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Product `%s' has no price given in `%s'.\n", + product->product_id, + assigned.currency); + continue; + } + if (0 != product->product_money_pot) + { + GNUNET_assert (0 <= + TALER_amount_add (&assigned, + &assigned, + price)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Contributing to product money pot %llu increment of %s\n", + (unsigned long long) product->product_money_pot, + TALER_amount2s (price)); + increment_pot (pc, + product->product_money_pot, + price); + } + } + + { + /* Compute what is left from the order total and account for that. + Also sanity-check and handle the case where the overall order + is below that of the sum of the products. */ + struct TALER_Amount left; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order brutto is %s\n", + TALER_amount2s (&pc->validate_tokens.brutto)); + if (0 > + TALER_amount_subtract (&left, + &pc->validate_tokens.brutto, + &assigned)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Total order brutto amount below sum from products, skipping per-product money pots\n"); + GNUNET_free (pc->compute_money_pots.pots); + GNUNET_free (pc->compute_money_pots.increments); + pc->compute_money_pots.num_pots = 0; + left = pc->validate_tokens.brutto; + } + + if ( (! TALER_amount_is_zero (&left)) && + (0 != contract->default_money_pot) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Computing money pot %llu increment as %s\n", + (unsigned long long) contract->default_money_pot, + TALER_amount2s (&left)); + increment_pot (pc, + contract->default_money_pot, + &left); + } + } + pc->compute_money_pots.pots_computed = true; + pc->phase++; +} + + +/** + * Function called with information about a coin that was deposited. + * + * @param cls closure + * @param exchange_url exchange where @a coin_pub was deposited + * @param coin_pub public key of the coin + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param refund_fee fee the exchange will charge for refunding this coin + */ +static void +check_coin_paid (void *cls, + const char *exchange_url, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_Amount *refund_fee) +{ + struct PayContext *pc = cls; + + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + if (dc->found_in_db) + continue; /* processed earlier, skip "expensive" memcmp() */ + /* Get matching coin from results*/ + if ( (0 != GNUNET_memcmp (coin_pub, + &dc->cdd.coin_pub)) || + (0 != + strcmp (exchange_url, + dc->exchange_url)) || + (GNUNET_OK != + TALER_amount_cmp_currency (amount_with_fee, + &dc->cdd.amount)) || + (0 != TALER_amount_cmp (amount_with_fee, + &dc->cdd.amount)) ) + continue; /* does not match, skip */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Deposit of coin `%s' already in our DB.\n", + TALER_B2S (coin_pub)); + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&pc->pay_transaction.total_paid, + amount_with_fee)) || + (GNUNET_OK != + TALER_amount_cmp_currency (&pc->pay_transaction.total_fees_paid, + deposit_fee)) ) + { + GNUNET_break_op (0); + pc->pay_transaction.deposit_currency_mismatch = true; + break; + } + GNUNET_assert (0 <= + TALER_amount_add (&pc->pay_transaction.total_paid, + &pc->pay_transaction.total_paid, + amount_with_fee)); + GNUNET_assert (0 <= + TALER_amount_add (&pc->pay_transaction.total_fees_paid, + &pc->pay_transaction.total_fees_paid, + deposit_fee)); + dc->deposit_fee = *deposit_fee; + dc->refund_fee = *refund_fee; + dc->cdd.amount = *amount_with_fee; + dc->found_in_db = true; + pc->pay_transaction.pending--; + } +} + + +/** + * Function called with information about a refund. Check if this coin was + * claimed by the wallet for the transaction, and if so add the refunded + * amount to the pc's "total_refunded" amount. + * + * @param cls closure with a `struct PayContext` + * @param coin_pub public coin from which the refund comes from + * @param refund_amount refund amount which is being taken from @a coin_pub + */ +static void +check_coin_refunded (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_Amount *refund_amount) +{ + struct PayContext *pc = cls; + + /* We look at refunds here that apply to the coins + that the customer is currently trying to pay us with. + + Such refunds are not "normal" refunds, but abort-pay refunds, which are + given in the case that the wallet aborts the payment. + In the case the wallet then decides to complete the payment *after* doing + an abort-pay refund (an unusual but possible case), we need + to make sure that existing refunds are accounted for. */ + + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + /* Get matching coins from results. */ + if (0 != GNUNET_memcmp (coin_pub, + &dc->cdd.coin_pub)) + continue; + if (GNUNET_OK != + TALER_amount_cmp_currency (&pc->pay_transaction.total_refunded, + refund_amount)) + { + GNUNET_break (0); + pc->pay_transaction.refund_currency_mismatch = true; + break; + } + GNUNET_assert (0 <= + TALER_amount_add (&pc->pay_transaction.total_refunded, + &pc->pay_transaction.total_refunded, + refund_amount)); + break; + } +} + + +/** + * Check whether the amount paid is sufficient to cover the price. + * + * @param pc payment context to check + * @return true if the payment is sufficient, false if it is + * insufficient + */ +static bool +check_payment_sufficient (struct PayContext *pc) +{ + struct TALER_Amount acc_fee; + struct TALER_Amount acc_amount; + struct TALER_Amount final_amount; + struct TALER_Amount total_wire_fee; + struct TALER_Amount total_needed; + + if (0 == pc->parse_pay.coins_cnt) + return TALER_amount_is_zero (&pc->validate_tokens.brutto); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &total_wire_fee)); + for (unsigned int i = 0; i < pc->parse_pay.num_exchanges; i++) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&total_wire_fee, + &pc->parse_pay.egs[i]->wire_fee)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + total_wire_fee.currency)); + return false; + } + if (0 > + TALER_amount_add (&total_wire_fee, + &total_wire_fee, + &pc->parse_pay.egs[i]->wire_fee)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, + "could not add exchange wire fee to total")); + return false; + } + } + + /** + * This loops calculates what are the deposit fee / total + * amount with fee / and wire fee, for all the coins. + */ + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &acc_fee)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &acc_amount)); + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + GNUNET_assert (dc->found_in_db); + if ( (GNUNET_OK != + TALER_amount_cmp_currency (&acc_fee, + &dc->deposit_fee)) || + (GNUNET_OK != + TALER_amount_cmp_currency (&acc_amount, + &dc->cdd.amount)) ) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + dc->deposit_fee.currency)); + return false; + } + if ( (0 > + TALER_amount_add (&acc_fee, + &dc->deposit_fee, + &acc_fee)) || + (0 > + TALER_amount_add (&acc_amount, + &dc->cdd.amount, + &acc_amount)) ) + { + GNUNET_break (0); + /* Overflow in these amounts? Very strange. */ + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); + return false; + } + if (1 == + TALER_amount_cmp (&dc->deposit_fee, + &dc->cdd.amount)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, + "Deposit fees exceed coin's contribution")); + return false; + } + } /* end deposit loop */ + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Amount received from wallet: %s\n", + TALER_amount2s (&acc_amount)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Deposit fee for all coins: %s\n", + TALER_amount2s (&acc_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Total wire fee: %s\n", + TALER_amount2s (&total_wire_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Deposit fee limit for merchant: %s\n", + TALER_amount2s (&pc->validate_tokens.max_fee)); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Total refunded amount: %s\n", + TALER_amount2s (&pc->pay_transaction.total_refunded)); + + /* Now compare exchange wire fee compared to what we are willing to pay */ + if (GNUNET_YES != + TALER_amount_cmp_currency (&total_wire_fee, + &acc_fee)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + total_wire_fee.currency)); + return false; + } + + /* add wire fee to the total fees */ + if (0 > + TALER_amount_add (&acc_fee, + &acc_fee, + &total_wire_fee)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); + return false; + } + if (-1 == TALER_amount_cmp (&pc->validate_tokens.max_fee, + &acc_fee)) + { + /** + * Sum of fees of *all* the different exchanges of all the coins are + * higher than the fixed limit that the merchant is willing to pay. The + * difference must be paid by the customer. + */ + struct TALER_Amount excess_fee; + + /* compute fee amount to be covered by customer */ + GNUNET_assert (TALER_AAR_RESULT_POSITIVE == + TALER_amount_subtract (&excess_fee, + &acc_fee, + &pc->validate_tokens.max_fee)); + /* add that to the total */ + if (0 > + TALER_amount_add (&total_needed, + &excess_fee, + &pc->validate_tokens.brutto)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); + return false; + } + } + else + { + /* Fees are fully covered by the merchant, all we require + is that the total payment is not below the contract's amount */ + total_needed = pc->validate_tokens.brutto; + } + + /* Do not count refunds towards the payment */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Subtracting total refunds from paid amount: %s\n", + TALER_amount2s (&pc->pay_transaction.total_refunded)); + if (0 > + TALER_amount_subtract (&final_amount, + &acc_amount, + &pc->pay_transaction.total_refunded)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS, + "refunded amount exceeds total payments")); + return false; + } + + if (-1 == TALER_amount_cmp (&final_amount, + &total_needed)) + { + /* acc_amount < total_needed */ + if (-1 < TALER_amount_cmp (&acc_amount, + &total_needed)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_PAYMENT_REQUIRED, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED, + "contract not paid up due to refunds")); + return false; + } + if (-1 < TALER_amount_cmp (&acc_amount, + &pc->validate_tokens.brutto)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES, + "contract not paid up due to fees (client may have calculated them badly)")); + return false; + } + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, + "payment insufficient")); + return false; + } + return true; +} + + +/** + * Execute the DB transaction. If required (from + * soft/serialization errors), the transaction can be + * restarted here. + * + * @param[in,out] pc payment context to transact + */ +static void +phase_execute_pay_transaction (struct PayContext *pc) +{ + struct TMH_HandlerContext *hc = pc->hc; + const char *instance_id = hc->instance->settings.id; + + if (pc->batch_deposits.got_451) + { + pc->phase = PP_FAIL_LEGAL_REASONS; + return; + } + /* Avoid re-trying transactions on soft errors forever! */ + if (pc->pay_transaction.retry_counter++ > MAX_RETRIES) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL)); + return; + } + + /* Initialize some amount accumulators + (used in check_coin_paid(), check_coin_refunded() + and check_payment_sufficient()). */ + GNUNET_break (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &pc->pay_transaction.total_paid)); + GNUNET_break (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &pc->pay_transaction.total_fees_paid)); + GNUNET_break (GNUNET_OK == + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, + &pc->pay_transaction.total_refunded)); + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + pc->parse_pay.dc[i].found_in_db = false; + pc->pay_transaction.pending = pc->parse_pay.coins_cnt; + + /* First, try to see if we have all we need already done */ + TMH_db->preflight (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "run pay")) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL)); + return; + } + + for (size_t i = 0; i<pc->parse_pay.tokens_cnt; i++) + { + struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; + enum GNUNET_DB_QueryStatus qs; + + /* Insert used token into database, the unique constraint will + case an error if this token was used before. */ + qs = TMH_db->insert_spent_token (TMH_db->cls, + &pc->check_contract.h_contract_terms, + &tuc->h_issue, + &tuc->pub, + &tuc->sig, + &tuc->unblinded_sig); + + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + return; /* do it again */ + case GNUNET_DB_STATUS_HARD_ERROR: + /* Always report on hard error as well to enable diagnostics */ + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert used token")); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* UNIQUE constraint violation, meaning this token was already used. */ + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID, + NULL)); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Good, proceed! */ + break; + } + } /* for all tokens */ + + { + enum GNUNET_DB_QueryStatus qs; + + /* Check if some of these coins already succeeded for _this_ contract. */ + qs = TMH_db->lookup_deposits (TMH_db->cls, + instance_id, + &pc->check_contract.h_contract_terms, + &check_coin_paid, + pc); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup deposits")); + return; + } + if (pc->pay_transaction.deposit_currency_mismatch) + { + TMH_db->rollback (TMH_db->cls); + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + pc->validate_tokens.brutto.currency)); + return; + } + } + + { + enum GNUNET_DB_QueryStatus qs; + + /* Check if we refunded some of the coins */ + qs = TMH_db->lookup_refunds (TMH_db->cls, + instance_id, + &pc->check_contract.h_contract_terms, + &check_coin_refunded, + pc); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup refunds")); + return; + } + if (pc->pay_transaction.refund_currency_mismatch) + { + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "refund currency in database does not match order currency")); + return; + } + } + + /* Check if there are coins that still need to be processed */ + if (0 != pc->pay_transaction.pending) + { + /* we made no DB changes, so we can just rollback */ + TMH_db->rollback (TMH_db->cls); + /* Ok, we need to first go to the network to process more coins. + We that interaction in *tiny* transactions (hence the rollback + above). */ + pc->phase = PP_BATCH_DEPOSITS; + return; + } + + /* 0 == pc->pay_transaction.pending: all coins processed, let's see if that was enough */ + if (! check_payment_sufficient (pc)) + { + /* check_payment_sufficient() will have queued an error already. + We need to still abort the transaction. */ + TMH_db->rollback (TMH_db->cls); + return; + } + /* Payment succeeded, save in database */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' (%s) was fully paid\n", + pc->order_id, + GNUNET_h2s (&pc->check_contract.h_contract_terms.hash)); + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->mark_contract_paid (TMH_db->cls, + instance_id, + &pc->check_contract.h_contract_terms, + pc->parse_pay.session_id, + pc->parse_wallet_data.choice_index); + if (qs < 0) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "mark contract paid")); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Marked contract paid returned %d\n", + (int) qs); + + if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) && + (0 < pc->compute_money_pots.num_pots) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Incrementing %u money pots by %s\n", + pc->compute_money_pots.num_pots, + TALER_amount2s (&pc->compute_money_pots.increments[0])); + qs = TMH_db->increment_money_pots (TMH_db->cls, + instance_id, + pc->compute_money_pots.num_pots, + pc->compute_money_pots.pots, + pc->compute_money_pots.increments); + switch (qs) + { + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + return; /* do it again */ + case GNUNET_DB_STATUS_HARD_ERROR: + /* Always report on hard error as well to enable diagnostics */ + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "increment_money_pots")); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* strange */ + GNUNET_break (0); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* Good, proceed! */ + break; + } + + } + + + } + + + { + const struct TALER_MERCHANT_ContractChoice *choice = + &pc->check_contract.contract_terms->details.v1 + .choices[pc->parse_wallet_data.choice_index]; + + for (size_t i = 0; i<pc->output_tokens_len; i++) + { + unsigned int output_index; + enum TALER_MERCHANT_ContractOutputType type; + + output_index = pc->output_tokens[i].output_index; + GNUNET_assert (output_index < choice->outputs_len); + type = choice->outputs[output_index].type; + + switch (type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + /* Well, good luck getting here */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "invalid output type")); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + /* We skip output tokens of donation receipts here, as they are handled in the + * phase_final_output_token_processing() callback from donau */ + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + struct SignedOutputToken *output = + &pc->output_tokens[i]; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->insert_issued_token (TMH_db->cls, + &pc->check_contract.h_contract_terms, + &output->h_issue, + &output->sig); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_db->rollback (TMH_db->cls); + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert output token")); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* Serialization failure, retry */ + TMH_db->rollback (TMH_db->cls); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* UNIQUE constraint violation, meaning this token was already used. */ + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "duplicate output token")); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + break; + } + } + } + + TMH_notify_order_change (hc->instance, + TMH_OSF_CLAIMED | TMH_OSF_PAID, + pc->check_contract.contract_terms->timestamp, + pc->check_contract.order_serial); + { + enum GNUNET_DB_QueryStatus qs; + json_t *jhook; + + jhook = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("contract_terms", + pc->check_contract.contract_terms_json), + GNUNET_JSON_pack_string ("order_id", + pc->order_id) + ); + GNUNET_assert (NULL != jhook); + qs = TMH_trigger_webhook (pc->hc->instance->settings.id, + "pay", + jhook); + json_decref (jhook); + if (qs < 0) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to trigger webhooks")); + return; + } + } + { + enum GNUNET_DB_QueryStatus qs; + + /* Now commit! */ + qs = TMH_db->commit (TMH_db->cls); + if (0 > qs) + { + /* commit failed */ + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return; /* do it again */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)); + return; + } + } + pc->phase++; +} + + +/** + * Ensures that the expected number of tokens for a @e key + * are provided as inputs and have valid signatures. + * + * @param[in,out] pc payment context we are processing + * @param family family the tokens should be from + * @param index number of the input we are handling + * @param expected_num number of tokens expected + * @return #GNUNET_YES on success + */ +static enum GNUNET_GenericReturnValue +find_valid_input_tokens ( + struct PayContext *pc, + const struct TALER_MERCHANT_ContractTokenFamily *family, + unsigned int index, + unsigned int expected_num) +{ + unsigned int num_validated = 0; + struct GNUNET_TIME_Timestamp now + = GNUNET_TIME_timestamp_get (); + const struct TALER_MERCHANT_ContractTokenFamilyKey *kig = NULL; + + for (unsigned int j = 0; j < expected_num; j++) + { + struct TokenUseConfirmation *tuc + = &pc->parse_pay.tokens[index + j]; + const struct TALER_MERCHANT_ContractTokenFamilyKey *key = NULL; + + for (unsigned int i = 0; i<family->keys_len; i++) + { + const struct TALER_MERCHANT_ContractTokenFamilyKey *ki + = &family->keys[i]; + + if (0 == + GNUNET_memcmp (&ki->pub.public_key->pub_key_hash, + &tuc->h_issue.hash)) + { + if (GNUNET_TIME_timestamp_cmp (ki->valid_after, + >, + now) || + GNUNET_TIME_timestamp_cmp (ki->valid_before, + <=, + now)) + { + /* We have a match, but not in the current validity period */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Public key %s currently not valid\n", + GNUNET_h2s (&ki->pub.public_key->pub_key_hash)); + kig = ki; + continue; + } + key = ki; + break; + } + } + if (NULL == key) + { + if (NULL != kig) + { + char start_str[128]; + char end_str[128]; + char emsg[350]; + + GNUNET_snprintf (start_str, + sizeof (start_str), + "%s", + GNUNET_STRINGS_timestamp_to_string (kig->valid_after)); + GNUNET_snprintf (end_str, + sizeof (end_str), + "%s", + GNUNET_STRINGS_timestamp_to_string (kig->valid_before)) + ; + /* FIXME: use more specific EC */ + GNUNET_snprintf (emsg, + sizeof (emsg), + "Token is only valid from %s to %s", + start_str, + end_str); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_GONE, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, + emsg)); + return GNUNET_NO; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token supplied for public key %s that is not acceptable\n", + GNUNET_h2s (&tuc->h_issue.hash)); + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN, + NULL)); + return GNUNET_NO; + } + if (GNUNET_OK != + TALER_token_issue_verify (&tuc->pub, + &key->pub, + &tuc->unblinded_sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token for public key with valid_after " + "`%s' has invalid issue signature\n", + GNUNET_TIME_timestamp2s (key->valid_after)); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID, + NULL)); + return GNUNET_NO; + } + + if (GNUNET_OK != + TALER_wallet_token_use_verify (&pc->check_contract.h_contract_terms, + &pc->parse_wallet_data.h_wallet_data, + &tuc->pub, + &tuc->sig)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token for public key with valid_before " + "`%s' has invalid use signature\n", + GNUNET_TIME_timestamp2s (key->valid_before)); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID, + NULL)); + return GNUNET_NO; + } + + num_validated++; + } + + if (num_validated != expected_num) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Expected %d tokens for family %s, but found %d\n", + expected_num, + family->slug, + num_validated); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH, + NULL)); + return GNUNET_NO; + } + return GNUNET_YES; +} + + +/** + * Check if an output token of the given @a tfk is mandatory, or if + * wallets are allowed to simply not support it and still proceed. + * + * @param tfk token family kind to check + * @return true if such outputs are mandatory and wallets must supply + * the corresponding blinded input + */ +/* FIXME: this function belongs into a lower-level lib! */ +static bool +test_tfk_mandatory (enum TALER_MERCHANTDB_TokenFamilyKind tfk) +{ + switch (tfk) + { + case TALER_MERCHANTDB_TFK_Discount: + return false; + case TALER_MERCHANTDB_TFK_Subscription: + return true; + } + GNUNET_break (0); + return false; +} + + +/** + * Sign the tokens provided by the wallet for a particular @a key. + * + * @param[in,out] pc reference for payment we are processing + * @param key token family data + * @param priv private key to use to sign with + * @param mandatory true if the token must exist, if false + * and the client did not provide an envelope, that's OK and + * we just also skimp on the signature + * @param index offset in the token envelope array (from other families) + * @param expected_num number of tokens of this type that we should create + * @return #GNUNET_NO on failure + * #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +sign_token_envelopes ( + struct PayContext *pc, + const struct TALER_MERCHANT_ContractTokenFamilyKey *key, + const struct TALER_TokenIssuePrivateKey *priv, + bool mandatory, + unsigned int index, + unsigned int expected_num) +{ + unsigned int num_signed = 0; + + for (unsigned int j = 0; j<expected_num; j++) + { + unsigned int pos = index + j; + const struct TokenEnvelope *env + = &pc->parse_wallet_data.token_envelopes[pos]; + struct SignedOutputToken *output + = &pc->output_tokens[pos]; + + if ( (pos >= pc->parse_wallet_data.token_envelopes_cnt) || + (pos >= pc->output_tokens_len) ) + { + GNUNET_assert (0); /* this should not happen */ + return GNUNET_NO; + } + if (NULL == env->blinded_token.blinded_pub) + { + if (! mandatory) + continue; + + /* mandatory token families require a token envelope. */ + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Token envelope for mandatory token family missing")); + return GNUNET_NO; + } + TALER_token_issue_sign (priv, + &env->blinded_token, + &output->sig); + output->h_issue.hash + = key->pub.public_key->pub_key_hash; + num_signed++; + } + + if (mandatory && + (num_signed != expected_num) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Expected %d token envelopes for public key with valid_after " + "'%s', but found %d\n", + expected_num, + GNUNET_TIME_timestamp2s (key->valid_after), + num_signed); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH, + NULL)); + return GNUNET_NO; + } + + return GNUNET_OK; +} + + +/** + * Find the family entry for the family of the given @a slug + * in @a pc. + * + * @param[in] pc payment context to search + * @param slug slug to search for + * @return NULL if @a slug was not found + */ +static const struct TALER_MERCHANT_ContractTokenFamily * +find_family (const struct PayContext *pc, + const char *slug) +{ + for (unsigned int i = 0; + i < pc->check_contract.contract_terms->details.v1.token_authorities_len; + i++) + { + const struct TALER_MERCHANT_ContractTokenFamily *tfi + = &pc->check_contract.contract_terms->details.v1.token_authorities[i]; + + if (0 == strcmp (tfi->slug, + slug)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Token family %s found with %u keys\n", + slug, + tfi->keys_len); + return tfi; + } + } + return NULL; +} + + +/** + * Handle contract output of type TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN. + * Looks up the token family, loads the matching private key, + * and signs the corresponding token envelopes from the wallet. + * + * @param pc context for the pay request + * @param output contract output we need to process + * @param output_index index of this output in the contract's outputs array + * @return #GNUNET_OK on success, #GNUNET_NO if an error was encountered + */ +static enum GNUNET_GenericReturnValue +handle_output_token (struct PayContext *pc, + const struct TALER_MERCHANT_ContractOutput *output, + unsigned int output_index) +{ + const struct TALER_MERCHANT_ContractTokenFamily *family; + struct TALER_MERCHANT_ContractTokenFamilyKey *key; + struct TALER_MERCHANTDB_TokenFamilyKeyDetails details; + enum GNUNET_DB_QueryStatus qs; + bool mandatory; + + /* Locate token family in the contract. + This should ever fail as this invariant should + have been checked when the contract was created. */ + family = find_family (pc, + output->details.token.token_family_slug); + if (NULL == family) + { + /* This "should never happen", so treat it as an internal error */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "token family not found in order")); + return GNUNET_SYSERR; + } + + /* Check the key_index field from the output. */ + if (output->details.token.key_index >= family->keys_len) + { + /* Also "should never happen", contract was presumably validated on insert */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "key index invalid for token family")); + return GNUNET_SYSERR; + } + + /* Pick the correct key inside that family. */ + key = &family->keys[output->details.token.key_index]; + + /* Fetch the private key from the DB for the merchant instance and + * this particular family/time interval. */ + qs = TMH_db->lookup_token_family_key ( + TMH_db->cls, + pc->hc->instance->settings.id, + family->slug, + pc->check_contract.contract_terms->timestamp, + pc->check_contract.contract_terms->pay_deadline, + &details); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Database error looking up token-family key for %s\n", + family->slug); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL)); + return GNUNET_NO; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log ( + GNUNET_ERROR_TYPE_ERROR, + "Token-family key for %s not found at [%llu,%llu]\n", + family->slug, + (unsigned long long) + pc->check_contract.contract_terms->timestamp.abs_time.abs_value_us, + (unsigned long long) + pc->check_contract.contract_terms->pay_deadline.abs_time.abs_value_us + ); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN, + family->slug)); + return GNUNET_NO; + + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + GNUNET_assert (NULL != details.priv.private_key); + GNUNET_free (details.token_family.slug); + GNUNET_free (details.token_family.name); + GNUNET_free (details.token_family.description); + json_decref (details.token_family.description_i18n); + GNUNET_CRYPTO_blind_sign_pub_decref (details.pub.public_key); + GNUNET_free (details.token_family.cipher_spec); + + /* Depending on the token family, decide if the token envelope + * is mandatory or optional. (Simplified logic here: adapt as needed.) */ + mandatory = test_tfk_mandatory (details.token_family.kind); + /* Actually sign the number of token envelopes specified in 'count'. + * 'output_index' is the offset into the parse_wallet_data arrays. */ + if (GNUNET_OK != + sign_token_envelopes (pc, + key, + &details.priv, + mandatory, + output_index, + output->details.token.count)) + { + /* sign_token_envelopes() already queued up an error via pay_end() */ + GNUNET_break_op (0); + return GNUNET_NO; + } + GNUNET_CRYPTO_blind_sign_priv_decref (details.priv.private_key); + return GNUNET_OK; +} + + +#ifdef HAVE_DONAU_DONAU_SERVICE_H +/** + * Handle checks for contract output of type TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT. + * For now, this does nothing and simply returns #GNUNET_OK. + * + * @param pc context for the pay request + * @param output the contract output describing the donation receipt requirement + * @return #GNUNET_OK on success, + * #GNUNET_NO if an error was already queued + */ +static enum GNUNET_GenericReturnValue +handle_output_donation_receipt ( + struct PayContext *pc, + const struct TALER_MERCHANT_ContractOutput *output) +{ + enum GNUNET_GenericReturnValue ret; + + ret = DONAU_get_donation_amount_from_bkps ( + pc->parse_wallet_data.donau_keys, + pc->parse_wallet_data.bkps, + pc->parse_wallet_data.num_bkps, + pc->parse_wallet_data.donau.donation_year, + &pc->parse_wallet_data.donation_amount); + switch (ret) + { + case GNUNET_SYSERR: + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + NULL)); + return GNUNET_NO; + case GNUNET_NO: + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inconsistent bkps / donau keys")); + return GNUNET_NO; + case GNUNET_OK: + break; + } + + if (GNUNET_OK != + TALER_amount_cmp_currency (&pc->parse_wallet_data.donation_amount, + &output->details.donation_receipt.amount)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + output->details.donation_receipt.amount.currency)); + return GNUNET_NO; + } + + if (0 != + TALER_amount_cmp (&pc->parse_wallet_data.donation_amount, + &output->details.donation_receipt.amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Wallet amount: %s\n", + TALER_amount2s (&pc->parse_wallet_data.donation_amount)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Donation receipt amount: %s\n", + TALER_amount2s (&output->details.donation_receipt.amount)); + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DONATION_AMOUNT_MISMATCH, + "donation amount mismatch")); + return GNUNET_NO; + } + { + struct TALER_Amount receipts_to_date; + + if (0 > + TALER_amount_add (&receipts_to_date, + &pc->parse_wallet_data.charity_receipts_to_date, + &pc->parse_wallet_data.donation_amount)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "adding donation amount")); + return GNUNET_NO; + } + + if (1 == + TALER_amount_cmp (&receipts_to_date, + &pc->parse_wallet_data.charity_max_per_year)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DONATION_AMOUNT_MISMATCH, + "donation limit exceeded")); + return GNUNET_NO; + } + pc->parse_wallet_data.charity_receipts_to_date = receipts_to_date; + } + return GNUNET_OK; +} + + +#endif /* HAVE_DONAU_DONAU_SERVICE_H */ + + +/** + * Count tokens produced by an output. + * + * @param pc pay context + * @param output output to consider + * @returns number of output tokens + */ +static unsigned int +count_output_tokens (const struct PayContext *pc, + const struct TALER_MERCHANT_ContractOutput *output) +{ + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + return output->details.token.count; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: +#ifdef HAVE_DONAU_DONAU_SERVICE_H + return pc->parse_wallet_data.num_bkps; +#else + return 0; +#endif + } + /* Not reached. */ + GNUNET_assert (0); +} + + +/** + * Validate tokens and token envelopes. First, we check if all tokens listed + * in the 'inputs' array of the selected choice are present in the 'tokens' + * array of the request. Then, we validate the signatures of each provided + * token. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_validate_tokens (struct PayContext *pc) +{ + /* We haven't seen a donau output yet. */ + pc->validate_tokens.donau_output_index = -1; + + switch (pc->check_contract.contract_terms->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + /* No tokens to validate */ + pc->phase = PP_COMPUTE_MONEY_POTS; + pc->validate_tokens.max_fee + = pc->check_contract.contract_terms->details.v0.max_fee; + pc->validate_tokens.brutto + = pc->check_contract.contract_terms->details.v0.brutto; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + { + const struct TALER_MERCHANT_ContractChoice *selected + = &pc->check_contract.contract_terms->details.v1.choices[ + pc->parse_wallet_data.choice_index]; + unsigned int output_off; + unsigned int cnt; + + pc->validate_tokens.max_fee = selected->max_fee; + pc->validate_tokens.brutto = selected->amount; + + for (unsigned int i = 0; i<selected->inputs_len; i++) + { + const struct TALER_MERCHANT_ContractInput *input + = &selected->inputs[i]; + const struct TALER_MERCHANT_ContractTokenFamily *family; + + switch (input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "input token type not valid")); + return; +#if FUTURE + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_COIN: + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_MERCHANT_GENERIC_FEATURE_NOT_AVAILABLE, + "token type not yet supported")); + return; +#endif + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + family = find_family (pc, + input->details.token.token_family_slug); + if (NULL == family) + { + /* this should never happen, since the choices and + token families are validated on insert. */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "token family not found in order")); + return; + } + if (GNUNET_NO == + find_valid_input_tokens (pc, + family, + i, + input->details.token.count)) + { + /* Error is already scheduled from find_valid_input_token. */ + return; + } + } + } + + /* calculate pc->output_tokens_len */ + output_off = 0; + for (unsigned int i = 0; i<selected->outputs_len; i++) + { + const struct TALER_MERCHANT_ContractOutput *output + = &selected->outputs[i]; + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + cnt = output->details.token.count; + if (output_off + cnt < output_off) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "output token counter overflow")); + return; + } + output_off += cnt; + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + /* check that this output type appears at most once */ + if (pc->validate_tokens.donau_output_index >= 0) + { + /* This should have been prevented when the + contract was initially created */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "two donau output sets in same contract")); + return; + } + pc->validate_tokens.donau_output_index = i; +#ifdef HAVE_DONAU_DONAU_SERVICE_H + if (output_off + pc->parse_wallet_data.num_bkps < output_off) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "output token counter overflow")); + return; + } + output_off += pc->parse_wallet_data.num_bkps; +#endif + break; + } + } + + + pc->output_tokens_len = output_off; + pc->output_tokens + = GNUNET_new_array (pc->output_tokens_len, + struct SignedOutputToken); + + /* calculate pc->output_tokens[].output_index */ + output_off = 0; + for (unsigned int i = 0; i<selected->outputs_len; i++) + { + const struct TALER_MERCHANT_ContractOutput *output + = &selected->outputs[i]; + cnt = count_output_tokens (pc, + output); + for (unsigned int j = 0; j<cnt; j++) + pc->output_tokens[output_off + j].output_index = i; + output_off += cnt; + } + + /* compute non-donau outputs */ + output_off = 0; + for (unsigned int i = 0; i<selected->outputs_len; i++) + { + const struct TALER_MERCHANT_ContractOutput *output + = &selected->outputs[i]; + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + cnt = output->details.token.count; + GNUNET_assert (output_off + cnt + <= pc->output_tokens_len); + if (GNUNET_OK != + handle_output_token (pc, + output, + output_off)) + { + /* Error is already scheduled from handle_output_token. */ + return; + } + output_off += cnt; + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: +#ifndef HAVE_DONAU_DONAU_SERVICE_H + /* We checked at parse time, and + wallet didn't want donau, so OK! */ + return; +#else + if ( (0 != pc->parse_wallet_data.num_bkps) && + (GNUNET_OK != + handle_output_donation_receipt (pc, + output)) ) + { + /* Error is already scheduled from handle_output_donation_receipt. */ + return; + } + output_off += pc->parse_wallet_data.num_bkps; + continue; +#endif + } /* switch on output token */ + } /* for all output token types */ + } /* case contract v1 */ + break; + } /* switch on contract type */ + + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + const struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + if (GNUNET_OK != + TALER_amount_cmp_currency (&dc->cdd.amount, + &pc->validate_tokens.brutto)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + pc->validate_tokens.brutto.currency)); + return; + } + } + + pc->phase = PP_COMPUTE_MONEY_POTS; +} + + +/** + * Function called with information about a coin that was deposited. + * Checks if this coin is in our list of deposits as well. + * + * @param cls closure with our `struct PayContext *` + * @param deposit_serial which deposit operation is this about + * @param exchange_url URL of the exchange that issued the coin + * @param h_wire hash of merchant's wire details + * @param deposit_timestamp when was the deposit made + * @param amount_with_fee amount the exchange will deposit for this coin + * @param deposit_fee fee the exchange will charge for this coin + * @param coin_pub public key of the coin + */ +static void +deposit_paid_check ( + void *cls, + uint64_t deposit_serial, + const char *exchange_url, + const struct TALER_MerchantWireHashP *h_wire, + struct GNUNET_TIME_Timestamp deposit_timestamp, + const struct TALER_Amount *amount_with_fee, + const struct TALER_Amount *deposit_fee, + const struct TALER_CoinSpendPublicKeyP *coin_pub) +{ + struct PayContext *pc = cls; + + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dci = &pc->parse_pay.dc[i]; + + if ( (0 == + GNUNET_memcmp (&dci->cdd.coin_pub, + coin_pub)) && + (0 == + strcmp (dci->exchange_url, + exchange_url)) && + (GNUNET_YES == + TALER_amount_cmp_currency (&dci->cdd.amount, + amount_with_fee)) && + (0 == + TALER_amount_cmp (&dci->cdd.amount, + amount_with_fee)) ) + { + dci->matched_in_db = true; + break; + } + } +} + + +/** + * Function called with information about a token that was spent. + * FIXME: Replace this with a more specific function for this cb + * + * @param cls closure with `struct PayContext *` + * @param spent_token_serial "serial" of the spent token unused + * @param h_contract_terms hash of the contract terms unused + * @param h_issue_pub hash of the token issue public key unused + * @param use_pub public key of the token + * @param use_sig signature of the token + * @param issue_sig signature of the token issue + */ +static void +input_tokens_paid_check ( + void *cls, + uint64_t spent_token_serial, + const struct TALER_PrivateContractHashP *h_contract_terms, + const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, + const struct TALER_TokenUsePublicKeyP *use_pub, + const struct TALER_TokenUseSignatureP *use_sig, + const struct TALER_TokenIssueSignature *issue_sig) +{ + struct PayContext *pc = cls; + + for (size_t i = 0; i<pc->parse_pay.tokens_cnt; i++) + { + struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; + + if ( (0 == + GNUNET_memcmp (&tuc->pub, + use_pub)) && + (0 == + GNUNET_memcmp (&tuc->sig, + use_sig)) && + (0 == + GNUNET_memcmp (&tuc->unblinded_sig, + issue_sig)) ) + { + tuc->found_in_db = true; + break; + } + } +} + + +/** + * Small helper function to append an output token signature from db + * + * @param cls closure with `struct PayContext *` + * @param h_issue hash of the token + * @param sig signature of the token + */ +static void +append_output_token_sig (void *cls, + struct GNUNET_HashCode *h_issue, + struct GNUNET_CRYPTO_BlindedSignature *sig) +{ + struct PayContext *pc = cls; + struct TALER_MERCHANT_ContractChoice *choice; + const struct TALER_MERCHANT_ContractOutput *output; + struct SignedOutputToken out; + unsigned int cnt; + + GNUNET_assert (TALER_MERCHANT_CONTRACT_VERSION_1 == + pc->check_contract.contract_terms->version); + choice = &pc->check_contract.contract_terms->details.v1 + .choices[pc->parse_wallet_data.choice_index]; + output = &choice->outputs[pc->output_index_gen]; + cnt = count_output_tokens (pc, + output); + out.output_index = pc->output_index_gen; + out.h_issue.hash = *h_issue; + out.sig.signature = sig; + GNUNET_CRYPTO_blind_sig_incref (sig); + GNUNET_array_append (pc->output_tokens, + pc->output_tokens_len, + out); + /* Go to next output once we've output all tokens for the current one. */ + pc->output_token_cnt++; + if (pc->output_token_cnt >= cnt) + { + pc->output_token_cnt = 0; + pc->output_index_gen++; + } +} + + +/** + * Handle case where contract was already paid. Either decides + * the payment is idempotent, or refunds the excess payment. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_contract_paid (struct PayContext *pc) +{ + json_t *refunds; + bool unmatched = false; + + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_deposits_by_order (TMH_db->cls, + pc->check_contract.order_serial, + &deposit_paid_check, + pc); + /* Since orders with choices can have a price of zero, + 0 is also a valid query state */ + if (qs < 0) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_deposits_by_order")); + return; + } + } + for (size_t i = 0; + i<pc->parse_pay.coins_cnt && ! unmatched; + i++) + { + struct DepositConfirmation *dci = &pc->parse_pay.dc[i]; + + if (! dci->matched_in_db) + unmatched = true; + } + /* Check if provided input tokens match token in the database */ + { + enum GNUNET_DB_QueryStatus qs; + + /* FIXME-Optimization: Maybe use h_contract instead of order_serial here? */ + qs = TMH_db->lookup_spent_tokens_by_order (TMH_db->cls, + pc->check_contract.order_serial, + &input_tokens_paid_check, + pc); + + if (qs < 0) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_spent_tokens_by_order")); + return; + } + } + for (size_t i = 0; i<pc->parse_pay.tokens_cnt && ! unmatched; i++) + { + struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; + + if (! tuc->found_in_db) + unmatched = true; + } + + /* In this part we are fetching token_sigs related output */ + if (! unmatched) + { + /* Everything fine, idempotent request, generate response immediately */ + enum GNUNET_DB_QueryStatus qs; + + pc->output_index_gen = 0; + qs = TMH_db->select_order_blinded_sigs ( + TMH_db->cls, + pc->order_id, + &append_output_token_sig, + pc); + if (0 > qs) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_order_blinded_sigs")); + return; + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Idempotent pay request for order `%s', signing again\n", + pc->order_id); + pc->phase = PP_SUCCESS_RESPONSE; + return; + } + /* Conflict, double-payment detected! */ + /* FIXME-#8674: What should we do with input tokens? + Currently there is no refund for tokens. */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client attempted to pay extra for already paid order `%s'\n", + pc->order_id); + refunds = json_array (); + GNUNET_assert (NULL != refunds); + for (size_t i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dci = &pc->parse_pay.dc[i]; + struct TALER_MerchantSignatureP merchant_sig; + + if (dci->matched_in_db) + continue; + TALER_merchant_refund_sign (&dci->cdd.coin_pub, + &pc->check_contract.h_contract_terms, + 0, /* rtransaction id */ + &dci->cdd.amount, + &pc->hc->instance->merchant_priv, + &merchant_sig); + GNUNET_assert ( + 0 == + json_array_append_new ( + refunds, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ( + "coin_pub", + &dci->cdd.coin_pub), + GNUNET_JSON_pack_data_auto ( + "merchant_sig", + &merchant_sig), + TALER_JSON_pack_amount ("amount", + &dci->cdd.amount), + GNUNET_JSON_pack_uint64 ("rtransaction_id", + 0)))); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Generating JSON response with code %d\n", + (int) TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID); + pay_end (pc, + TALER_MHD_REPLY_JSON_PACK ( + pc->connection, + MHD_HTTP_CONFLICT, + TALER_MHD_PACK_EC ( + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID), + GNUNET_JSON_pack_array_steal ("refunds", + refunds))); +} + + +/** + * Check the database state for the given order. + * Schedules an error response in the connection on failure. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_check_contract (struct PayContext *pc) +{ + /* obtain contract terms */ + enum GNUNET_DB_QueryStatus qs; + bool paid = false; + + if (NULL != pc->check_contract.contract_terms_json) + { + json_decref (pc->check_contract.contract_terms_json); + pc->check_contract.contract_terms_json = NULL; + } + if (NULL != pc->check_contract.contract_terms) + { + TALER_MERCHANT_contract_free (pc->check_contract.contract_terms); + pc->check_contract.contract_terms = NULL; + } + qs = TMH_db->lookup_contract_terms2 (TMH_db->cls, + pc->hc->instance->settings.id, + pc->order_id, + &pc->check_contract.contract_terms_json, + &pc->check_contract.order_serial, + &paid, + NULL, + &pc->check_contract.pos_key, + &pc->check_contract.pos_alg); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract terms")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + pc->order_id)); + return; + } + /* hash contract (needed later) */ +#if DEBUG + json_dumpf (pc->check_contract.contract_terms_json, + stderr, + JSON_INDENT (2)); +#endif + if (GNUNET_OK != + TALER_JSON_contract_hash (pc->check_contract.contract_terms_json, + &pc->check_contract.h_contract_terms)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + NULL)); + return; + } + + /* Parse the contract terms even for paid orders, + as later phases need it. */ + + pc->check_contract.contract_terms = TALER_MERCHANT_contract_parse ( + pc->check_contract.contract_terms_json, + true); + + if (NULL == pc->check_contract.contract_terms) + { + /* invalid contract */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + pc->order_id)); + return; + } + + if (paid) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' paid, checking for double-payment\n", + pc->order_id); + pc->phase = PP_CONTRACT_PAID; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling payment for order `%s' with contract hash `%s'\n", + pc->order_id, + GNUNET_h2s (&pc->check_contract.h_contract_terms.hash)); + + /* Check fundamentals */ + { + switch (pc->check_contract.contract_terms->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + { + if (pc->parse_wallet_data.choice_index > 0) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, + "contract terms v0 has no choices")); + return; + } + } + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + { + if (pc->parse_wallet_data.choice_index < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' has non-empty choices array but" + "request is missing 'choice_index' field\n", + pc->order_id); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING, + NULL)); + return; + } + if (pc->parse_wallet_data.choice_index >= + pc->check_contract.contract_terms->details.v1.choices_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' has choices array with %u elements but " + "request has 'choice_index' field with value %d\n", + pc->order_id, + pc->check_contract.contract_terms->details.v1.choices_len, + pc->parse_wallet_data.choice_index); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, + NULL)); + return; + } + } + break; + default: + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract 'version' in database not supported by this backend") + ); + return; + } + } + + if (GNUNET_TIME_timestamp_cmp (pc->check_contract.contract_terms-> + wire_deadline, + <, + pc->check_contract.contract_terms-> + refund_deadline)) + { + /* This should already have been checked when creating the order! */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, + NULL)); + return; + } + if (GNUNET_TIME_absolute_is_past (pc->check_contract.contract_terms-> + pay_deadline.abs_time)) + { + /* too late */ + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_GONE, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, + NULL)); + return; + } + +/* Make sure wire method (still) exists for this instance */ + { + struct TMH_WireMethod *wm; + + wm = pc->hc->instance->wm_head; + while (0 != GNUNET_memcmp (&pc->check_contract.contract_terms->h_wire, + &wm->h_wire)) + wm = wm->next; + if (NULL == wm) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, + NULL)); + return; + } + pc->check_contract.wm = wm; + } + pc->phase = PP_VALIDATE_TOKENS; +} + + +/** + * Try to parse the wallet_data object of the pay request into + * the given context. Schedules an error response in the connection + * on failure. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_parse_wallet_data (struct PayContext *pc) +{ + const json_t *tokens_evs; + const json_t *donau_obj; + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_int16 ("choice_index", + &pc->parse_wallet_data.choice_index), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("tokens_evs", + &tokens_evs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("donau", + &donau_obj), + NULL), + GNUNET_JSON_spec_end () + }; + + pc->parse_wallet_data.choice_index = -1; + if (NULL == pc->parse_pay.wallet_data) + { + pc->phase = PP_CHECK_CONTRACT; + return; + } + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + pc->parse_pay.wallet_data, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + } + + pc->parse_wallet_data.token_envelopes_cnt + = json_array_size (tokens_evs); + if (pc->parse_wallet_data.token_envelopes_cnt > + MAX_TOKEN_ALLOWED_OUTPUTS) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'tokens_evs' array too long")); + return; + } + pc->parse_wallet_data.token_envelopes + = GNUNET_new_array (pc->parse_wallet_data.token_envelopes_cnt, + struct TokenEnvelope); + + { + unsigned int tokens_ev_index; + json_t *token_ev; + + json_array_foreach (tokens_evs, + tokens_ev_index, + token_ev) + { + struct TokenEnvelope *ev + = &pc->parse_wallet_data.token_envelopes[tokens_ev_index]; + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_token_envelope (NULL, + &ev->blinded_token), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + if (json_is_null (token_ev)) + continue; + res = TALER_MHD_parse_json_data (pc->connection, + token_ev, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + + for (unsigned int j = 0; j<tokens_ev_index; j++) + { + if (0 == + GNUNET_memcmp (ev->blinded_token.blinded_pub, + pc->parse_wallet_data.token_envelopes[j]. + blinded_token.blinded_pub)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate token envelope in list")); + return; + } + } + } + } + +#ifdef HAVE_DONAU_DONAU_SERVICE_H + if (NULL != donau_obj) + { + const char *donau_url_tmp; + const json_t *budikeypairs; + json_t *donau_keys_json; + + /* Fetching and checking that all 3 are present in some way */ + struct GNUNET_JSON_Specification dspec[] = { + GNUNET_JSON_spec_string ("url", + &donau_url_tmp), + GNUNET_JSON_spec_uint64 ("year", + &pc->parse_wallet_data.donau.donation_year), + GNUNET_JSON_spec_array_const ("budikeypairs", + &budikeypairs), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + donau_obj, + dspec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + + /* Check if the needed data is present for the given donau URL */ + { + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_order_charity ( + TMH_db->cls, + pc->hc->instance->settings.id, + donau_url_tmp, + &pc->parse_wallet_data.charity_id, + &pc->parse_wallet_data.charity_priv, + &pc->parse_wallet_data.charity_max_per_year, + &pc->parse_wallet_data.charity_receipts_to_date, + &donau_keys_json, + &pc->parse_wallet_data.donau_instance_serial); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_order_charity")); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + TMH_db->rollback (TMH_db->cls); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_DONAU_CHARITY_UNKNOWN, + donau_url_tmp)); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + pc->parse_wallet_data.donau.donau_url = + GNUNET_strdup (donau_url_tmp); + break; + } + } + + { + pc->parse_wallet_data.donau_keys = + DONAU_keys_from_json (donau_keys_json); + json_decref (donau_keys_json); + if (NULL == pc->parse_wallet_data.donau_keys) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Invalid donau_keys")); + return; + } + } + + /* Stage to parse the budikeypairs from json to struct */ + if (0 != json_array_size (budikeypairs)) + { + size_t num_bkps = json_array_size (budikeypairs); + struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps = + GNUNET_new_array (num_bkps, + struct DONAU_BlindedUniqueDonorIdentifierKeyPair); + + /* Change to json for each*/ + for (size_t i = 0; i < num_bkps; i++) + { + const json_t *bkp_obj = json_array_get (budikeypairs, + i); + if (GNUNET_SYSERR == + merchant_parse_json_bkp (&bkps[i], + bkp_obj)) + { + GNUNET_break_op (0); + for (size_t j = 0; i < j; j++) + GNUNET_CRYPTO_blinded_message_decref ( + bkps[j].blinded_udi.blinded_message); + GNUNET_free (bkps); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Failed to parse budikeypairs")); + return; + } + } + + pc->parse_wallet_data.num_bkps = num_bkps; + pc->parse_wallet_data.bkps = bkps; + } + } +#else + /* Donau not compiled in: reject request if a donau object was given. */ + if (NULL != donau_obj) + { + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_MERCHANT_GENERIC_DONAU_NOT_CONFIGURED, + "donau support disabled")); + return; + } +#endif /* HAVE_DONAU_DONAU_SERVICE_H */ + + TALER_json_hash (pc->parse_pay.wallet_data, + &pc->parse_wallet_data.h_wallet_data); + + pc->phase = PP_CHECK_CONTRACT; +} + + +/** + * Try to parse the pay request into the given pay context. + * Schedules an error response in the connection on failure. + * + * @param[in,out] pc context we use to handle the payment + */ +static void +phase_parse_pay (struct PayContext *pc) +{ + const char *session_id = NULL; + const json_t *coins; + const json_t *tokens; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("coins", + &coins), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("session_id", + &session_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("wallet_data", + &pc->parse_pay.wallet_data), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("tokens", + &tokens), + NULL), + GNUNET_JSON_spec_end () + }; + +#if DEBUG + { + char *dump = json_dumps (pc->hc->request_body, + JSON_INDENT (2) + | JSON_ENCODE_ANY + | JSON_SORT_KEYS); + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "POST /orders/%s/pay – request body follows:\n%s\n", + pc->order_id, + dump); + + free (dump); + + } +#endif /* DEBUG */ + + GNUNET_assert (PP_PARSE_PAY == pc->phase); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + pc->hc->request_body, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + } + + /* copy session ID (if set) */ + if (NULL != session_id) + { + pc->parse_pay.session_id = GNUNET_strdup (session_id); + } + else + { + /* use empty string as default if client didn't specify it */ + pc->parse_pay.session_id = GNUNET_strdup (""); + } + + pc->parse_pay.coins_cnt = json_array_size (coins); + if (pc->parse_pay.coins_cnt > MAX_COIN_ALLOWED_COINS) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'coins' array too long")); + return; + } + /* note: 1 coin = 1 deposit confirmation expected */ + pc->parse_pay.dc = GNUNET_new_array (pc->parse_pay.coins_cnt, + struct DepositConfirmation); + + /* This loop populates the array 'dc' in 'pc' */ + { + unsigned int coins_index; + json_t *coin; + + json_array_foreach (coins, coins_index, coin) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[coins_index]; + const char *exchange_url; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_sig", + &dc->cdd.coin_sig), + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &dc->cdd.coin_pub), + TALER_JSON_spec_denom_sig ("ub_sig", + &dc->cdd.denom_sig), + GNUNET_JSON_spec_fixed_auto ("h_denom", + &dc->cdd.h_denom_pub), + TALER_JSON_spec_amount_any ("contribution", + &dc->cdd.amount), + TALER_JSON_spec_web_url ("exchange_url", + &exchange_url), + /* if a minimum age was required, the minimum_age_sig and + * age_commitment must be provided */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", + &dc->minimum_age_sig), + &dc->no_minimum_age_sig), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_age_commitment ("age_commitment", + &dc->age_commitment), + &dc->no_age_commitment), + /* if minimum age was not required, but coin with age restriction set + * was used, h_age_commitment must be provided. */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("h_age_commitment", + &dc->cdd.h_age_commitment), + &dc->no_h_age_commitment), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + struct ExchangeGroup *eg = NULL; + + res = TALER_MHD_parse_json_data (pc->connection, + coin, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + for (unsigned int j = 0; j<coins_index; j++) + { + if (0 == + GNUNET_memcmp (&dc->cdd.coin_pub, + &pc->parse_pay.dc[j].cdd.coin_pub)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate coin in list")); + return; + } + } + + dc->exchange_url = GNUNET_strdup (exchange_url); + dc->index = coins_index; + dc->pc = pc; + + /* Check the consistency of the (potential) age restriction + * information. */ + if (dc->no_age_commitment != dc->no_minimum_age_sig) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inconsistent: 'age_commitment' vs. 'minimum_age_sig'" + )); + return; + } + + /* Setup exchange group */ + for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) + { + if (0 == + strcmp (pc->parse_pay.egs[i]->exchange_url, + exchange_url)) + { + eg = pc->parse_pay.egs[i]; + break; + } + } + if (NULL == eg) + { + eg = GNUNET_new (struct ExchangeGroup); + eg->pc = pc; + eg->exchange_url = dc->exchange_url; + eg->total = dc->cdd.amount; + GNUNET_array_append (pc->parse_pay.egs, + pc->parse_pay.num_exchanges, + eg); + } + else + { + if (0 > + TALER_amount_add (&eg->total, + &eg->total, + &dc->cdd.amount)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts")); + return; + } + } + } + } + + pc->parse_pay.tokens_cnt = json_array_size (tokens); + if (pc->parse_pay.tokens_cnt > MAX_TOKEN_ALLOWED_INPUTS) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'tokens' array too long")); + return; + } + + pc->parse_pay.tokens = GNUNET_new_array (pc->parse_pay.tokens_cnt, + struct TokenUseConfirmation); + + /* This loop populates the array 'tokens' in 'pc' */ + { + unsigned int tokens_index; + json_t *token; + + json_array_foreach (tokens, tokens_index, token) + { + struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[tokens_index]; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_fixed_auto ("token_sig", + &tuc->sig), + GNUNET_JSON_spec_fixed_auto ("token_pub", + &tuc->pub), + GNUNET_JSON_spec_fixed_auto ("h_issue", + &tuc->h_issue), + TALER_JSON_spec_token_issue_sig ("ub_sig", + &tuc->unblinded_sig), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (pc->connection, + token, + ispec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + + for (unsigned int j = 0; j<tokens_index; j++) + { + if (0 == + GNUNET_memcmp (&tuc->pub, + &pc->parse_pay.tokens[j].pub)) + { + GNUNET_break_op (0); + pay_end (pc, + TALER_MHD_reply_with_error (pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duplicate token in list")); + return; + } + } + } + } + + pc->phase = PP_PARSE_WALLET_DATA; +} + + +/** + * Custom cleanup routine for a `struct PayContext`. + * + * @param cls the `struct PayContext` to clean up. + */ +static void +pay_context_cleanup (void *cls) +{ + struct PayContext *pc = cls; + + if (NULL != pc->batch_deposits.timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->batch_deposits.timeout_task); + pc->batch_deposits.timeout_task = NULL; + } + if (NULL != pc->check_contract.contract_terms_json) + { + json_decref (pc->check_contract.contract_terms_json); + pc->check_contract.contract_terms_json = NULL; + } + for (unsigned int i = 0; i<pc->parse_pay.coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->parse_pay.dc[i]; + + TALER_denom_sig_free (&dc->cdd.denom_sig); + GNUNET_free (dc->exchange_url); + } + GNUNET_free (pc->parse_pay.dc); + for (unsigned int i = 0; i<pc->parse_pay.tokens_cnt; i++) + { + struct TokenUseConfirmation *tuc = &pc->parse_pay.tokens[i]; + + TALER_token_issue_sig_free (&tuc->unblinded_sig); + } + GNUNET_free (pc->parse_pay.tokens); + for (unsigned int i = 0; i<pc->parse_pay.num_exchanges; i++) + { + struct ExchangeGroup *eg = pc->parse_pay.egs[i]; + + if (NULL != eg->fo) + TMH_EXCHANGES_keys4exchange_cancel (eg->fo); + GNUNET_free (eg); + } + GNUNET_free (pc->parse_pay.egs); + if (NULL != pc->check_contract.contract_terms) + { + TALER_MERCHANT_contract_free (pc->check_contract.contract_terms); + pc->check_contract.contract_terms = NULL; + } + if (NULL != pc->response) + { + MHD_destroy_response (pc->response); + pc->response = NULL; + } + GNUNET_free (pc->parse_pay.session_id); + GNUNET_CONTAINER_DLL_remove (pc_head, + pc_tail, + pc); + GNUNET_free (pc->check_contract.pos_key); + GNUNET_free (pc->compute_money_pots.pots); + GNUNET_free (pc->compute_money_pots.increments); +#ifdef HAVE_DONAU_DONAU_SERVICE_H + if (NULL != pc->parse_wallet_data.bkps) + { + for (size_t i = 0; i < pc->parse_wallet_data.num_bkps; i++) + GNUNET_CRYPTO_blinded_message_decref ( + pc->parse_wallet_data.bkps[i].blinded_udi.blinded_message); + GNUNET_array_grow (pc->parse_wallet_data.bkps, + pc->parse_wallet_data.num_bkps, + 0); + } + if (NULL != pc->parse_wallet_data.donau_keys) + { + DONAU_keys_decref (pc->parse_wallet_data.donau_keys); + pc->parse_wallet_data.donau_keys = NULL; + } + GNUNET_free (pc->parse_wallet_data.donau.donau_url); +#endif + for (unsigned int i = 0; i<pc->parse_wallet_data.token_envelopes_cnt; i++) + { + struct TokenEnvelope *ev + = &pc->parse_wallet_data.token_envelopes[i]; + + GNUNET_CRYPTO_blinded_message_decref (ev->blinded_token.blinded_pub); + } + GNUNET_free (pc->parse_wallet_data.token_envelopes); + if (NULL != pc->output_tokens) + { + for (unsigned int i = 0; i<pc->output_tokens_len; i++) + if (NULL != pc->output_tokens[i].sig.signature) + GNUNET_CRYPTO_blinded_sig_decref (pc->output_tokens[i].sig.signature); + GNUNET_free (pc->output_tokens); + pc->output_tokens = NULL; + } + GNUNET_free (pc); +} + + +MHD_RESULT +TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct PayContext *pc = hc->ctx; + + GNUNET_assert (NULL != hc->infix); + if (NULL == pc) + { + pc = GNUNET_new (struct PayContext); + pc->connection = connection; + pc->hc = hc; + pc->order_id = hc->infix; + hc->ctx = pc; + hc->cc = &pay_context_cleanup; + GNUNET_CONTAINER_DLL_insert (pc_head, + pc_tail, + pc); + } + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing /pay in phase %d\n", + (int) pc->phase); + switch (pc->phase) + { + case PP_PARSE_PAY: + phase_parse_pay (pc); + break; + case PP_PARSE_WALLET_DATA: + phase_parse_wallet_data (pc); + break; + case PP_CHECK_CONTRACT: + phase_check_contract (pc); + break; + case PP_VALIDATE_TOKENS: + phase_validate_tokens (pc); + break; + case PP_CONTRACT_PAID: + phase_contract_paid (pc); + break; + case PP_COMPUTE_MONEY_POTS: + phase_compute_money_pots (pc); + break; + case PP_PAY_TRANSACTION: + phase_execute_pay_transaction (pc); + break; + case PP_REQUEST_DONATION_RECEIPT: +#ifdef HAVE_DONAU_DONAU_SERVICE_H + phase_request_donation_receipt (pc); +#else + pc->phase++; +#endif + break; + case PP_FINAL_OUTPUT_TOKEN_PROCESSING: + phase_final_output_token_processing (pc); + break; + case PP_PAYMENT_NOTIFICATION: + phase_payment_notification (pc); + break; + case PP_SUCCESS_RESPONSE: + phase_success_response (pc); + break; + case PP_BATCH_DEPOSITS: + phase_batch_deposits (pc); + break; + case PP_RETURN_RESPONSE: + phase_return_response (pc); + break; + case PP_FAIL_LEGAL_REASONS: + phase_fail_for_legal_reasons (pc); + break; + case PP_END_YES: + return MHD_YES; + case PP_END_NO: + return MHD_NO; + default: + /* should not be reachable */ + GNUNET_assert (0); + return MHD_NO; + } + switch (pc->suspended) + { + case GNUNET_SYSERR: + /* during shutdown, we don't generate any more replies */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing /pay ends due to shutdown in phase %d\n", + (int) pc->phase); + return MHD_NO; + case GNUNET_NO: + /* continue to next phase */ + break; + case GNUNET_YES: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing /pay suspended in phase %d\n", + (int) pc->phase); + return MHD_YES; + } + } + /* impossible to get here */ + GNUNET_assert (0); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_post-orders-ORDER_ID-pay.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.h b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + (C) 2014-2020 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/> +*/ +/** + * @file taler-merchant-httpd_post-orders-ORDER_ID-pay.h + * @brief headers for POST /orders/$ID/pay handler + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAY_H +#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_PAY_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +TMH_force_pc_resume (void); + + +/** + * Process payment for a claimed order. + * + * @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_post_orders_ID_pay (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.c @@ -0,0 +1,846 @@ +/* + This file is part of TALER + (C) 2020-2022 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-httpd_post-orders-ORDER_ID-refund.c + * @brief handling of POST /orders/$ID/refund requests + * @author Jonathan Buchanan + */ +#include "taler/platform.h" +#include <taler/taler_dbevents.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_get-exchanges.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-refund.h" + + +/** + * Information we keep for each coin to be refunded. + */ +struct CoinRefund +{ + + /** + * Kept in a DLL. + */ + struct CoinRefund *next; + + /** + * Kept in a DLL. + */ + struct CoinRefund *prev; + + /** + * Request to connect to the target exchange. + */ + struct TMH_EXCHANGES_KeysOperation *fo; + + /** + * Handle for the refund operation with the exchange. + */ + struct TALER_EXCHANGE_PostCoinsRefundHandle *rh; + + /** + * Request this operation is part of. + */ + struct PostRefundData *prd; + + /** + * URL of the exchange for this @e coin_pub. + */ + char *exchange_url; + + /** + * Fully reply from the exchange, only possibly set if + * we got a JSON reply and a non-#MHD_HTTP_OK error code + */ + json_t *exchange_reply; + + /** + * When did the merchant grant the refund. To be used to group events + * in the wallet. + */ + struct GNUNET_TIME_Timestamp execution_time; + + /** + * Coin to refund. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Refund transaction ID to use. + */ + uint64_t rtransaction_id; + + /** + * Unique serial number identifying the refund. + */ + uint64_t refund_serial; + + /** + * Amount to refund. + */ + struct TALER_Amount refund_amount; + + /** + * Public key of the exchange affirming the refund. + */ + struct TALER_ExchangePublicKeyP exchange_pub; + + /** + * Signature of the exchange affirming the refund. + */ + struct TALER_ExchangeSignatureP exchange_sig; + + /** + * HTTP status from the exchange, #MHD_HTTP_OK if + * @a exchange_pub and @a exchange_sig are valid. + */ + unsigned int exchange_status; + + /** + * HTTP error code from the exchange. + */ + enum TALER_ErrorCode exchange_code; + +}; + + +/** + * Context for the operation. + */ +struct PostRefundData +{ + + /** + * Hashed version of contract terms. All zeros if not provided. + */ + struct TALER_PrivateContractHashP h_contract_terms; + + /** + * DLL of (suspended) requests. + */ + struct PostRefundData *next; + + /** + * DLL of (suspended) requests. + */ + struct PostRefundData *prev; + + /** + * Refunds for this order. Head of DLL. + */ + struct CoinRefund *cr_head; + + /** + * Refunds for this order. Tail of DLL. + */ + struct CoinRefund *cr_tail; + + /** + * Context of the request. + */ + struct TMH_HandlerContext *hc; + + /** + * Entry in the #resume_timeout_heap for this check payment, if we are + * suspended. + */ + struct TMH_SuspendedConnection sc; + + /** + * order ID for the payment + */ + const char *order_id; + + /** + * Where to get the contract + */ + const char *contract_url; + + /** + * fulfillment URL of the contract (valid as long as + * @e contract_terms is valid). + */ + const char *fulfillment_url; + + /** + * session of the client + */ + const char *session_id; + + /** + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. + */ + json_t *contract_terms; + + /** + * Total refunds granted for this payment. Only initialized + * if @e refunded is set to true. + */ + struct TALER_Amount refund_amount; + + /** + * Did we suspend @a connection and are thus in + * the #prd_head DLL (#GNUNET_YES). Set to + * #GNUNET_NO if we are not suspended, and to + * #GNUNET_SYSERR if we should close the connection + * without a response due to shutdown. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Return code: #TALER_EC_NONE if successful. + */ + enum TALER_ErrorCode ec; + + /** + * HTTP status to use for the reply, 0 if not yet known. + */ + unsigned int http_status; + + /** + * Set to true if we are dealing with an unclaimed order + * (and thus @e h_contract_terms is not set, and certain + * DB queries will not work). + */ + bool unclaimed; + + /** + * Set to true if this payment has been refunded and + * @e refund_amount is initialized. + */ + bool refunded; + + /** + * Set to true if a refund is still available for the + * wallet for this payment. + */ + bool refund_available; + + /** + * Set to true if the client requested HTML, otherwise + * we generate JSON. + */ + bool generate_html; + +}; + + +/** + * Head of DLL of (suspended) requests. + */ +static struct PostRefundData *prd_head; + +/** + * Tail of DLL of (suspended) requests. + */ +static struct PostRefundData *prd_tail; + + +/** + * Function called when we are done processing a refund request. + * Frees memory associated with @a ctx. + * + * @param ctx a `struct PostRefundData` + */ +static void +refund_cleanup (void *ctx) +{ + struct PostRefundData *prd = ctx; + struct CoinRefund *cr; + + while (NULL != (cr = prd->cr_head)) + { + GNUNET_CONTAINER_DLL_remove (prd->cr_head, + prd->cr_tail, + cr); + json_decref (cr->exchange_reply); + GNUNET_free (cr->exchange_url); + if (NULL != cr->fo) + { + TMH_EXCHANGES_keys4exchange_cancel (cr->fo); + cr->fo = NULL; + } + if (NULL != cr->rh) + { + TALER_EXCHANGE_post_coins_refund_cancel (cr->rh); + cr->rh = NULL; + } + GNUNET_free (cr); + } + json_decref (prd->contract_terms); + GNUNET_free (prd); +} + + +/** + * Force resuming all suspended order lookups, needed during shutdown. + */ +void +TMH_force_wallet_refund_order_resume (void) +{ + struct PostRefundData *prd; + + while (NULL != (prd = prd_head)) + { + GNUNET_CONTAINER_DLL_remove (prd_head, + prd_tail, + prd); + GNUNET_assert (GNUNET_YES == prd->suspended); + prd->suspended = GNUNET_SYSERR; + MHD_resume_connection (prd->sc.con); + } +} + + +/** + * Check if @a prd has exchange requests still pending. + * + * @param prd state to check + * @return true if activities are still pending + */ +static bool +exchange_operations_pending (struct PostRefundData *prd) +{ + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + if ( (NULL != cr->fo) || + (NULL != cr->rh) ) + return true; + } + return false; +} + + +/** + * Check if @a prd is ready to be resumed, and if so, do it. + * + * @param prd refund request to be possibly ready + */ +static void +check_resume_prd (struct PostRefundData *prd) +{ + if ( (TALER_EC_NONE == prd->ec) && + exchange_operations_pending (prd) ) + return; + GNUNET_CONTAINER_DLL_remove (prd_head, + prd_tail, + prd); + GNUNET_assert (prd->suspended); + prd->suspended = GNUNET_NO; + MHD_resume_connection (prd->sc.con); + TALER_MHD_daemon_trigger (); +} + + +/** + * Notify applications waiting for a client to obtain + * a refund. + * + * @param prd refund request with the change + */ +static void +notify_refund_obtained (struct PostRefundData *prd) +{ + struct TMH_OrderPayEventP refund_eh = { + .header.size = htons (sizeof (refund_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_REFUND_OBTAINED), + .merchant_pub = prd->hc->instance->merchant_pub + }; + + GNUNET_CRYPTO_hash (prd->order_id, + strlen (prd->order_id), + &refund_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &refund_eh.header, + NULL, + 0); +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * refund request to an exchange. + * + * @param cls a `struct CoinRefund` + * @param rr response data + */ +static void +refund_cb (void *cls, + const struct TALER_EXCHANGE_PostCoinsRefundResponse *rr) +{ + struct CoinRefund *cr = cls; + const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr; + + cr->rh = NULL; + cr->exchange_status = hr->http_status; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Exchange refund status for coin %s is %u\n", + TALER_B2S (&cr->coin_pub), + hr->http_status); + switch (hr->http_status) + { + case MHD_HTTP_OK: + { + enum GNUNET_DB_QueryStatus qs; + + cr->exchange_pub = rr->details.ok.exchange_pub; + cr->exchange_sig = rr->details.ok.exchange_sig; + qs = TMH_db->insert_refund_proof (TMH_db->cls, + cr->refund_serial, + &rr->details.ok.exchange_sig, + &rr->details.ok.exchange_pub); + if (0 >= qs) + { + /* generally, this is relatively harmless for the merchant, but let's at + least log this. */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to persist exchange response to /refund in database: %d\n", + qs); + } + else + { + notify_refund_obtained (cr->prd); + } + } + break; + default: + cr->exchange_code = hr->ec; + cr->exchange_reply = json_incref ((json_t*) hr->reply); + break; + } + check_resume_prd (cr->prd); +} + + +/** + * Function called with the result of a + * #TMH_EXCHANGES_keys4exchange() + * operation. + * + * @param cls a `struct CoinRefund *` + * @param keys keys of exchange, NULL on error + * @param exchange representation of the exchange + */ +static void +exchange_found_cb (void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct CoinRefund *cr = cls; + struct PostRefundData *prd = cr->prd; + + (void) exchange; + cr->fo = NULL; + if (NULL == keys) + { + prd->http_status = MHD_HTTP_GATEWAY_TIMEOUT; + prd->ec = TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT; + check_resume_prd (prd); + return; + } + cr->rh = TALER_EXCHANGE_post_coins_refund_create ( + TMH_curl_ctx, + cr->exchange_url, + keys, + &cr->refund_amount, + &prd->h_contract_terms, + &cr->coin_pub, + cr->rtransaction_id, + &prd->hc->instance->merchant_priv); + GNUNET_assert (NULL != cr->rh); + GNUNET_assert (TALER_EC_NONE == + TALER_EXCHANGE_post_coins_refund_start (cr->rh, + &refund_cb, + cr)); +} + + +/** + * Function called with information about a refund. + * It is responsible for summing up the refund amount. + * + * @param cls closure + * @param refund_serial unique serial number of the refund + * @param timestamp time of the refund (for grouping of refunds in the wallet UI) + * @param coin_pub public coin from which the refund comes from + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param rtransaction_id identificator of the refund + * @param reason human-readable explanation of the refund + * @param refund_amount refund amount which is being taken from @a coin_pub + * @param pending true if the this refund was not yet processed by the wallet/exchange + */ +static void +process_refunds_cb (void *cls, + uint64_t refund_serial, + struct GNUNET_TIME_Timestamp timestamp, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + uint64_t rtransaction_id, + const char *reason, + const struct TALER_Amount *refund_amount, + bool pending) +{ + struct PostRefundData *prd = cls; + struct CoinRefund *cr; + + for (cr = prd->cr_head; + NULL != cr; + cr = cr->next) + if (cr->refund_serial == refund_serial) + return; + /* already known */ + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Found refund of %s for coin %s with reason `%s' in database\n", + TALER_amount2s (refund_amount), + TALER_B2S (coin_pub), + reason); + cr = GNUNET_new (struct CoinRefund); + cr->refund_serial = refund_serial; + cr->exchange_url = GNUNET_strdup (exchange_url); + cr->prd = prd; + cr->coin_pub = *coin_pub; + cr->rtransaction_id = rtransaction_id; + cr->refund_amount = *refund_amount; + cr->execution_time = timestamp; + GNUNET_CONTAINER_DLL_insert (prd->cr_head, + prd->cr_tail, + cr); + if (prd->refunded) + { + GNUNET_assert (0 <= + TALER_amount_add (&prd->refund_amount, + &prd->refund_amount, + refund_amount)); + return; + } + prd->refund_amount = *refund_amount; + prd->refunded = true; + prd->refund_available |= pending; +} + + +/** + * Obtain refunds for an order. + * + * @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_post_orders_ID_refund (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct PostRefundData *prd = hc->ctx; + enum GNUNET_DB_QueryStatus qs; + + if (NULL == prd) + { + prd = GNUNET_new (struct PostRefundData); + prd->sc.con = connection; + prd->hc = hc; + prd->order_id = hc->infix; + hc->ctx = prd; + hc->cc = &refund_cleanup; + { + enum GNUNET_GenericReturnValue res; + + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("h_contract", + &prd->h_contract_terms), + GNUNET_JSON_spec_end () + }; + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + TMH_db->preflight (TMH_db->cls); + { + json_t *contract_terms; + uint64_t order_serial; + + qs = TMH_db->lookup_contract_terms (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &contract_terms, + &order_serial, + NULL); + if (0 > qs) + { + /* single, read-only SQL statements should never cause + serialization problems */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract terms"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + hc->infix); + } + { + struct TALER_PrivateContractHashP h_contract_terms; + + if (GNUNET_OK != + TALER_JSON_contract_hash (contract_terms, + &h_contract_terms)) + { + GNUNET_break (0); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + NULL); + } + json_decref (contract_terms); + if (0 != GNUNET_memcmp (&h_contract_terms, + &prd->h_contract_terms)) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + } + } + } + } + if (GNUNET_SYSERR == prd->suspended) + return MHD_NO; /* we are in shutdown */ + + if (TALER_EC_NONE != prd->ec) + { + GNUNET_break (0 != prd->http_status); + /* kill pending coin refund operations immediately, just to be + extra sure they don't modify 'prd' after we already created + a reply (this might not be needed, but feels safer). */ + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + if (NULL != cr->fo) + { + TMH_EXCHANGES_keys4exchange_cancel (cr->fo); + cr->fo = NULL; + } + if (NULL != cr->rh) + { + TALER_EXCHANGE_post_coins_refund_cancel (cr->rh); + cr->rh = NULL; + } + } + return TALER_MHD_reply_with_error (connection, + prd->http_status, + prd->ec, + NULL); + } + + qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, + hc->instance->settings.id, + &prd->h_contract_terms, + &process_refunds_cb, + prd); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "detailed refunds"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "no coins found that could be refunded"); + } + + /* Now launch exchange interactions, unless we already have the + response in the database! */ + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + qs = TMH_db->lookup_refund_proof (TMH_db->cls, + cr->refund_serial, + &cr->exchange_sig, + &cr->exchange_pub); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "refund proof"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + if (NULL == cr->exchange_reply) + { + /* We need to talk to the exchange */ + cr->fo = TMH_EXCHANGES_keys4exchange (cr->exchange_url, + false, + &exchange_found_cb, + cr); + if (NULL == cr->fo) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, + cr->exchange_url); + } + } + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* We got a reply earlier, set status accordingly */ + cr->exchange_status = MHD_HTTP_OK; + break; + } + } + + /* Check if there are still exchange operations pending */ + if (exchange_operations_pending (prd)) + { + if (GNUNET_NO == prd->suspended) + { + prd->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + GNUNET_CONTAINER_DLL_insert (prd_head, + prd_tail, + prd); + } + return MHD_YES; /* we're still talking to the exchange */ + } + + { + json_t *ra; + + ra = json_array (); + GNUNET_assert (NULL != ra); + for (struct CoinRefund *cr = prd->cr_head; + NULL != cr; + cr = cr->next) + { + json_t *refund; + + if (MHD_HTTP_OK != cr->exchange_status) + { + if (NULL == cr->exchange_reply) + { + refund = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "failure"), + GNUNET_JSON_pack_uint64 ("exchange_status", + cr->exchange_status), + GNUNET_JSON_pack_uint64 ("rtransaction_id", + cr->rtransaction_id), + GNUNET_JSON_pack_data_auto ("coin_pub", + &cr->coin_pub), + TALER_JSON_pack_amount ("refund_amount", + &cr->refund_amount), + GNUNET_JSON_pack_timestamp ("execution_time", + cr->execution_time)); + } + else + { + refund = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "failure"), + GNUNET_JSON_pack_uint64 ("exchange_status", + cr->exchange_status), + GNUNET_JSON_pack_uint64 ("exchange_code", + cr->exchange_code), + GNUNET_JSON_pack_object_incref ("exchange_reply", + cr->exchange_reply), + GNUNET_JSON_pack_uint64 ("rtransaction_id", + cr->rtransaction_id), + GNUNET_JSON_pack_data_auto ("coin_pub", + &cr->coin_pub), + TALER_JSON_pack_amount ("refund_amount", + &cr->refund_amount), + GNUNET_JSON_pack_timestamp ("execution_time", + cr->execution_time)); + } + } + else + { + refund = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "success"), + GNUNET_JSON_pack_uint64 ("exchange_status", + cr->exchange_status), + GNUNET_JSON_pack_data_auto ("exchange_sig", + &cr->exchange_sig), + GNUNET_JSON_pack_data_auto ("exchange_pub", + &cr->exchange_pub), + GNUNET_JSON_pack_uint64 ("rtransaction_id", + cr->rtransaction_id), + GNUNET_JSON_pack_data_auto ("coin_pub", + &cr->coin_pub), + TALER_JSON_pack_amount ("refund_amount", + &cr->refund_amount), + GNUNET_JSON_pack_timestamp ("execution_time", + cr->execution_time)); + } + GNUNET_assert ( + 0 == + json_array_append_new (ra, + refund)); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + TALER_JSON_pack_amount ("refund_amount", + &prd->refund_amount), + GNUNET_JSON_pack_array_steal ("refunds", + ra), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &hc->instance->merchant_pub)); + } + + return MHD_YES; +} + + +/* end of taler-merchant-httpd_post-orders-ORDER_ID-refund.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.h b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-refund.h @@ -0,0 +1,48 @@ +/* + This file is part of TALER + (C) 2020 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/> +*/ +/** + * @file taler-merchant-httpd_post-orders-ORDER_ID-refund.h + * @brief headers for POST /orders/$ID/refund handler + * @author Jonathan Buchanan + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_REFUND_H +#define TALER_EXCHANGE_HTTPD_POST_ORDERS_ID_REFUND_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Obtain refunds for an order. + * + * @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_post_orders_ID_refund (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Force resuming all suspended order lookups, needed during shutdown. + */ +void +TMH_force_wallet_refund_order_resume (void); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c @@ -0,0 +1,122 @@ +/* + This file is part of TALER + (C) 2026 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-httpd_post-orders-ORDER_ID-unclaim.c + * @brief headers for POST /orders/$ID/unclaim handler + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_signatures.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h" + + +MHD_RESULT +TMH_post_orders_ID_unclaim (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *order_id = hc->infix; + struct GNUNET_CRYPTO_EddsaPublicKey nonce; + struct GNUNET_CRYPTO_EddsaSignature nsig; + struct GNUNET_HashCode h_contract; + enum GNUNET_DB_QueryStatus qs; + + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("unclaim_sig", + &nsig), + GNUNET_JSON_spec_fixed_auto ("nonce", + &nonce), + GNUNET_JSON_spec_fixed_auto ("h_contract", + &h_contract), + GNUNET_JSON_spec_end () + }; + 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; + } + } + + if (GNUNET_OK != + TALER_wallet_order_unclaim_verify (&h_contract, + &nonce, + &nsig)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unclaim_sig"); + } + TMH_db->preflight (TMH_db->cls); + qs = TMH_db->insert_unclaim_signature (TMH_db->cls, + hc->instance->settings.id, + &hc->instance->merchant_pub, + order_id, + &nonce, + &h_contract, + &nsig); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "insert_unclaim_signature"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "insert_unclaim_signature"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND, + order_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; /* Good! return signature (below) */ + } + + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_post-orders-ORDER_ID-unclaim.c */ diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + (C) 2026 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/> +*/ +/** + * @file taler-merchant-httpd_post-orders-ORDER_ID-unclaim.h + * @brief headers for POST /orders/$ID/unclaim handler + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_POST_ORDERS_ID_UNCLAIM_H +#define TALER_MERCHANT_HTTPD_POST_ORDERS_ID_UNCLAIM_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + +/** + * Manage a POST /orders/$ID/unclaim request. Allows the client to + * unclaim an order. + * + * @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_post_orders_ID_unclaim (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-accounts.c b/src/backend/taler-merchant-httpd_post-private-accounts.c @@ -0,0 +1,470 @@ +/* + This file is part of TALER + (C) 2020-2024 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-httpd_post-private-accounts.c + * @brief implementing POST /private/accounts request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-accounts.h" +#include "taler-merchant-httpd_helper.h" +#include "taler/taler_merchant_bank_lib.h" +#include <taler/taler_dbevents.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_mfa.h" +#include <regex.h> + +/** + * Maximum number of retries we do on serialization failures. + */ +#define MAX_RETRIES 5 + +/** + * Closure for account_cb(). + */ +struct PostAccountContext +{ + /** + * Payto URI of the account to add (from the request). + */ + struct TALER_FullPayto uri; + + /** + * Hash of the wire details (@e uri and @e salt). + * Set if @e have_same_account is true. + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Salt value used for hashing @e uri. + * Set if @e have_same_account is true. + */ + struct TALER_WireSaltP salt; + + /** + * Credit facade URL from the request. + */ + const char *credit_facade_url; + + /** + * Facade credentials from the request. + */ + const json_t *credit_facade_credentials; + + /** + * Wire subject metadata from the request. + */ + const char *extra_wire_subject_metadata; + + /** + * True if we have ANY account already and thus require MFA. + */ + bool have_any_account; + + /** + * True if we have exact match already and thus require MFA. + */ + bool have_same_account; + + /** + * True if we have an account with the same normalized payto + * already and thus the client can only do PATCH but not POST. + */ + bool have_conflicting_account; +}; + + +/** + * Callback invoked with information about a bank account. + * + * @param cls closure with a `struct PostAccountContext` + * @param merchant_priv private key of the merchant instance + * @param ad details about the account + */ +static void +account_cb ( + void *cls, + const struct TALER_MerchantPrivateKeyP *merchant_priv, + const struct TALER_MERCHANTDB_AccountDetails *ad) +{ + struct PostAccountContext *pac = cls; + + if (! ad->active) + return; + pac->have_any_account = true; + if ( (0 == TALER_full_payto_cmp (pac->uri, + ad->payto_uri) ) && + ( (pac->credit_facade_credentials == + ad->credit_facade_credentials) || + ( (NULL != pac->credit_facade_credentials) && + (NULL != ad->credit_facade_credentials) && + (1 == json_equal (pac->credit_facade_credentials, + ad->credit_facade_credentials)) ) ) && + ( (pac->extra_wire_subject_metadata == + ad->extra_wire_subject_metadata) || + ( (NULL != pac->extra_wire_subject_metadata) && + (NULL != ad->extra_wire_subject_metadata) && + (0 == strcmp (pac->extra_wire_subject_metadata, + ad->extra_wire_subject_metadata)) ) ) && + ( (pac->credit_facade_url == ad->credit_facade_url) || + ( (NULL != pac->credit_facade_url) && + (NULL != ad->credit_facade_url) && + (0 == strcmp (pac->credit_facade_url, + ad->credit_facade_url)) ) ) ) + { + pac->have_same_account = true; + pac->salt = ad->salt; + pac->h_wire = ad->h_wire; + return; + } + + if (0 == TALER_full_payto_normalize_and_cmp (pac->uri, + ad->payto_uri) ) + { + pac->have_conflicting_account = true; + return; + } +} + + +MHD_RESULT +TMH_private_post_account (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct PostAccountContext pac = { 0 }; + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_full_payto_uri ("payto_uri", + &pac.uri), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("credit_facade_url", + &pac.credit_facade_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("extra_wire_subject_metadata", + &pac.extra_wire_subject_metadata), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("credit_facade_credentials", + &pac.credit_facade_credentials), + NULL), + GNUNET_JSON_spec_end () + }; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + ispec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + { + char *err; + + if (NULL != + (err = TALER_payto_validate (pac.uri))) + { + MHD_RESULT mret; + + GNUNET_break_op (0); + mret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + err); + GNUNET_free (err); + return mret; + } + } + if (! TALER_is_valid_subject_metadata_string ( + pac.extra_wire_subject_metadata)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "extra_wire_subject_metadata"); + } + + { + char *apt = GNUNET_strdup (TMH_allowed_payment_targets); + char *method = TALER_payto_get_method (pac.uri.full_payto); + bool ok; + + ok = false; + for (const char *tok = strtok (apt, + " "); + NULL != tok; + tok = strtok (NULL, + " ")) + { + if (0 == strcmp ("*", + tok)) + ok = true; + if (0 == strcmp (method, + tok)) + ok = true; + if (ok) + break; + } + GNUNET_free (method); + GNUNET_free (apt); + if (! ok) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + "The payment target type is forbidden by policy"); + } + } + + if ( (NULL != TMH_payment_target_regex) && + (0 != + regexec (&TMH_payment_target_re, + pac.uri.full_payto, + 0, + NULL, + 0)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + "The specific account is forbidden by policy"); + } + + if ( (NULL == pac.credit_facade_url) != + (NULL == pac.credit_facade_credentials) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + (NULL == pac.credit_facade_url) + ? "credit_facade_url" + : "credit_facade_credentials"); + } + if ( (NULL != pac.credit_facade_url) || + (NULL != pac.credit_facade_credentials) ) + { + struct TALER_MERCHANT_BANK_AuthenticationData auth; + + if (GNUNET_OK != + TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials, + pac.credit_facade_url, + &auth)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "credit_facade_url or credit_facade_credentials"); + } + TALER_MERCHANT_BANK_auth_free (&auth); + } + + TMH_db->preflight (TMH_db->cls); + for (unsigned int retries = 0; + retries < MAX_RETRIES; + retries++) + { + enum GNUNET_DB_QueryStatus qs; + struct TMH_WireMethod *wm; + + TMH_db->rollback (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "post-account")) + { + GNUNET_break (0); + break; + } + qs = TMH_db->select_accounts (TMH_db->cls, + mi->settings.id, + &account_cb, + &pac); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_accounts"); + case GNUNET_DB_STATUS_SOFT_ERROR: + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if (pac.have_same_account) + { + /* Idempotent request */ + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ( + "salt", + &pac.salt), + GNUNET_JSON_pack_data_auto ( + "h_wire", + &pac.h_wire)); + + } + + if (pac.have_conflicting_account) + { + /* Conflict, refuse request */ + TMH_db->rollback (TMH_db->cls); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, + pac.uri.full_payto); + } + + if (pac.have_any_account) + { + /* MFA needed */ + enum GNUNET_GenericReturnValue ret; + + ret = TMH_mfa_check_simple (hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + mi); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Account creation MFA check returned %d\n", + (int) ret); + if (GNUNET_OK != ret) + { + TMH_db->rollback (TMH_db->cls); + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + /* convert provided payto URI into internal data structure with salts */ + wm = TMH_setup_wire_account (pac.uri, + pac.credit_facade_url, + pac.credit_facade_credentials); + GNUNET_assert (NULL != wm); + { + struct TALER_MERCHANTDB_AccountDetails ad = { + .payto_uri = wm->payto_uri, + .salt = wm->wire_salt, + .instance_id = mi->settings.id, + .h_wire = wm->h_wire, + .credit_facade_url = wm->credit_facade_url, + .credit_facade_credentials = wm->credit_facade_credentials, + .extra_wire_subject_metadata = (char *) pac.extra_wire_subject_metadata, + .active = wm->active + }; + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + }; + + qs = TMH_db->insert_account (TMH_db->cls, + &ad); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + TMH_wire_method_free (wm); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "insert_account"); + case GNUNET_DB_STATUS_SOFT_ERROR: + continue; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + TMH_wire_method_free (wm); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_account"); + } + + TMH_db->event_notify (TMH_db->cls, + &es, + NULL, + 0); + qs = TMH_db->commit (TMH_db->cls); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_wire_method_free (wm); + continue; + case GNUNET_DB_STATUS_HARD_ERROR: + TMH_wire_method_free (wm); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "post-account"); + } + /* Finally, also update our running process */ + GNUNET_CONTAINER_DLL_insert (mi->wm_head, + mi->wm_tail, + wm); + /* Note: we may not need to do this, as we notified + about the account change above. But also hardly hurts. */ + TMH_reload_instances (mi->settings.id); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_data_auto ("salt", + &wm-> + wire_salt + ), + GNUNET_JSON_pack_data_auto ("h_wire", + &wm->h_wire)); + } /* end retries */ + TMH_db->rollback (TMH_db->cls); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "post-accounts"); +} + + +/* end of taler-merchant-httpd_post-private-accounts.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-accounts.h b/src/backend/taler-merchant-httpd_post-private-accounts.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + (C) 2020-2023 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-httpd_post-private-accounts.h + * @brief implementing POST /private/account request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_ACCOUNT_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_ACCOUNT_H + +#include "taler-merchant-httpd.h" + + +/** + * Add bank account to an instance. + * + * @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_account (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-categories.c b/src/backend/taler-merchant-httpd_post-private-categories.c @@ -0,0 +1,170 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_post-private-categories.c + * @brief implementing POST /private/categories request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-categories.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +MHD_RESULT +TMH_private_post_categories (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *category_name; + const json_t *category_name_i18n; + uint64_t category_id; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &category_name), + GNUNET_JSON_spec_object_const ("name_i18n", + &category_name_i18n), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != mi); + { + 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; + } + } + + /* finally, interact with DB until no serialization error */ + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + json_t *xcategory_name_i18n; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "POST /categories")) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->select_category_by_name (TMH_db->cls, + mi->settings.id, + category_name, + &xcategory_name_i18n, + &category_id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + /* restart transaction */ + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Good, we can proceed! */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* idempotency check: is etp == tp? */ + { + bool eq; + + eq = (1 == json_equal (xcategory_name_i18n, + category_name_i18n)); + json_decref (xcategory_name_i18n); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return eq + ? TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("category_id", + category_id)) + : TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_CATEGORIES_CONFLICT_CATEGORY_EXISTS, + category_name); + } + } /* end switch (qs) */ + + qs = TMH_db->insert_category (TMH_db->cls, + mi->settings.id, + category_name, + category_name_i18n, + &category_id); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + TMH_db->rollback (TMH_db->cls); + break; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } +retry: + GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); + TMH_db->rollback (TMH_db->cls); + } /* for RETRIES loop */ + GNUNET_JSON_parse_free (spec); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + (GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? TALER_EC_GENERIC_DB_SOFT_FAILURE + : TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("category_id", + category_id)); +} + + +/* end of taler-merchant-httpd_post-private-categories.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-categories.h b/src/backend/taler-merchant-httpd_post-private-categories.h @@ -0,0 +1,45 @@ +/* + This file is part of TALER + (C) 2024 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-httpd_post-private-categories.h + * @brief implementing POST /categories request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_CATEGORIES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_CATEGORIES_H + +#include "taler-merchant-httpd.h" + + +/** + * Generate a product category. + * + * @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_categories ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-donau.c b/src/backend/taler-merchant-httpd_post-private-donau.c @@ -0,0 +1,352 @@ +/* + This file is part of TALER + Copyright (C) 2024, 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-httpd_post-private-donau.c + * @brief implementation of POST /donau + * @author Bohdan Potuzhnyi + * @author Vlada Svirsh + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include "donau/donau_service.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include "taler/taler_merchant_service.h" +#include "taler-merchant-httpd_post-private-donau.h" + +/** + * Context for the POST /donau request handler. + */ +struct PostDonauCtx +{ + /** + * Stored in a DLL. + */ + struct PostDonauCtx *next; + + /** + * Stored in a DLL. + */ + struct PostDonauCtx *prev; + + /** + * Connection to the MHD server + */ + struct MHD_Connection *connection; + + /** + * Context of the request handler. + */ + struct TMH_HandlerContext *hc; + + /** + * URL of the DONAU service + * to which the charity belongs. + */ + const char *donau_url; + + /** + * ID of the charity in the DONAU service. + */ + uint64_t charity_id; + + /** + * Handle returned by DONAU_charities_get(); needed to cancel on + * connection abort, etc. + */ + struct DONAU_CharityGetHandle *get_handle; + + /** + * Response to return. + */ + struct MHD_Response *response; + + /** + * HTTP status for @e response. + */ + unsigned int http_status; + + /** + * #GNUNET_YES if we are suspended, + * #GNUNET_NO if not, + * #GNUNET_SYSERR on shutdown + */ + enum GNUNET_GenericReturnValue suspended; +}; + + +/** + * Head of active pay context DLL. + */ +static struct PostDonauCtx *pdc_head; + +/** + * Tail of active pay context DLL. + */ +static struct PostDonauCtx *pdc_tail; + + +void +TMH_force_post_donau_resume () +{ + for (struct PostDonauCtx *pdc = pdc_head; + NULL != pdc; + pdc = pdc->next) + { + if (GNUNET_YES == pdc->suspended) + { + pdc->suspended = GNUNET_SYSERR; + MHD_resume_connection (pdc->connection); + } + } +} + + +/** + * Callback for DONAU_charities_get() to handle the response. + * + * @param cls closure with PostDonauCtx + * @param gcr response from Donau + */ +static void +donau_charity_get_cb (void *cls, + const struct DONAU_GetCharityResponse *gcr) +{ + struct PostDonauCtx *pdc = cls; + enum GNUNET_DB_QueryStatus qs; + + pdc->get_handle = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing DONAU charity get response"); + /* Anything but 200 => propagate Donau’s response. */ + if (MHD_HTTP_OK != gcr->hr.http_status) + { + pdc->http_status = MHD_HTTP_BAD_GATEWAY; + pdc->response = TALER_MHD_MAKE_JSON_PACK ( + TALER_MHD_PACK_EC (gcr->hr.ec), + GNUNET_JSON_pack_uint64 ("donau_http_status", + gcr->hr.http_status)); + pdc->suspended = GNUNET_NO; + MHD_resume_connection (pdc->connection); + TALER_MHD_daemon_trigger (); + return; + } + + if (0 != + GNUNET_memcmp (&gcr->details.ok.charity.charity_pub.eddsa_pub, + &pdc->hc->instance->merchant_pub.eddsa_pub)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Charity key at donau does not match our merchant key\n"); + pdc->http_status = MHD_HTTP_CONFLICT; + pdc->response = TALER_MHD_make_error ( + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "charity_pub != merchant_pub"); + MHD_resume_connection (pdc->connection); + TALER_MHD_daemon_trigger (); + return; + } + + qs = TMH_db->insert_donau_instance (TMH_db->cls, + pdc->donau_url, + &gcr->details.ok.charity, + pdc->charity_id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + pdc->response = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_donau_instance"); + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + pdc->response = TALER_MHD_make_error ( + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_donau_instance"); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* presumably idempotent + concurrent, no need to notify, but still respond */ + pdc->http_status = MHD_HTTP_NO_CONTENT; + pdc->response = MHD_create_response_from_buffer_static (0, + NULL); + TALER_MHD_add_global_headers (pdc->response, + false); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_DONAU_KEYS) + }; + + TMH_db->event_notify (TMH_db->cls, + &es, + pdc->donau_url, + strlen (pdc->donau_url) + 1); + pdc->http_status = MHD_HTTP_NO_CONTENT; + pdc->response = MHD_create_response_from_buffer_static (0, + NULL); + TALER_MHD_add_global_headers (pdc->response, + false); + break; + } + } + pdc->suspended = GNUNET_NO; + MHD_resume_connection (pdc->connection); + TALER_MHD_daemon_trigger (); +} + + +/** + * Cleanup function for the PostDonauCtx. + * + * @param cls closure with PostDonauCtx + */ +static void +post_donau_cleanup (void *cls) +{ + struct PostDonauCtx *pdc = cls; + + if (pdc->get_handle) + { + DONAU_charity_get_cancel (pdc->get_handle); + pdc->get_handle = NULL; + } + GNUNET_CONTAINER_DLL_remove (pdc_head, + pdc_tail, + pdc); + GNUNET_free (pdc); +} + + +/** + * Handle a POST "/donau" 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_donau_instance (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct PostDonauCtx *pdc = hc->ctx; + + if (NULL == pdc) + { + enum GNUNET_DB_QueryStatus qs; + + pdc = GNUNET_new (struct PostDonauCtx); + pdc->connection = connection; + pdc->hc = hc; + hc->ctx = pdc; + hc->cc = &post_donau_cleanup; + GNUNET_CONTAINER_DLL_insert (pdc_head, + pdc_tail, + pdc); + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("donau_url", + &pdc->donau_url), + GNUNET_JSON_spec_uint64 ("charity_id", + &pdc->charity_id), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + TALER_MHD_parse_json_data (connection, + hc->request_body, + spec)) + { + GNUNET_break_op (0); + return MHD_NO; + } + } + qs = TMH_db->check_donau_instance (TMH_db->cls, + &hc->instance->merchant_pub, + pdc->donau_url, + pdc->charity_id); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check_donau_instance"); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + pdc->http_status = MHD_HTTP_NO_CONTENT; + pdc->response = MHD_create_response_from_buffer_static (0, + NULL); + TALER_MHD_add_global_headers (pdc->response, + false); + goto respond; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* normal case, continue below */ + break; + } + + { + struct DONAU_CharityPrivateKeyP cp; + + /* Merchant private key IS our charity private key */ + cp.eddsa_priv = hc->instance->merchant_priv.eddsa_priv; + pdc->get_handle = + DONAU_charity_get (TMH_curl_ctx, + pdc->donau_url, + pdc->charity_id, + &cp, + &donau_charity_get_cb, + pdc); + } + if (NULL == pdc->get_handle) + { + GNUNET_break (0); + GNUNET_free (pdc); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "Failed to initiate Donau lookup"); + } + pdc->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + return MHD_YES; + } +respond: + if (NULL != pdc->response) + { + MHD_RESULT res; + + GNUNET_break (GNUNET_NO == pdc->suspended); + res = MHD_queue_response (pdc->connection, + pdc->http_status, + pdc->response); + MHD_destroy_response (pdc->response); + return res; + } + GNUNET_break (GNUNET_SYSERR == pdc->suspended); + return MHD_NO; +} diff --git a/src/backend/taler-merchant-httpd_post-private-donau.h b/src/backend/taler-merchant-httpd_post-private-donau.h @@ -0,0 +1,49 @@ +/* + This file is part of TALER + Copyright (C) 2024 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-httpd_post-private-donau.h + * @brief implementation of POST /donau + * @author Bohdan Potuzhnyi + * @author Vlada Svirsh + */ + +#ifndef TALER_MERCHANT_HTTPD_POST_DONAU_INSTANCE_H +#define TALER_MERCHANT_HTTPD_POST_DONAU_INSTANCE_H + +#include "taler-merchant-httpd.h" + + +/** + * Resume all connections suspended on Donau-interaction during shutdown. + */ +void +TMH_force_post_donau_resume (void); + + +/** + * Handle a POST "/donau" 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_donau_instance (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-groups.c b/src/backend/taler-merchant-httpd_post-private-groups.c @@ -0,0 +1,88 @@ +/* + 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 taler-merchant-httpd_post-private-groups.c + * @brief implementation of POST /private/groups + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-groups.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_post_groups (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *group_name; + const char *description; + enum GNUNET_DB_QueryStatus qs; + uint64_t group_id; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("group_name", + &group_name), + GNUNET_JSON_spec_string ("description", + &description), + 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; + } + } + + qs = TMH_db->insert_product_group (TMH_db->cls, + hc->instance->settings.id, + group_name, + description, + &group_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, + "insert_product_group"); + } + 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_PRODUCT_GROUP_CONFLICTING_NAME, + group_name); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("group_serial_id", + group_id)); +} diff --git a/src/backend/taler-merchant-httpd_post-private-groups.h b/src/backend/taler-merchant-httpd_post-private-groups.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 taler-merchant-httpd_post-private-groups.h + * @brief HTTP serving layer for creating product groups + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_GROUPS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_GROUPS_H + +#include "taler-merchant-httpd.h" + +/** + * Handle POST /private/groups 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_groups (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c b/src/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c @@ -0,0 +1,467 @@ +/* + This file is part of TALER + (C) 2014-2024 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-httpd_post-private-orders-ORDER_ID-refund.c + * @brief Handle request to increase the refund for an order + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h" +#include "taler-merchant-httpd_get-private-orders.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_get-exchanges.h" + + +/** + * How often do we retry the non-trivial refund INSERT database + * transaction? + */ +#define MAX_RETRIES 5 + + +/** + * Use database to notify other clients about the + * @a order_id being refunded + * + * @param hc handler context we operate in + * @param amount the (total) refunded amount + */ +static void +trigger_refund_notification ( + struct TMH_HandlerContext *hc, + const struct TALER_Amount *amount) +{ + { + const char *as; + struct TMH_OrderRefundEventP refund_eh = { + .header.size = htons (sizeof (refund_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND), + .merchant_pub = hc->instance->merchant_pub + }; + + /* Resume clients that may wait for this refund */ + as = TALER_amount2s (amount); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Awakening clients on %s waiting for refund of no more than %s\n", + hc->infix, + as); + GNUNET_CRYPTO_hash (hc->infix, + strlen (hc->infix), + &refund_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &refund_eh.header, + as, + strlen (as)); + } + { + struct TMH_OrderPayEventP pay_eh = { + .header.size = htons (sizeof (pay_eh)), + .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), + .merchant_pub = hc->instance->merchant_pub + }; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Notifying clients about status change of order %s\n", + hc->infix); + GNUNET_CRYPTO_hash (hc->infix, + strlen (hc->infix), + &pay_eh.h_order_id); + TMH_db->event_notify (TMH_db->cls, + &pay_eh.header, + NULL, + 0); + } +} + + +/** + * Make a taler://refund URI + * + * @param connection MHD connection to take host and path from + * @param instance_id merchant's instance ID, must not be NULL + * @param order_id order ID to show a refund for, must not be NULL + * @returns the URI, must be freed with #GNUNET_free + */ +static char * +make_taler_refund_uri (struct MHD_Connection *connection, + const char *instance_id, + const char *order_id) +{ + struct GNUNET_Buffer buf; + + GNUNET_assert (NULL != instance_id); + GNUNET_assert (NULL != order_id); + if (GNUNET_OK != + TMH_taler_uri_by_connection (connection, + "refund", + instance_id, + &buf)) + { + GNUNET_break (0); + return NULL; + } + GNUNET_buffer_write_path (&buf, + order_id); + GNUNET_buffer_write_path (&buf, + ""); /* Trailing slash */ + return GNUNET_buffer_reap_str (&buf); +} + + +/** + * Wrapper around #TMH_EXCHANGES_get_limit() that + * determines the refund limit for a given @a exchange_url + * + * @param cls unused + * @param exchange_url base URL of the exchange to get + * the refund limit for + * @param[in,out] amount lowered to the maximum refund + * allowed at the exchange + */ +static void +get_refund_limit (void *cls, + const char *exchange_url, + struct TALER_Amount *amount) +{ + (void) cls; + TMH_EXCHANGES_get_limit (exchange_url, + TALER_KYCLOGIC_KYC_TRIGGER_REFUND, + amount); +} + + +/** + * Handle request for increasing the refund associated with + * a contract. + * + * @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_orders_ID_refund ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_Amount refund; + const char *reason; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("refund", + &refund), + GNUNET_JSON_spec_string ("reason", + &reason), + GNUNET_JSON_spec_end () + }; + enum TALER_MERCHANTDB_RefundStatus rs; + struct TALER_PrivateContractHashP h_contract; + json_t *contract_terms; + struct GNUNET_TIME_Timestamp timestamp; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + { + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + } + + { + enum GNUNET_DB_QueryStatus qs; + uint64_t order_serial; + struct GNUNET_TIME_Timestamp refund_deadline; + struct GNUNET_TIME_Timestamp wire_deadline; + + qs = TMH_db->lookup_contract_terms (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &contract_terms, + &order_serial, + NULL); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_contract_terms"); + } + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, + hc->infix); + } + if (GNUNET_OK != + TALER_JSON_contract_hash (contract_terms, + &h_contract)) + { + GNUNET_break (0); + json_decref (contract_terms); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "Could not hash contract terms"); + } + { + struct GNUNET_JSON_Specification cspec[] = { + GNUNET_JSON_spec_timestamp ("refund_deadline", + &refund_deadline), + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &wire_deadline), + GNUNET_JSON_spec_timestamp ("timestamp", + &timestamp), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_YES != + GNUNET_JSON_parse (contract_terms, + cspec, + NULL, NULL)) + { + GNUNET_break (0); + json_decref (contract_terms); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + "mandatory fields missing"); + } + if (GNUNET_TIME_timestamp_cmp (timestamp, + ==, + refund_deadline)) + { + /* refund was never allowed, so we should refuse hard */ + json_decref (contract_terms); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT, + NULL); + } + if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time)) + { + /* it is too late for refunds */ + /* NOTE: We MAY still be lucky that the exchange did not yet + wire the funds, so we will try to give the refund anyway */ + } + if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time)) + { + /* it is *really* too late for refunds */ + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + TALER_EC_MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE, + NULL); + } + } + } + + TMH_db->preflight (TMH_db->cls); + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "increase refund")) + { + GNUNET_break (0); + json_decref (contract_terms); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + rs = TMH_db->increase_refund (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &refund, + &get_refund_limit, + NULL, + reason); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "increase refund returned %d\n", + rs); + if (TALER_MERCHANTDB_RS_SUCCESS != rs) + TMH_db->rollback (TMH_db->cls); + if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs) + continue; + if (TALER_MERCHANTDB_RS_SUCCESS == rs) + { + enum GNUNET_DB_QueryStatus qs; + json_t *rargs; + + rargs = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("timestamp", + timestamp), + GNUNET_JSON_pack_string ("order_id", + hc->infix), + GNUNET_JSON_pack_object_incref ("contract_terms", + contract_terms), + TALER_JSON_pack_amount ("refund_amount", + &refund), + GNUNET_JSON_pack_string ("reason", + reason) + ); + GNUNET_assert (NULL != rargs); + qs = TMH_trigger_webhook ( + hc->instance->settings.id, + "refund", + rargs); + json_decref (rargs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + rs = TALER_MERCHANTDB_RS_HARD_ERROR; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + continue; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + qs = TMH_db->commit (TMH_db->cls); + break; + } + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + GNUNET_break (0); + rs = TALER_MERCHANTDB_RS_HARD_ERROR; + break; + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + continue; + trigger_refund_notification (hc, + &refund); + } + break; + } /* retries loop */ + json_decref (contract_terms); + + switch (rs) + { + case TALER_MERCHANTDB_RS_LEGAL_FAILURE: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Refund amount %s exceeded legal limits of the exchanges involved\n", + TALER_amount2s (&refund)); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, + NULL); + case TALER_MERCHANTDB_RS_BAD_CURRENCY: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Refund amount %s is not in the currency of the original payment\n", + TALER_amount2s (&refund)); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Order was paid in a different currency"); + case TALER_MERCHANTDB_RS_TOO_HIGH: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Refusing refund amount %s that is larger than original payment\n", + TALER_amount2s (&refund)); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, + "Amount above payment"); + case TALER_MERCHANTDB_RS_SOFT_ERROR: + case TALER_MERCHANTDB_RS_HARD_ERROR: + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + case TALER_MERCHANTDB_RS_NO_SUCH_ORDER: + /* We know the order exists from the + "lookup_contract_terms" at the beginning; + so if we get 'no such order' here, it + must be read as "no PAID order" */ + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID, + hc->infix); + case TALER_MERCHANTDB_RS_SUCCESS: + /* continued below */ + break; + } /* end switch */ + + { + uint64_t order_serial; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_order_summary (TMH_db->cls, + hc->instance->settings.id, + hc->infix, + &timestamp, + &order_serial); + if (0 >= qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL); + } + TMH_notify_order_change (hc->instance, + TMH_OSF_CLAIMED + | TMH_OSF_PAID + | TMH_OSF_REFUNDED, + timestamp, + order_serial); + } + { + MHD_RESULT ret; + char *taler_refund_uri; + + taler_refund_uri = make_taler_refund_uri (connection, + hc->instance->settings.id, + hc->infix); + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("taler_refund_uri", + taler_refund_uri), + GNUNET_JSON_pack_data_auto ("h_contract", + &h_contract)); + GNUNET_free (taler_refund_uri); + return ret; + } +} + + +/* end of taler-merchant-httpd_post-private-orders-ORDER_ID-refund.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h b/src/backend/taler-merchant-httpd_post-private-orders-ORDER_ID-refund.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2016, 2017, 2020 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-httpd_post-private-orders-ORDER_ID-refund.h + * @brief Handle request to increase the refund for an order + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_ID_REFUND_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_ID_REFUND_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Handle request for increasing the refund associated with + * a contract. + * + * @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_orders_ID_refund (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-orders.c b/src/backend/taler-merchant-httpd_post-private-orders.c @@ -0,0 +1,4900 @@ +/* + This file is part of TALER + (C) 2014-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-httpd_post-private-orders.c + * @brief the POST /orders handler + * @author Christian Grothoff + * @author Marcello Stanisci + * @author Christian Blättler + */ +#include "taler/platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_db_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_time_lib.h> +#include <jansson.h> +#include <microhttpd.h> +#include <string.h> +#include <taler/taler_error_codes.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_util.h> +#include <taler/taler_merchant_util.h> +#include <time.h> +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_post-private-orders.h" +#include "taler-merchant-httpd_get-exchanges.h" +#include "taler-merchant-httpd_contract.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_get-private-orders.h" + +#include "taler/taler_merchantdb_plugin.h" + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + +/** + * Maximum number of inventory products per order. + */ +#define MAX_PRODUCTS 1024 + +/** + * What is the label under which we find/place the merchant's + * jurisdiction in the locations list by default? + */ +#define STANDARD_LABEL_MERCHANT_JURISDICTION "_mj" + +/** + * What is the label under which we find/place the merchant's + * address in the locations list by default? + */ +#define STANDARD_LABEL_MERCHANT_ADDRESS "_ma" + +/** + * How long do we wait at most for /keys from the exchange(s)? + * Ensures that we do not block forever just because some exchange + * fails to respond *or* because our taler-merchant-keyscheck + * refuses a forced download. + */ +#define MAX_KEYS_WAIT \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) + +/** + * Generate the base URL for the given merchant instance. + * + * @param connection the MHD connection + * @param instance_id the merchant instance ID + * @returns the merchant instance's base URL + */ +static char * +make_merchant_base_url (struct MHD_Connection *connection, + const char *instance_id) +{ + struct GNUNET_Buffer buf; + + if (GNUNET_OK != + TMH_base_url_by_connection (connection, + instance_id, + &buf)) + return NULL; + GNUNET_buffer_write_path (&buf, + ""); + return GNUNET_buffer_reap_str (&buf); +} + + +/** + * Information about a product we are supposed to add to the order + * based on what we know it from our inventory. + */ +struct InventoryProduct +{ + /** + * Identifier of the product in the inventory. + */ + const char *product_id; + + /** + * Number of units of the product to add to the order (integer part). + */ + uint64_t quantity; + + /** + * Fractional part of the quantity in units of 1/1000000 of the base value. + */ + uint32_t quantity_frac; + + /** + * True if the integer quantity field was missing in the request. + */ + bool quantity_missing; + + /** + * String representation of the quantity, if supplied. + */ + const char *unit_quantity; + + /** + * True if the string quantity field was missing in the request. + */ + bool unit_quantity_missing; + + /** + * Money pot associated with the product. 0 for none. + */ + uint64_t product_money_pot; + +}; + + +/** + * Handle for a rekey operation where we (re)request + * the /keys from the exchange. + */ +struct RekeyExchange +{ + /** + * Kept in a DLL. + */ + struct RekeyExchange *prev; + + /** + * Kept in a DLL. + */ + struct RekeyExchange *next; + + /** + * order this is for. + */ + struct OrderContext *oc; + + /** + * Base URL of the exchange. + */ + char *url; + + /** + * Request for keys. + */ + struct TMH_EXCHANGES_KeysOperation *fo; + +}; + + +/** + * Data structure where we evaluate the viability of a given + * wire method for this order. + */ +struct WireMethodCandidate +{ + /** + * Kept in a DLL. + */ + struct WireMethodCandidate *next; + + /** + * Kept in a DLL. + */ + struct WireMethodCandidate *prev; + + /** + * The wire method we are evaluating. + */ + const struct TMH_WireMethod *wm; + + /** + * List of exchanges to use when we use this wire method. + */ + json_t *exchanges; + + /** + * Array of maximum amounts that could be paid over all available exchanges + * for this @a wm. Used to determine if this order creation requests exceeds + * legal limits. + */ + struct TALER_Amount *total_exchange_limits; + + /** + * Length of the @e total_exchange_limits array. + */ + unsigned int num_total_exchange_limits; + +}; + + +/** + * Information we keep per order we are processing. + */ +struct OrderContext +{ + /** + * Information set in the #ORDER_PHASE_PARSE_REQUEST phase. + */ + struct + { + /** + * Order field of the request + */ + json_t *order; + + /** + * Set to how long refunds will be allowed. + */ + struct GNUNET_TIME_Relative refund_delay; + + /** + * RFC8905 payment target type to find a matching merchant account + */ + const char *payment_target; + + /** + * Shared key to use with @e pos_algorithm. + */ + char *pos_key; + + /** + * Selected algorithm (by template) when we are to + * generate an OTP code for payment confirmation. + */ + enum TALER_MerchantConfirmationAlgorithm pos_algorithm; + + /** + * Hash of the POST request data, used to detect + * idempotent requests. + */ + struct TALER_MerchantPostDataHashP h_post_data; + + /** + * Length of the @e inventory_products array. + */ + unsigned int inventory_products_length; + + /** + * Specifies that some products are to be included in the + * order from the inventory. For these inventory management + * is performed (so the products must be in stock). + */ + struct InventoryProduct *inventory_products; + + /** + * Length of the @e uuids array. + */ + unsigned int uuids_length; + + /** + * array of UUIDs used to reserve products from @a inventory_products. + */ + struct GNUNET_Uuid *uuids; + + /** + * Claim token for the request. + */ + struct TALER_ClaimTokenP claim_token; + + /** + * Session ID (optional) to use for the order. + */ + const char *session_id; + + } parse_request; + + /** + * Information set in the #ORDER_PHASE_PARSE_ORDER phase. + */ + struct + { + + /** + * Our order ID. + */ + char *order_id; + + /** + * Summary of the contract. + */ + const char *summary; + + /** + * Internationalized summary. + */ + const json_t *summary_i18n; + + /** + * URL that will show that the contract was successful + * after it has been paid for. + */ + const char *fulfillment_url; + + /** + * Message shown to the customer after paying for the contract. + * Either fulfillment_url or fulfillment_message must be specified. + */ + const char *fulfillment_message; + + /** + * Map from IETF BCP 47 language tags to localized fulfillment messages. + */ + const json_t *fulfillment_message_i18n; + + /** + * Length of the @e products array. + */ + size_t products_len; + + /** + * Array of products that are being sold. + */ + struct TALER_MERCHANT_ProductSold *products; + + /** + * URL where the same contract could be ordered again (if available). + */ + const char *public_reorder_url; + + /** + * Merchant base URL. + */ + char *merchant_base_url; + + /** + * Timestamp of the order. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** + * Deadline for refunds. + */ + struct GNUNET_TIME_Timestamp refund_deadline; + + /** + * Payment deadline. + */ + struct GNUNET_TIME_Timestamp pay_deadline; + + /** + * Wire transfer deadline. + */ + struct GNUNET_TIME_Timestamp wire_deadline; + + /** + * Wire transfer round-up interval to apply. + */ + enum GNUNET_TIME_RounderInterval wire_deadline_rounder; + + /** + * Delivery date. + */ + struct GNUNET_TIME_Timestamp delivery_date; + + /** + * Delivery location. + */ + const json_t *delivery_location; + + /** + * Specifies for how long the wallet should try to get an + * automatic refund for the purchase. + */ + struct GNUNET_TIME_Relative auto_refund; + + /** + * Nonce generated by the wallet and echoed by the merchant + * in this field when the proposal is generated. + */ + const char *nonce; + + /** + * Extra data that is only interpreted by the merchant frontend. + */ + const json_t *extra; + + /** + * Minimum age required by the order. + */ + uint32_t minimum_age; + + /** + * Money pot to increment for whatever order payment amount + * is not yet assigned to a pot via the Product. + */ + uint64_t order_default_money_pot; + + /** + * Version of the contract terms. + */ + enum TALER_MERCHANT_ContractVersion version; + + /** + * Details present depending on @e version. + */ + union + { + /** + * Details only present for v0. + */ + struct + { + /** + * Gross amount value of the contract. Used to + * compute @e max_stefan_fee. + */ + struct TALER_Amount brutto; + + /** + * Tip included by the customer (part of the total amount). + */ + struct TALER_Amount tip; + + /** + * True if @e tip was not provided. + */ + bool no_tip; + + /** + * Maximum fee as given by the client request. + */ + struct TALER_Amount max_fee; + } v0; + + /** + * Details only present for v1. + */ + struct + { + /** + * Array of contract choices. Is null for v0 contracts. + */ + const json_t *choices; + } v1; + } details; + + } parse_order; + + /** + * Information set in the #ORDER_PHASE_PARSE_CHOICES phase. + */ + struct + { + /** + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. + */ + struct TALER_MERCHANT_ContractChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + /** + * Array of token families referenced in the contract. + */ + struct TALER_MERCHANT_ContractTokenFamily *token_families; + + /** + * Length of the @e token_families array. + */ + unsigned int token_families_len; + } parse_choices; + + /** + * Information set in the #ORDER_PHASE_MERGE_INVENTORY phase. + */ + struct + { + /** + * Merged array of products in the @e order. + */ + json_t *products; + } merge_inventory; + + /** + * Information set in the #ORDER_PHASE_ADD_PAYMENT_DETAILS phase. + */ + struct + { + + /** + * DLL of wire methods under evaluation. + */ + struct WireMethodCandidate *wmc_head; + + /** + * DLL of wire methods under evaluation. + */ + struct WireMethodCandidate *wmc_tail; + + /** + * Array of maximum amounts that appear in the contract choices + * per currency. + * Determines the maximum amounts that a client could pay for this + * order and which we must thus make sure is acceptable for the + * selected wire method/account if possible. + */ + struct TALER_Amount *max_choice_limits; + + /** + * Length of the @e max_choice_limits array. + */ + unsigned int num_max_choice_limits; + + /** + * Set to true if we may need an exchange. True if any amount is non-zero. + */ + bool need_exchange; + + } add_payment_details; + + /** + * Information set in the #ORDER_PHASE_SELECT_WIRE_METHOD phase. + */ + struct + { + + /** + * Array of exchanges we find acceptable for this order and wire method. + */ + json_t *exchanges; + + /** + * Wire method (and our bank account) we have selected + * to be included for this order. + */ + const struct TMH_WireMethod *wm; + + } select_wire_method; + + /** + * Information set in the #ORDER_PHASE_SET_EXCHANGES phase. + */ + struct + { + + /** + * Forced requests to /keys to update our exchange + * information. + */ + struct RekeyExchange *pending_reload_head; + + /** + * Forced requests to /keys to update our exchange + * information. + */ + struct RekeyExchange *pending_reload_tail; + + /** + * How long do we wait at most until giving up on getting keys? + */ + struct GNUNET_TIME_Absolute keys_timeout; + + /** + * Task to wake us up on @e keys_timeout. + */ + struct GNUNET_SCHEDULER_Task *wakeup_task; + + /** + * Array of reasons why a particular exchange may be + * limited or not be eligible. + */ + json_t *exchange_rejections; + + /** + * Did we previously force reloading of /keys from + * all exchanges? Set to 'true' to prevent us from + * doing it again (and again...). + */ + bool forced_reload; + + /** + * Did we find a working exchange? + */ + bool exchange_ok; + + /** + * Did we find an exchange that justifies + * reloading keys? + */ + bool promising_exchange; + + /** + * Set to true once we have attempted to load exchanges + * for the first time. + */ + bool exchanges_tried; + + /** + * Details depending on the contract version. + */ + union + { + + /** + * Details for contract v0. + */ + struct + { + /** + * Maximum fee for @e order based on STEFAN curves. + * Used to set @e max_fee if not provided as part of + * @e order. + */ + struct TALER_Amount max_stefan_fee; + + } v0; + + /** + * Details for contract v1. + */ + struct + { + /** + * Maximum fee for @e order based on STEFAN curves by + * contract choice. + * Used to set @e max_fee if not provided as part of + * @e order. + */ + struct TALER_Amount *max_stefan_fees; + + } v1; + + } details; + + } set_exchanges; + + /** + * Information set in the #ORDER_PHASE_SET_MAX_FEE phase. + */ + struct + { + + /** + * Details depending on the contract version. + */ + union + { + + /** + * Details for contract v0. + */ + struct + { + /** + * Maximum fee + */ + struct TALER_Amount max_fee; + } v0; + + /** + * Details for contract v1. + */ + struct + { + /** + * Maximum fees by contract choice. + */ + struct TALER_Amount *max_fees; + + } v1; + + } details; + } set_max_fee; + + /** + * Information set in the #ORDER_PHASE_EXECUTE_ORDER phase. + */ + struct + { + /** + * Which product (by offset) is out of stock, UINT_MAX if all were in-stock. + */ + unsigned int out_of_stock_index; + + /** + * Set to a previous claim token *if* @e idempotent + * is also true. + */ + struct TALER_ClaimTokenP token; + + /** + * Set to true if the order was idempotent and there + * was an equivalent one before. + */ + bool idempotent; + + /** + * Set to true if the order is in conflict with a + * previous order with the same order ID. + */ + bool conflict; + } execute_order; + + struct + { + /** + * Contract terms to store in the database. + */ + json_t *contract; + } serialize_order; + + /** + * Connection of the request. + */ + struct MHD_Connection *connection; + + /** + * Kept in a DLL while suspended. + */ + struct OrderContext *next; + + /** + * Kept in a DLL while suspended. + */ + struct OrderContext *prev; + + /** + * Handler context for the request. + */ + struct TMH_HandlerContext *hc; + + /** + * #GNUNET_YES if suspended. + */ + enum GNUNET_GenericReturnValue suspended; + + /** + * Current phase of setting up the order. + */ + enum + { + ORDER_PHASE_PARSE_REQUEST, + ORDER_PHASE_PARSE_ORDER, + ORDER_PHASE_PARSE_CHOICES, + ORDER_PHASE_MERGE_INVENTORY, + ORDER_PHASE_ADD_PAYMENT_DETAILS, + ORDER_PHASE_SET_EXCHANGES, + ORDER_PHASE_SELECT_WIRE_METHOD, + ORDER_PHASE_SET_MAX_FEE, + ORDER_PHASE_SERIALIZE_ORDER, + ORDER_PHASE_SALT_FORGETTABLE, + ORDER_PHASE_CHECK_CONTRACT, + ORDER_PHASE_EXECUTE_ORDER, + + /** + * Processing is done, we should return #MHD_YES. + */ + ORDER_PHASE_FINISHED_MHD_YES, + + /** + * Processing is done, we should return #MHD_NO. + */ + ORDER_PHASE_FINISHED_MHD_NO + } phase; + + +}; + + +/** + * Kept in a DLL while suspended. + */ +static struct OrderContext *oc_head; + +/** + * Kept in a DLL while suspended. + */ +static struct OrderContext *oc_tail; + + +void +TMH_force_orders_resume () +{ + struct OrderContext *oc; + + while (NULL != (oc = oc_head)) + { + GNUNET_CONTAINER_DLL_remove (oc_head, + oc_tail, + oc); + oc->suspended = GNUNET_SYSERR; + MHD_resume_connection (oc->connection); + } +} + + +/** + * Add the given @a val to the @a array. Adds the + * amount to a given entry in @a array if one with the same + * currency exists, otherwise extends the @a array. + * + * @param[in,out] array pointer to array of amounts + * @param[in,out] array_len length of @a array + * @param val amount to add + * @param cap cap for the sums to enforce, can be NULL + */ +static void +add_to_currency_vector (struct TALER_Amount **array, + unsigned int *array_len, + const struct TALER_Amount *val, + const struct TALER_Amount *cap) +{ + for (unsigned int i = 0; i<*array_len; i++) + { + struct TALER_Amount *ai = &(*array)[i]; + + if (GNUNET_OK == + TALER_amount_cmp_currency (ai, + val)) + { + enum TALER_AmountArithmeticResult aar; + + aar = TALER_amount_add (ai, + ai, + val); + /* If we have a cap, we tolerate the overflow */ + GNUNET_assert ( (aar >= 0) || + ( (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) && + (NULL != cap) ) ); + if (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) + { + *ai = *cap; + } + else if (NULL != cap) + GNUNET_assert (GNUNET_OK == + TALER_amount_min (ai, + ai, + cap)); + return; + } + } + GNUNET_array_append (*array, + *array_len, + *val); + if (NULL != cap) + { + struct TALER_Amount *ai = &(*array)[(*array_len) - 1]; + + GNUNET_assert (GNUNET_OK == + TALER_amount_min (ai, + ai, + cap)); + } +} + + +/** + * Update the phase of @a oc based on @a mret. + * + * @param[in,out] oc order to update phase for + * @param mret #MHD_NO to close with #MHD_NO + * #MHD_YES to close with #MHD_YES + */ +static void +finalize_order (struct OrderContext *oc, + MHD_RESULT mret) +{ + oc->phase = (MHD_YES == mret) + ? ORDER_PHASE_FINISHED_MHD_YES + : ORDER_PHASE_FINISHED_MHD_NO; +} + + +/** + * Update the phase of @a oc based on @a ret. + * + * @param[in,out] oc order to update phase for + * @param ret #GNUNET_SYSERR to close with #MHD_NO + * #GNUNET_NO to close with #MHD_YES + * #GNUNET_OK is not allowed! + */ +static void +finalize_order2 (struct OrderContext *oc, + enum GNUNET_GenericReturnValue ret) +{ + GNUNET_assert (GNUNET_OK != ret); + oc->phase = (GNUNET_NO == ret) + ? ORDER_PHASE_FINISHED_MHD_YES + : ORDER_PHASE_FINISHED_MHD_NO; +} + + +/** + * Generate an error response for @a oc. + * + * @param[in,out] oc order context to respond to + * @param http_status HTTP status code to set + * @param ec error code to set + * @param detail error message detail to set + */ +static void +reply_with_error (struct OrderContext *oc, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *detail) +{ + MHD_RESULT mret; + + mret = TALER_MHD_reply_with_error (oc->connection, + http_status, + ec, + detail); + finalize_order (oc, + mret); +} + + +/** + * Clean up memory used by @a wmc. + * + * @param[in,out] oc order context the WMC is part of + * @param[in] wmc wire method candidate to free + */ +static void +free_wmc (struct OrderContext *oc, + struct WireMethodCandidate *wmc) +{ + GNUNET_CONTAINER_DLL_remove (oc->add_payment_details.wmc_head, + oc->add_payment_details.wmc_tail, + wmc); + GNUNET_array_grow (wmc->total_exchange_limits, + wmc->num_total_exchange_limits, + 0); + json_decref (wmc->exchanges); + GNUNET_free (wmc); +} + + +/** + * Clean up memory used by @a cls. + * + * @param[in] cls the `struct OrderContext` to clean up + */ +static void +clean_order (void *cls) +{ + struct OrderContext *oc = cls; + struct RekeyExchange *rx; + + while (NULL != oc->add_payment_details.wmc_head) + free_wmc (oc, + oc->add_payment_details.wmc_head); + while (NULL != (rx = oc->set_exchanges.pending_reload_head)) + { + GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + TMH_EXCHANGES_keys4exchange_cancel (rx->fo); + GNUNET_free (rx->url); + GNUNET_free (rx); + } + GNUNET_array_grow (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + 0); + if (NULL != oc->set_exchanges.wakeup_task) + { + GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); + oc->set_exchanges.wakeup_task = NULL; + } + if (NULL != oc->select_wire_method.exchanges) + { + json_decref (oc->select_wire_method.exchanges); + oc->select_wire_method.exchanges = NULL; + } + if (NULL != oc->set_exchanges.exchange_rejections) + { + json_decref (oc->set_exchanges.exchange_rejections); + oc->set_exchanges.exchange_rejections = NULL; + } + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + GNUNET_free (oc->set_max_fee.details.v1.max_fees); + GNUNET_free (oc->set_exchanges.details.v1.max_stefan_fees); + break; + } + if (NULL != oc->merge_inventory.products) + { + json_decref (oc->merge_inventory.products); + oc->merge_inventory.products = NULL; + } + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + TALER_MERCHANT_contract_choice_free (&oc->parse_choices.choices[i]); + } + GNUNET_array_grow (oc->parse_choices.choices, + oc->parse_choices.choices_len, + 0); + for (size_t i = 0; i<oc->parse_order.products_len; i++) + { + TALER_MERCHANT_product_sold_free (&oc->parse_order.products[i]); + } + GNUNET_free (oc->parse_order.products); + oc->parse_order.products_len = 0; + for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) + { + struct TALER_MERCHANT_ContractTokenFamily *mctf + = &oc->parse_choices.token_families[i]; + + GNUNET_free (mctf->slug); + GNUNET_free (mctf->name); + GNUNET_free (mctf->description); + json_decref (mctf->description_i18n); + switch (mctf->kind) + { + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: + for (size_t j = 0; j<mctf->details.subscription.trusted_domains_len; j++) + GNUNET_free (mctf->details.subscription.trusted_domains[j]); + GNUNET_free (mctf->details.subscription.trusted_domains); + break; + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: + for (size_t j = 0; j<mctf->details.discount.expected_domains_len; j++) + GNUNET_free (mctf->details.discount.expected_domains[j]); + GNUNET_free (mctf->details.discount.expected_domains); + break; + } + for (unsigned int j = 0; j<mctf->keys_len; j++) + { + GNUNET_CRYPTO_blind_sign_pub_decref (mctf->keys[j].pub.public_key); + } + GNUNET_array_grow (mctf->keys, + mctf->keys_len, + 0); + } + GNUNET_array_grow (oc->parse_choices.token_families, + oc->parse_choices.token_families_len, + 0); + GNUNET_array_grow (oc->parse_request.inventory_products, + oc->parse_request.inventory_products_length, + 0); + GNUNET_array_grow (oc->parse_request.uuids, + oc->parse_request.uuids_length, + 0); + GNUNET_free (oc->parse_request.pos_key); + json_decref (oc->parse_request.order); + json_decref (oc->serialize_order.contract); + GNUNET_free (oc->parse_order.order_id); + GNUNET_free (oc->parse_order.merchant_base_url); + GNUNET_free (oc); +} + + +/* ***************** ORDER_PHASE_EXECUTE_ORDER **************** */ + +/** + * Compute remaining stock (integer and fractional parts) for a product. + * + * @param pd product details with current totals/sold/lost + * @param[out] available_value remaining whole units (normalized, non-negative) + * @param[out] available_frac remaining fractional units (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) + */ +static void +compute_available_quantity (const struct TALER_MERCHANTDB_ProductDetails *pd, + uint64_t *available_value, + uint32_t *available_frac) +{ + int64_t value; + int64_t frac; + + GNUNET_assert (NULL != available_value); + GNUNET_assert (NULL != available_frac); + + if ( (INT64_MAX == pd->total_stock) && + (INT32_MAX == pd->total_stock_frac) ) + { + *available_value = pd->total_stock; + *available_frac = pd->total_stock_frac; + return; + } + + value = (int64_t) pd->total_stock + - (int64_t) pd->total_sold + - (int64_t) pd->total_lost; + frac = (int64_t) pd->total_stock_frac + - (int64_t) pd->total_sold_frac + - (int64_t) pd->total_lost_frac; + + if (frac < 0) + { + int64_t borrow = ((-frac) + TALER_MERCHANT_UNIT_FRAC_BASE - 1) + / TALER_MERCHANT_UNIT_FRAC_BASE; + value -= borrow; + frac += borrow * (int64_t) TALER_MERCHANT_UNIT_FRAC_BASE; + } + else if (frac >= TALER_MERCHANT_UNIT_FRAC_BASE) + { + int64_t carry = frac / TALER_MERCHANT_UNIT_FRAC_BASE; + value += carry; + frac -= carry * (int64_t) TALER_MERCHANT_UNIT_FRAC_BASE; + } + + if (value < 0) + { + value = 0; + frac = 0; + } + + *available_value = (uint64_t) value; + *available_frac = (uint32_t) frac; +} + + +/** + * Execute the database transaction to setup the order. + * + * @param[in,out] oc order context + * @return transaction status, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if @a uuids were insufficient to reserve required inventory + */ +static enum GNUNET_DB_QueryStatus +execute_transaction (struct OrderContext *oc) +{ + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp timestamp; + uint64_t order_serial; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "insert_order")) + { + GNUNET_break (0); + return GNUNET_DB_STATUS_HARD_ERROR; + } + + /* Test if we already have an order with this id */ + { + json_t *contract_terms; + struct TALER_MerchantPostDataHashP orig_post; + + qs = TMH_db->lookup_order (TMH_db->cls, + oc->hc->instance->settings.id, + oc->parse_order.order_id, + &oc->execute_order.token, + &orig_post, + &contract_terms); + /* If yes, check for idempotency */ + if (0 > qs) + { + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + TMH_db->rollback (TMH_db->cls); + json_decref (contract_terms); + /* Comparing the contract terms is sufficient because all the other + params get added to it at some point. */ + if (0 == GNUNET_memcmp (&orig_post, + &oc->parse_request.h_post_data)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation idempotent\n"); + oc->execute_order.idempotent = true; + return qs; + } + GNUNET_break_op (0); + oc->execute_order.conflict = true; + return qs; + } + } + + /* Setup order */ + qs = TMH_db->insert_order (TMH_db->cls, + oc->hc->instance->settings.id, + oc->parse_order.order_id, + oc->parse_request.session_id, + &oc->parse_request.h_post_data, + oc->parse_order.pay_deadline, + &oc->parse_request.claim_token, + oc->serialize_order.contract, /* called 'contract terms' at database. */ + oc->parse_request.pos_key, + oc->parse_request.pos_algorithm); + if (qs <= 0) + { + /* qs == 0: probably instance does not exist (anymore) */ + TMH_db->rollback (TMH_db->cls); + return qs; + } + /* Migrate locks from UUIDs to new order: first release old locks */ + for (unsigned int i = 0; i<oc->parse_request.uuids_length; i++) + { + qs = TMH_db->unlock_inventory (TMH_db->cls, + &oc->parse_request.uuids[i]); + if (qs < 0) + { + TMH_db->rollback (TMH_db->cls); + return qs; + } + /* qs == 0 is OK here, that just means we did not HAVE any lock under this + UUID */ + } + /* Migrate locks from UUIDs to new order: acquire new locks + (note: this can basically ONLY fail on serializability OR + because the UUID locks were insufficient for the desired + quantities). */ + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) + { + qs = TMH_db->insert_order_lock ( + TMH_db->cls, + oc->hc->instance->settings.id, + oc->parse_order.order_id, + oc->parse_request.inventory_products[i].product_id, + oc->parse_request.inventory_products[i].quantity, + oc->parse_request.inventory_products[i].quantity_frac); + if (qs < 0) + { + TMH_db->rollback (TMH_db->cls); + return qs; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* qs == 0: lock acquisition failed due to insufficient stocks */ + TMH_db->rollback (TMH_db->cls); + oc->execute_order.out_of_stock_index = i; /* indicate which product is causing the issue */ + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; + } + } + oc->execute_order.out_of_stock_index = UINT_MAX; + + /* Get the order serial and timestamp for the order we just created to + update long-poll clients. */ + qs = TMH_db->lookup_order_summary (TMH_db->cls, + oc->hc->instance->settings.id, + oc->parse_order.order_id, + &timestamp, + &order_serial); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) + { + TMH_db->rollback (TMH_db->cls); + return qs; + } + + { + json_t *jhook; + + jhook = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("order_id", + oc->parse_order.order_id), + GNUNET_JSON_pack_object_incref ("contract", + oc->serialize_order.contract), + GNUNET_JSON_pack_string ("instance_id", + oc->hc->instance->settings.id) + ); + GNUNET_assert (NULL != jhook); + qs = TMH_trigger_webhook (oc->hc->instance->settings.id, + "order_created", + jhook); + json_decref (jhook); + if (0 > qs) + { + TMH_db->rollback (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + return qs; + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "failed to trigger webhooks"); + return qs; + } + } + + TMH_notify_order_change (oc->hc->instance, + TMH_OSF_NONE, + timestamp, + order_serial); + /* finally, commit transaction (note: if it fails, we ALSO re-acquire + the UUID locks, which is exactly what we want) */ + qs = TMH_db->commit (TMH_db->cls); + if (0 > qs) + return qs; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* 1 == success! */ +} + + +/** + * The request was successful, generate the #MHD_HTTP_OK response. + * + * @param[in,out] oc context to update + * @param claim_token claim token to use, NULL if none + */ +static void +yield_success_response (struct OrderContext *oc, + const struct TALER_ClaimTokenP *claim_token) +{ + MHD_RESULT ret; + + ret = TALER_MHD_REPLY_JSON_PACK ( + oc->connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("order_id", + oc->parse_order.order_id), + GNUNET_JSON_pack_timestamp ("pay_deadline", + oc->parse_order.pay_deadline), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ( + "token", + claim_token))); + finalize_order (oc, + ret); +} + + +/** + * Transform an order into a proposal and store it in the + * database. Write the resulting proposal or an error message + * of a MHD connection. + * + * @param[in,out] oc order context + */ +static void +phase_execute_order (struct OrderContext *oc) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Executing database transaction to create order '%s' for instance '%s'\n", + oc->parse_order.order_id, + settings->id); + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + TMH_db->preflight (TMH_db->cls); + qs = execute_transaction (oc); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } + if (0 >= qs) + { + /* Special report if retries insufficient */ + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + { + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* should be: contract (!) with same order ID + already exists */ + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, + oc->parse_order.order_id); + return; + } + /* Other hard transaction error (disk full, etc.) */ + GNUNET_break (0); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + return; + } + + /* DB transaction succeeded, check for idempotent */ + if (oc->execute_order.idempotent) + { + yield_success_response (oc, + GNUNET_is_zero (&oc->execute_order.token) + ? NULL + : &oc->execute_order.token); + return; + } + if (oc->execute_order.conflict) + { + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, + oc->parse_order.order_id); + return; + } + + /* DB transaction succeeded, check for out-of-stock */ + if (oc->execute_order.out_of_stock_index < UINT_MAX) + { + /* We had a product that has insufficient quantities, + generate the details for the response. */ + struct TALER_MERCHANTDB_ProductDetails pd; + MHD_RESULT ret; + const struct InventoryProduct *ip; + size_t num_categories = 0; + uint64_t *categories = NULL; + uint64_t available_quantity; + uint32_t available_quantity_frac; + char requested_quantity_buf[64]; + char available_quantity_buf[64]; + + ip = &oc->parse_request.inventory_products[ + oc->execute_order.out_of_stock_index]; + memset (&pd, + 0, + sizeof (pd)); + qs = TMH_db->lookup_product ( + TMH_db->cls, + oc->hc->instance->settings.id, + ip->product_id, + &pd, + &num_categories, + &categories); + switch (qs) + { + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_free (categories); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation failed: product out of stock\n"); + + compute_available_quantity (&pd, + &available_quantity, + &available_quantity_frac); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_QUANTITY, + ip->quantity, + ip->quantity_frac, + sizeof (requested_quantity_buf), + requested_quantity_buf); + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_QUANTITY, + available_quantity, + available_quantity_frac, + sizeof (available_quantity_buf), + available_quantity_buf); + ret = TALER_MHD_REPLY_JSON_PACK ( + oc->connection, + MHD_HTTP_GONE, + GNUNET_JSON_pack_string ( + "product_id", + ip->product_id), + GNUNET_JSON_pack_uint64 ( + "requested_quantity", + ip->quantity), + GNUNET_JSON_pack_string ( + "unit_requested_quantity", + requested_quantity_buf), + GNUNET_JSON_pack_uint64 ( + "available_quantity", + available_quantity), + GNUNET_JSON_pack_string ( + "unit_available_quantity", + available_quantity_buf), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ( + "restock_expected", + pd.next_restock))); + TALER_MERCHANTDB_product_details_free (&pd); + finalize_order (oc, + ret); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation failed: unknown product out of stock\n"); + finalize_order (oc, + TALER_MHD_REPLY_JSON_PACK ( + oc->connection, + MHD_HTTP_GONE, + GNUNET_JSON_pack_string ( + "product_id", + ip->product_id), + GNUNET_JSON_pack_uint64 ( + "requested_quantity", + ip->quantity), + GNUNET_JSON_pack_uint64 ( + "available_quantity", + 0))); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + return; + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + return; + } + GNUNET_break (0); + oc->phase = ORDER_PHASE_FINISHED_MHD_NO; + return; + } /* end 'out of stock' case */ + + /* Everything in-stock, generate positive response */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order creation succeeded\n"); + yield_success_response (oc, + GNUNET_is_zero (&oc->parse_request.claim_token) + ? NULL + : &oc->parse_request.claim_token); +} + + +/* ***************** ORDER_PHASE_CHECK_CONTRACT **************** */ + + +/** + * Check that the contract is now well-formed. Upon success, continue + * processing with execute_order(). + * + * @param[in,out] oc order context + */ +static void +phase_check_contract (struct OrderContext *oc) +{ + struct TALER_PrivateContractHashP h_control; + + switch (TALER_JSON_contract_hash (oc->serialize_order.contract, + &h_control)) + { + case GNUNET_SYSERR: + GNUNET_break (0); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "could not compute hash of serialized order"); + return; + case GNUNET_NO: + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "order contained unallowed values"); + return; + case GNUNET_OK: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Contract hash is %s\n", + GNUNET_h2s (&h_control.hash)); + oc->phase++; + return; + } + GNUNET_assert (0); +} + + +/* ***************** ORDER_PHASE_SALT_FORGETTABLE **************** */ + + +/** + * Modify the final contract terms adding salts for + * items that are forgettable. + * + * @param[in,out] oc order context + */ +static void +phase_salt_forgettable (struct OrderContext *oc) +{ + if (GNUNET_OK != + TALER_JSON_contract_seed_forgettable (oc->parse_request.order, + oc->serialize_order.contract)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_JSON_INVALID, + "could not compute hash of order due to bogus forgettable fields"); + return; + } + oc->phase++; +} + + +/* ***************** ORDER_PHASE_SERIALIZE_ORDER **************** */ + +/** + * Get rounded time interval. @a start is calculated by rounding + * @a ts down to the nearest multiple of @a precision. + * + * @param precision rounding precision. + * year, month, day, hour, minute are supported. + * @param ts timestamp to round + * @param[out] start start of the interval + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +get_rounded_time_interval_down (struct GNUNET_TIME_Relative precision, + struct GNUNET_TIME_Timestamp ts, + struct GNUNET_TIME_Timestamp *start) +{ + enum GNUNET_TIME_RounderInterval ri; + + ri = GNUNET_TIME_relative_to_round_interval (precision); + if ( (GNUNET_TIME_RI_NONE == ri) && + (! GNUNET_TIME_relative_is_zero (precision)) ) + { + *start = ts; + return GNUNET_SYSERR; + } + *start = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_round_down (ts.abs_time, + ri)); + return GNUNET_OK; +} + + +/** + * Get rounded time interval. @a start is calculated by rounding + * @a ts up to the nearest multiple of @a precision. + * + * @param precision rounding precision. + * year, month, day, hour, minute are supported. + * @param ts timestamp to round + * @param[out] start start of the interval + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +get_rounded_time_interval_up (struct GNUNET_TIME_Relative precision, + struct GNUNET_TIME_Timestamp ts, + struct GNUNET_TIME_Timestamp *start) +{ + enum GNUNET_TIME_RounderInterval ri; + + ri = GNUNET_TIME_relative_to_round_interval (precision); + if ( (GNUNET_TIME_RI_NONE == ri) && + (! GNUNET_TIME_relative_is_zero (precision)) ) + { + *start = ts; + return GNUNET_SYSERR; + } + *start = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_round_up (ts.abs_time, + ri)); + return GNUNET_OK; +} + + +/** + * Find the family entry for the family of the given @a slug + * in @a oc. + * + * @param[in] oc order context to search + * @param slug slug to search for + * @return NULL if @a slug was not found + */ +static struct TALER_MERCHANT_ContractTokenFamily * +find_family (const struct OrderContext *oc, + const char *slug) +{ + for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) + { + if (0 == strcmp (oc->parse_choices.token_families[i].slug, + slug)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Token family %s already in order\n", + slug); + return &oc->parse_choices.token_families[i]; + } + } + return NULL; +} + + +/** + * Function called with each applicable family key that should + * be added to the respective token family of the order. + * + * @param cls a `struct OrderContext *` to expand + * @param tfkd token family key details to add to the contract + */ +static void +add_family_key (void *cls, + const struct TALER_MERCHANTDB_TokenFamilyKeyDetails *tfkd) +{ + struct OrderContext *oc = cls; + const struct TALER_MERCHANTDB_TokenFamilyDetails *tf = &tfkd->token_family; + struct TALER_MERCHANT_ContractTokenFamily *family; + + family = find_family (oc, + tf->slug); + if (NULL == family) + { + /* Family not yet in our contract terms, create new entry */ + struct TALER_MERCHANT_ContractTokenFamily new_family = { + .slug = GNUNET_strdup (tf->slug), + .name = GNUNET_strdup (tf->name), + .description = GNUNET_strdup (tf->description), + .description_i18n = json_incref (tf->description_i18n), + }; + + switch (tf->kind) + { + case TALER_MERCHANTDB_TFK_Subscription: + { + json_t *tdomains = json_object_get (tf->extra_data, + "trusted_domains"); + json_t *dom; + size_t i; + + new_family.kind = TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION; + new_family.critical = true; + new_family.details.subscription.trusted_domains_len + = json_array_size (tdomains); + GNUNET_assert (new_family.details.subscription.trusted_domains_len + < UINT_MAX); + new_family.details.subscription.trusted_domains + = GNUNET_new_array ( + new_family.details.subscription.trusted_domains_len, + char *); + json_array_foreach (tdomains, i, dom) + { + const char *val; + + val = json_string_value (dom); + GNUNET_break (NULL != val); + if (NULL != val) + new_family.details.subscription.trusted_domains[i] + = GNUNET_strdup (val); + } + break; + } + case TALER_MERCHANTDB_TFK_Discount: + { + json_t *edomains = json_object_get (tf->extra_data, + "expected_domains"); + json_t *dom; + size_t i; + + new_family.kind = TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT; + new_family.critical = false; + new_family.details.discount.expected_domains_len + = json_array_size (edomains); + GNUNET_assert (new_family.details.discount.expected_domains_len + < UINT_MAX); + new_family.details.discount.expected_domains + = GNUNET_new_array ( + new_family.details.discount.expected_domains_len, + char *); + json_array_foreach (edomains, i, dom) + { + const char *val; + + val = json_string_value (dom); + GNUNET_break (NULL != val); + if (NULL != val) + new_family.details.discount.expected_domains[i] + = GNUNET_strdup (val); + } + break; + } + } + GNUNET_array_append (oc->parse_choices.token_families, + oc->parse_choices.token_families_len, + new_family); + family = &oc->parse_choices.token_families[ + oc->parse_choices.token_families_len - 1]; + } + if (NULL == tfkd->pub.public_key) + return; + for (unsigned int i = 0; i<family->keys_len; i++) + { + if (TALER_token_issue_pub_cmp (&family->keys[i].pub, + &tfkd->pub)) + { + /* A matching key is already in the list. */ + return; + } + } + + { + struct TALER_MERCHANT_ContractTokenFamilyKey key; + + TALER_token_issue_pub_copy (&key.pub, + &tfkd->pub); + key.valid_after = tfkd->signature_validity_start; + key.valid_before = tfkd->signature_validity_end; + GNUNET_array_append (family->keys, + family->keys_len, + key); + } +} + + +/** + * Check if the token family with the given @a slug is already present in the + * list of token families for this order. If not, fetch its details and add it + * to the list. + * + * @param[in,out] oc order context + * @param slug slug of the token family + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +add_input_token_family (struct OrderContext *oc, + const char *slug) +{ + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + struct GNUNET_TIME_Timestamp end = oc->parse_order.pay_deadline; + enum GNUNET_DB_QueryStatus qs; + enum TALER_ErrorCode ec = TALER_EC_INVALID; /* make compiler happy */ + unsigned int http_status = 0; /* make compiler happy */ + + qs = TMH_db->lookup_token_family_keys (TMH_db->cls, + oc->hc->instance->settings.id, + slug, + now, + end, + &add_family_key, + oc); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Input token family slug %s unknown\n", + slug); + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return GNUNET_OK; + } + reply_with_error (oc, + http_status, + ec, + slug); + return GNUNET_SYSERR; +} + + +/** + * Find the index of a key in the @a family that is valid at + * the time @a valid_at. + * + * @param family to search + * @param valid_at time when the key must be valid + * @param[out] key_index index to initialize + * @return #GNUNET_OK if a matching key was found + */ +static enum GNUNET_GenericReturnValue +find_key_index (struct TALER_MERCHANT_ContractTokenFamily *family, + struct GNUNET_TIME_Timestamp valid_at, + unsigned int *key_index) +{ + for (unsigned int i = 0; i<family->keys_len; i++) + { + if ( (GNUNET_TIME_timestamp_cmp (family->keys[i].valid_after, + <=, + valid_at)) && + (GNUNET_TIME_timestamp_cmp (family->keys[i].valid_before, + >=, + valid_at)) ) + { + /* The token family and a matching key already exist. */ + *key_index = i; + return GNUNET_OK; + } + } + return GNUNET_NO; +} + + +/** + * Create fresh key pair based on @a cipher_spec. + * + * @param cipher_spec which kind of key pair should we generate + * @param[out] priv set to new private key + * @param[out] pub set to new public key + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +create_key (const char *cipher_spec, + struct TALER_TokenIssuePrivateKey *priv, + struct TALER_TokenIssuePublicKey *pub) +{ + unsigned int len; + char dummy; + + if (0 == strcmp ("cs", + cipher_spec)) + { + GNUNET_CRYPTO_blind_sign_keys_create ( + &priv->private_key, + &pub->public_key, + GNUNET_CRYPTO_BSA_CS); + return GNUNET_OK; + } + if (1 == + sscanf (cipher_spec, + "rsa(%u)%c", + &len, + &dummy)) + { + GNUNET_CRYPTO_blind_sign_keys_create ( + &priv->private_key, + &pub->public_key, + GNUNET_CRYPTO_BSA_RSA, + len); + return GNUNET_OK; + } + return GNUNET_SYSERR; +} + + +/** + * Check if the token family with the given @a slug is already present in the + * list of token families for this order. If not, fetch its details and add it + * to the list. Also checks if there is a public key with that expires after + * the payment deadline. If not, generates a new key pair and stores it in + * the database. + * + * @param[in,out] oc order context + * @param slug slug of the token family + * @param valid_at time when the token returned must be valid + * @param[out] key_index set to the index of the respective public + * key in the @a slug's token family keys array. + * @return #GNUNET_OK on success, #GNUNET_SYSERR on error + */ +static enum GNUNET_GenericReturnValue +add_output_token_family (struct OrderContext *oc, + const char *slug, + struct GNUNET_TIME_Timestamp valid_at, + unsigned int *key_index) +{ + struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; + struct TALER_MERCHANT_ContractTokenFamily *family; + enum GNUNET_DB_QueryStatus qs; + + family = find_family (oc, + slug); + if ( (NULL != family) && + (GNUNET_OK == + find_key_index (family, + valid_at, + key_index)) ) + return GNUNET_OK; + qs = TMH_db->lookup_token_family_key (TMH_db->cls, + oc->hc->instance->settings.id, + slug, + valid_at, + oc->parse_order.pay_deadline, + &key_details); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_token_family_key"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* Single-statement transaction shouldn't possibly cause serialization errors. + Thus treating like a hard error. */ + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "lookup_token_family_key"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Output token family slug %s unknown\n", + slug); + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + slug); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Lookup of token family %s at %llu yielded %s\n", + slug, + (unsigned long long) valid_at.abs_time.abs_value_us, + NULL == key_details.pub.public_key ? "no key" : "a key"); + + if (NULL == family) + { + add_family_key (oc, + &key_details); + family = find_family (oc, + slug); + GNUNET_assert (NULL != family); + } + /* we don't need the full family details anymore */ + GNUNET_free (key_details.token_family.slug); + GNUNET_free (key_details.token_family.name); + GNUNET_free (key_details.token_family.description); + json_decref (key_details.token_family.description_i18n); + json_decref (key_details.token_family.extra_data); + + if (NULL != key_details.pub.public_key) + { + /* lookup_token_family_key must have found a matching key, + and it must have been added. Find and use the index. */ + GNUNET_CRYPTO_blind_sign_pub_decref (key_details.pub.public_key); + GNUNET_CRYPTO_blind_sign_priv_decref (key_details.priv.private_key); + GNUNET_free (key_details.token_family.cipher_spec); + GNUNET_assert (GNUNET_OK == + find_key_index (family, + valid_at, + key_index)); + return GNUNET_OK; + } + + /* No suitable key exists, create one! */ + { + struct TALER_MERCHANT_ContractTokenFamilyKey key; + enum GNUNET_DB_QueryStatus iqs; + struct TALER_TokenIssuePrivateKey token_priv; + struct GNUNET_TIME_Timestamp key_expires; + struct GNUNET_TIME_Timestamp round_start; + + if (GNUNET_OK != + get_rounded_time_interval_down ( + key_details.token_family.validity_granularity, + valid_at, + &round_start)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unsupported validity granularity interval %s found in database for token family %s!\n", + GNUNET_TIME_relative2s ( + key_details.token_family.validity_granularity, + false), + slug); + GNUNET_free (key_details.token_family.cipher_spec); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "get_rounded_time_interval_down failed"); + return GNUNET_SYSERR; + } + if (GNUNET_TIME_relative_cmp ( + key_details.token_family.duration, + <, + GNUNET_TIME_relative_add ( + key_details.token_family.validity_granularity, + key_details.token_family.start_offset))) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Inconsistent duration %s found in database for token family %s (below validity granularity plus start_offset)!\n", + GNUNET_TIME_relative2s (key_details.token_family.duration, + false), + slug); + GNUNET_free (key_details.token_family.cipher_spec); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "duration, validty_granularity and start_offset inconsistent for token family"); + return GNUNET_SYSERR; + } + key.valid_after + = GNUNET_TIME_timestamp_max ( + GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_subtract ( + round_start.abs_time, + key_details.token_family.start_offset)), + key_details.token_family.valid_after); + key.valid_before + = GNUNET_TIME_timestamp_min ( + GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add ( + key.valid_after.abs_time, + key_details.token_family.duration)), + key_details.token_family.valid_before); + GNUNET_assert (GNUNET_OK == + get_rounded_time_interval_down ( + key_details.token_family.validity_granularity, + key.valid_before, + &key_expires)); + if (GNUNET_TIME_timestamp_cmp ( + key_expires, + ==, + round_start)) + { + /* valid_before does not actually end after the + next rounded validity period would start; + determine next rounded validity period + start point and extend valid_before to cover + the full validity period */ + GNUNET_assert ( + GNUNET_OK == + get_rounded_time_interval_up ( + key_details.token_family.validity_granularity, + key.valid_before, + &key_expires)); + /* This should basically always end up being key_expires */ + key.valid_before = GNUNET_TIME_timestamp_max (key.valid_before, + key_expires); + } + if (GNUNET_OK != + create_key (key_details.token_family.cipher_spec, + &token_priv, + &key.pub)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unsupported cipher family %s found in database for token family %s!\n", + key_details.token_family.cipher_spec, + slug); + GNUNET_free (key_details.token_family.cipher_spec); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "invalid cipher stored in local database for token family"); + return GNUNET_SYSERR; + } + GNUNET_free (key_details.token_family.cipher_spec); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Storing new key for slug %s of %s\n", + slug, + oc->hc->instance->settings.id); + iqs = TMH_db->insert_token_family_key (TMH_db->cls, + oc->hc->instance->settings.id, + slug, + &key.pub, + &token_priv, + key_expires, + key.valid_after, + key.valid_before); + GNUNET_CRYPTO_blind_sign_priv_decref (token_priv.private_key); + switch (iqs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* Single-statement transaction shouldn't possibly cause serialization errors. + Thus treating like a hard error. */ + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + *key_index = family->keys_len; + GNUNET_array_append (family->keys, + family->keys_len, + key); + } + return GNUNET_OK; +} + + +/** + * Build JSON array that represents all of the token families + * in the contract. + * + * @param[in] oc v1-style order context + * @return JSON array with token families for the contract + */ +static json_t * +output_token_families (struct OrderContext *oc) +{ + json_t *token_families = json_object (); + + GNUNET_assert (NULL != token_families); + for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) + { + const struct TALER_MERCHANT_ContractTokenFamily *family + = &oc->parse_choices.token_families[i]; + json_t *jfamily; + + jfamily = TALER_MERCHANT_json_from_token_family (family); + + GNUNET_assert (jfamily != NULL); + + GNUNET_assert (0 == + json_object_set_new (token_families, + family->slug, + jfamily)); + } + return token_families; +} + + +/** + * Build JSON array that represents all of the contract choices + * in the contract. + * + * @param[in] oc v1-style order context + * @return JSON array with token families for the contract + */ +static json_t * +output_contract_choices (struct OrderContext *oc) +{ + json_t *choices = json_array (); + + GNUNET_assert (NULL != choices); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + oc->parse_choices.choices[i].max_fee = + oc->set_max_fee.details.v1.max_fees[i]; + GNUNET_assert (0 == json_array_append_new ( + choices, + TALER_MERCHANT_json_from_contract_choice ( + &oc->parse_choices.choices[i], + false))); + } + + return choices; +} + + +/** + * Serialize order into @a oc->serialize_order.contract, + * ready to be stored in the database. Upon success, continue + * processing with check_contract(). + * + * @param[in,out] oc order context + */ +static void +phase_serialize_order (struct OrderContext *oc) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + json_t *merchant; + + merchant = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + settings->name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("website", + settings->website)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("email", + settings->email)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("logo", + settings->logo))); + GNUNET_assert (NULL != merchant); + { + json_t *loca; + + /* Handle merchant address */ + loca = settings->address; + if (NULL != loca) + { + loca = json_deep_copy (loca); + GNUNET_assert (NULL != loca); + GNUNET_assert (0 == + json_object_set_new (merchant, + "address", + loca)); + } + } + { + json_t *juri; + + /* Handle merchant jurisdiction */ + juri = settings->jurisdiction; + if (NULL != juri) + { + juri = json_deep_copy (juri); + GNUNET_assert (NULL != juri); + GNUNET_assert (0 == + json_object_set_new (merchant, + "jurisdiction", + juri)); + } + } + + oc->serialize_order.contract = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_int64 ("version", + oc->parse_order.version), + GNUNET_JSON_pack_string ("summary", + oc->parse_order.summary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ( + "summary_i18n", + (json_t *) oc->parse_order.summary_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("public_reorder_url", + oc->parse_order.public_reorder_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_message", + oc->parse_order.fulfillment_message)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ( + "fulfillment_message_i18n", + (json_t *) oc->parse_order.fulfillment_message_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + oc->parse_order.fulfillment_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_uint64 ("minimum_age", + oc->parse_order.minimum_age)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_uint64 ("default_money_pot", + oc->parse_order.order_default_money_pot)), + GNUNET_JSON_pack_array_incref ("products", + oc->merge_inventory.products), + GNUNET_JSON_pack_data_auto ("h_wire", + &oc->select_wire_method.wm->h_wire), + GNUNET_JSON_pack_string ("wire_method", + oc->select_wire_method.wm->wire_method), + GNUNET_JSON_pack_string ("order_id", + oc->parse_order.order_id), + GNUNET_JSON_pack_timestamp ("timestamp", + oc->parse_order.timestamp), + GNUNET_JSON_pack_timestamp ("pay_deadline", + oc->parse_order.pay_deadline), + GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", + oc->parse_order.wire_deadline), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("delivery_date", + oc->parse_order.delivery_date)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ( + "delivery_location", + (json_t *) oc->parse_order.delivery_location)), + GNUNET_JSON_pack_string ("merchant_base_url", + oc->parse_order.merchant_base_url), + GNUNET_JSON_pack_object_steal ("merchant", + merchant), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &oc->hc->instance->merchant_pub), + GNUNET_JSON_pack_array_incref ("exchanges", + oc->select_wire_method.exchanges), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("extra", + (json_t *) oc->parse_order.extra)) + ); + + { + json_t *xtra; + + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + xtra = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("max_fee", + &oc->set_max_fee.details.v0.max_fee), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + oc->parse_order.details.v0.no_tip + ? NULL + : &oc->parse_order.details.v0.tip)), + TALER_JSON_pack_amount ("amount", + &oc->parse_order.details.v0.brutto)); + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + { + json_t *token_families = output_token_families (oc); + json_t *choices = output_contract_choices (oc); + + if ( (NULL == token_families) || + (NULL == choices) ) + { + GNUNET_break (0); + return; + } + xtra = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("choices", + choices), + GNUNET_JSON_pack_object_steal ("token_families", + token_families)); + break; + } + default: + GNUNET_assert (0); + } + GNUNET_assert (0 == + json_object_update (oc->serialize_order.contract, + xtra)); + json_decref (xtra); + } + + + /* Pack does not work here, because it doesn't set zero-values for timestamps */ + GNUNET_assert (0 == + json_object_set_new (oc->serialize_order.contract, + "refund_deadline", + GNUNET_JSON_from_timestamp ( + oc->parse_order.refund_deadline))); + /* auto_refund should only be set if it is not 0 */ + if (! GNUNET_TIME_relative_is_zero (oc->parse_order.auto_refund)) + { + /* Pack does not work here, because it sets zero-values for relative times */ + GNUNET_assert (0 == + json_object_set_new (oc->serialize_order.contract, + "auto_refund", + GNUNET_JSON_from_time_rel ( + oc->parse_order.auto_refund))); + } + + oc->phase++; +} + + +/* ***************** ORDER_PHASE_SET_MAX_FEE **************** */ + + +/** + * Set @a max_fee in @a oc based on @a max_stefan_fee value if not overridden + * by @a client_fee. If neither is set, set the fee to zero using currency + * from @a brutto. + * + * @param[in,out] oc order context + * @param brutto brutto amount to compute fee for + * @param client_fee client-given fee override (or invalid) + * @param max_stefan_fee maximum STEFAN fee of any exchange + * @param max_fee set to the maximum stefan fee + */ +static void +compute_fee (struct OrderContext *oc, + const struct TALER_Amount *brutto, + const struct TALER_Amount *client_fee, + const struct TALER_Amount *max_stefan_fee, + struct TALER_Amount *max_fee) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings + = &oc->hc->instance->settings; + + if (GNUNET_OK == + TALER_amount_is_valid (client_fee)) + { + *max_fee = *client_fee; + return; + } + if ( (settings->use_stefan) && + (NULL != max_stefan_fee) && + (GNUNET_OK == + TALER_amount_is_valid (max_stefan_fee)) ) + { + *max_fee = *max_stefan_fee; + return; + } + GNUNET_assert ( + GNUNET_OK == + TALER_amount_set_zero (brutto->currency, + max_fee)); +} + + +/** + * Initialize "set_max_fee" in @a oc based on STEFAN value or client + * preference. Upon success, continue processing in next phase. + * + * @param[in,out] oc order context + */ +static void +phase_set_max_fee (struct OrderContext *oc) +{ + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + compute_fee (oc, + &oc->parse_order.details.v0.brutto, + &oc->parse_order.details.v0.max_fee, + &oc->set_exchanges.details.v0.max_stefan_fee, + &oc->set_max_fee.details.v0.max_fee); + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + oc->set_max_fee.details.v1.max_fees + = GNUNET_new_array (oc->parse_choices.choices_len, + struct TALER_Amount); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + compute_fee (oc, + &oc->parse_choices.choices[i].amount, + &oc->parse_choices.choices[i].max_fee, + NULL != oc->set_exchanges.details.v1.max_stefan_fees + ? &oc->set_exchanges.details.v1.max_stefan_fees[i] + : NULL, + &oc->set_max_fee.details.v1.max_fees[i]); + break; + default: + GNUNET_break (0); + break; + } + oc->phase++; +} + + +/* ***************** ORDER_PHASE_SELECT_WIRE_METHOD **************** */ + +/** + * Check that the @a brutto amount is at or below the + * limits we have for the respective wire method candidate. + * + * @param wmc wire method candidate to check + * @param brutto amount to check + * @return true if the amount is OK, false if it is too high + */ +static bool +check_limits (struct WireMethodCandidate *wmc, + const struct TALER_Amount *brutto) +{ + for (unsigned int i = 0; i<wmc->num_total_exchange_limits; i++) + { + const struct TALER_Amount *total_exchange_limit + = &wmc->total_exchange_limits[i]; + + if (GNUNET_OK != + TALER_amount_cmp_currency (brutto, + total_exchange_limit)) + continue; + if (1 != + TALER_amount_cmp (brutto, + total_exchange_limit)) + return true; + } + return false; +} + + +/** + * Phase to select a wire method that will be acceptable for the order. + * If none is "perfect" (allows all choices), might jump back to the + * previous phase to force "/keys" downloads to see if that helps. + * + * @param[in,out] oc order context + */ +static void +phase_select_wire_method (struct OrderContext *oc) +{ + const struct TALER_Amount *ea; + struct WireMethodCandidate *best = NULL; + unsigned int max_choices = 0; + unsigned int want_choices = 0; + + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + unsigned int num_choices = 0; + + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + want_choices = 1; + ea = &oc->parse_order.details.v0.brutto; + if (TALER_amount_is_zero (ea) || + check_limits (wmc, + ea)) + num_choices++; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + want_choices = oc->parse_choices.choices_len; + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + ea = &oc->parse_choices.choices[i].amount; + if (TALER_amount_is_zero (ea) || + check_limits (wmc, + ea)) + num_choices++; + } + break; + default: + GNUNET_assert (0); + } + if (num_choices > max_choices) + { + best = wmc; + max_choices = num_choices; + } + } + + if ( (want_choices > max_choices) && + (oc->set_exchanges.promising_exchange) && + (! oc->set_exchanges.forced_reload) ) + { + oc->set_exchanges.exchange_ok = false; + /* Not all choices in the contract can work with these + exchanges, try again with forcing /keys download */ + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + json_array_clear (wmc->exchanges); + GNUNET_array_grow (wmc->total_exchange_limits, + wmc->num_total_exchange_limits, + 0); + } + oc->phase = ORDER_PHASE_SET_EXCHANGES; + return; + } + + if ( (NULL == best) && + (NULL != oc->parse_request.payment_target) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create order: lacking suitable exchanges for payment target `%s'\n", + oc->parse_request.payment_target); + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, + oc->parse_request.payment_target); + return; + } + + if (NULL == best) + { + MHD_RESULT mret; + + /* We actually do not have ANY workable exchange(s) */ + mret = TALER_MHD_reply_json_steal ( + oc->connection, + GNUNET_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS), + GNUNET_JSON_pack_array_incref ( + "exchange_rejections", + oc->set_exchanges.exchange_rejections)), + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS); + finalize_order (oc, + mret); + return; + } + + if (want_choices > max_choices) + { + /* Some choices are unpayable */ + GNUNET_log ( + GNUNET_ERROR_TYPE_WARNING, + "Creating order, but some choices do not work with the selected wire method\n"); + } + if ( (0 == json_array_size (best->exchanges)) && + (oc->add_payment_details.need_exchange) ) + { + /* We did not find any reasonable exchange */ + GNUNET_log ( + GNUNET_ERROR_TYPE_WARNING, + "Creating order, but only for choices without payment\n"); + } + + oc->select_wire_method.wm + = best->wm; + oc->select_wire_method.exchanges + = json_incref (best->exchanges); + oc->phase++; +} + + +/* ***************** ORDER_PHASE_SET_EXCHANGES **************** */ + +/** + * Exchange `/keys` processing is done, resume handling + * the order. + * + * @param[in,out] oc context to resume + */ +static void +resume_with_keys (struct OrderContext *oc) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming order processing after /keys downloads\n"); + GNUNET_assert (GNUNET_YES == oc->suspended); + GNUNET_CONTAINER_DLL_remove (oc_head, + oc_tail, + oc); + oc->suspended = GNUNET_NO; + MHD_resume_connection (oc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Given a @a brutto amount for exchange with @a keys, set the + * @a stefan_fee. Note that @a stefan_fee is updated to the maximum + * of the input and the computed fee. + * + * @param[in,out] keys exchange keys + * @param brutto some brutto amount the client is to pay + * @param[in,out] stefan_fee set to STEFAN fee to be paid by the merchant + */ +static void +compute_stefan_fee (const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_Amount *brutto, + struct TALER_Amount *stefan_fee) +{ + struct TALER_Amount net; + + if (GNUNET_SYSERR != + TALER_EXCHANGE_keys_stefan_b2n (keys, + brutto, + &net)) + { + struct TALER_Amount fee; + + TALER_EXCHANGE_keys_stefan_round (keys, + &net); + if (-1 == TALER_amount_cmp (brutto, + &net)) + { + /* brutto < netto! */ + /* => after rounding, there is no real difference */ + net = *brutto; + } + GNUNET_assert (0 <= + TALER_amount_subtract (&fee, + brutto, + &net)); + if ( (GNUNET_OK != + TALER_amount_is_valid (stefan_fee)) || + (-1 == TALER_amount_cmp (stefan_fee, + &fee)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Updated STEFAN-based fee to %s\n", + TALER_amount2s (&fee)); + *stefan_fee = fee; + } + } +} + + +/** + * Update MAX STEFAN fees based on @a keys. + * + * @param[in,out] oc order context to update + * @param keys keys to derive STEFAN fees from + */ +static void +update_stefan (struct OrderContext *oc, + const struct TALER_EXCHANGE_Keys *keys) +{ + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + compute_stefan_fee (keys, + &oc->parse_order.details.v0.brutto, + &oc->set_exchanges.details.v0.max_stefan_fee); + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + oc->set_exchanges.details.v1.max_stefan_fees + = GNUNET_new_array (oc->parse_choices.choices_len, + struct TALER_Amount); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + if (0 == strcasecmp (keys->currency, + oc->parse_choices.choices[i].amount.currency)) + compute_stefan_fee (keys, + &oc->parse_choices.choices[i].amount, + &oc->set_exchanges.details.v1.max_stefan_fees[i]); + break; + default: + GNUNET_assert (0); + } +} + + +/** + * Check our KYC status at all exchanges as our current limit is + * too low and we failed to create an order. + * + * @param oc order context + * @param wmc wire method candidate to notify for + * @param exchange_url exchange to notify about + */ +static void +notify_kyc_required (const struct OrderContext *oc, + const struct WireMethodCandidate *wmc, + const char *exchange_url) +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) + }; + char *hws; + char *extra; + + hws = GNUNET_STRINGS_data_to_string_alloc ( + &wmc->wm->h_wire, + sizeof (wmc->wm->h_wire)); + + GNUNET_asprintf (&extra, + "%s %s", + hws, + exchange_url); + TMH_db->event_notify (TMH_db->cls, + &es, + extra, + strlen (extra) + 1); + GNUNET_free (extra); + GNUNET_free (hws); +} + + +/** + * Add a reason why a particular exchange was rejected to our + * response data. + * + * @param[in,out] oc order context to update + * @param exchange_url exchange this is about + * @param ec error code to set for the exchange + */ +static void +add_rejection (struct OrderContext *oc, + const char *exchange_url, + enum TALER_ErrorCode ec) +{ + if (NULL == oc->set_exchanges.exchange_rejections) + { + oc->set_exchanges.exchange_rejections = json_array (); + GNUNET_assert (NULL != oc->set_exchanges.exchange_rejections); + } + GNUNET_assert (0 == + json_array_append_new ( + oc->set_exchanges.exchange_rejections, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + TALER_JSON_pack_ec (ec)))); +} + + +/** + * Checks the limits that apply for this @a exchange and + * the @a wmc and if the exchange is acceptable at all, adds it + * to the list of exchanges for the @a wmc. + * + * @param oc context of the order + * @param exchange internal handle for the exchange + * @param exchange_url base URL of this exchange + * @param wmc wire method to evaluate this exchange for + * @return true if the exchange is acceptable for the contract + */ +static bool +get_acceptable (struct OrderContext *oc, + const struct TMH_Exchange *exchange, + const char *exchange_url, + struct WireMethodCandidate *wmc) +{ + const struct TALER_Amount *max_needed = NULL; + unsigned int priority = 42; /* make compiler happy */ + json_t *j_exchange; + enum TMH_ExchangeStatus res; + struct TALER_Amount max_amount; + + for (unsigned int i = 0; + i<oc->add_payment_details.num_max_choice_limits; + i++) + { + struct TALER_Amount *val = &oc->add_payment_details.max_choice_limits[i]; + + if (0 == strcasecmp (val->currency, + TMH_EXCHANGES_get_currency (exchange))) + { + max_needed = val; + break; + } + } + if (NULL == max_needed) + { + /* exchange currency not relevant for any of our choices, skip it */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exchange %s with currency `%s' is not applicable to this order\n", + exchange_url, + TMH_EXCHANGES_get_currency (exchange)); + add_rejection (oc, + exchange_url, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH); + return false; + } + + max_amount = *max_needed; + res = TMH_exchange_check_debit ( + oc->hc->instance->settings.id, + exchange, + wmc->wm, + &max_amount); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exchange %s evaluated at %d with max %s\n", + exchange_url, + res, + TALER_amount2s (&max_amount)); + if (TALER_amount_is_zero (&max_amount)) + { + if (! TALER_amount_is_zero (max_needed)) + { + /* Trigger re-checking the current deposit limit when + * paying non-zero amount with zero deposit limit */ + notify_kyc_required (oc, + wmc, + exchange_url); + } + /* If deposit is impossible, we don't list the + * exchange in the contract terms. */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exchange %s deposit limit is zero, skipping it\n", + exchange_url); + add_rejection (oc, + exchange_url, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED); + return false; + } + switch (res) + { + case TMH_ES_OK: + case TMH_ES_RETRY_OK: + priority = 1024; /* high */ + oc->set_exchanges.exchange_ok = true; + break; + case TMH_ES_NO_ACC: + if (oc->set_exchanges.forced_reload) + priority = 0; /* fresh negative response */ + else + priority = 512; /* stale negative response */ + break; + case TMH_ES_NO_CURR: + if (oc->set_exchanges.forced_reload) + priority = 0; /* fresh negative response */ + else + priority = 512; /* stale negative response */ + break; + case TMH_ES_NO_KEYS: + if (oc->set_exchanges.forced_reload) + priority = 256; /* fresh, no accounts yet */ + else + priority = 768; /* stale, no accounts yet */ + break; + case TMH_ES_NO_ACC_RETRY_OK: + if (oc->set_exchanges.forced_reload) + { + priority = 0; /* fresh negative response */ + } + else + { + oc->set_exchanges.promising_exchange = true; + priority = 512; /* stale negative response */ + } + break; + case TMH_ES_NO_CURR_RETRY_OK: + if (oc->set_exchanges.forced_reload) + priority = 0; /* fresh negative response */ + else + priority = 512; /* stale negative response */ + break; + case TMH_ES_NO_KEYS_RETRY_OK: + if (oc->set_exchanges.forced_reload) + { + priority = 256; /* fresh, no accounts yet */ + } + else + { + oc->set_exchanges.promising_exchange = true; + priority = 768; /* stale, no accounts yet */ + } + break; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exchange %s deposit limit is %s, adding it!\n", + exchange_url, + TALER_amount2s (&max_amount)); + + j_exchange = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("url", + exchange_url), + GNUNET_JSON_pack_uint64 ("priority", + priority), + TALER_JSON_pack_amount ("max_contribution", + &max_amount), + GNUNET_JSON_pack_data_auto ("master_pub", + TMH_EXCHANGES_get_master_pub (exchange))); + GNUNET_assert (NULL != j_exchange); + /* Add exchange to list of exchanges for this wire method + candidate */ + GNUNET_assert (0 == + json_array_append_new (wmc->exchanges, + j_exchange)); + add_to_currency_vector (&wmc->total_exchange_limits, + &wmc->num_total_exchange_limits, + &max_amount, + max_needed); + return true; +} + + +/** + * Function called with the result of a #TMH_EXCHANGES_keys4exchange() + * operation. + * + * @param cls closure with our `struct RekeyExchange *` + * @param keys the keys of the exchange + * @param exchange representation of the exchange + */ +static void +keys_cb ( + void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct RekeyExchange *rx = cls; + struct OrderContext *oc = rx->oc; + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + bool applicable = false; + + rx->fo = NULL; + GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + if (NULL == keys) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to download %skeys\n", + rx->url); + oc->set_exchanges.promising_exchange = true; + add_rejection (oc, + rx->url, + TALER_EC_MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE); + goto cleanup; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response for %skeys\n", + rx->url); + + /* Evaluate the use of this exchange for each wire method candidate */ + for (unsigned int j = 0; j<keys->accounts_len; j++) + { + struct TALER_FullPayto full_payto = keys->accounts[j].fpayto_uri; + char *wire_method = TALER_payto_get_method (full_payto.full_payto); + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Exchange `%s' has wire method `%s'\n", + rx->url, + wire_method); + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Order could use wire method `%s'\n", + wmc->wm->wire_method); + if (0 == strcmp (wmc->wm->wire_method, + wire_method) ) + { + applicable |= get_acceptable (oc, + exchange, + rx->url, + wmc); + } + } + GNUNET_free (wire_method); + } + if ( (! applicable) && + (! oc->set_exchanges.forced_reload) ) + { + /* Checks for 'forced_reload' to not log the error *again* + if we forced a re-load and are encountering the + applicability error a 2nd time */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Exchange `%s' %u wire methods are not applicable to this order\n", + rx->url, + keys->accounts_len); + add_rejection (oc, + rx->url, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED) + ; + } + if (applicable && + settings->use_stefan) + update_stefan (oc, + keys); +cleanup: + GNUNET_free (rx->url); + GNUNET_free (rx); + if (NULL != oc->set_exchanges.pending_reload_head) + return; + resume_with_keys (oc); +} + + +/** + * Force re-downloading of /keys from @a exchange, + * we currently have no acceptable exchange, so we + * should try to get one. + * + * @param cls closure with our `struct OrderContext` + * @param url base URL of the exchange + * @param exchange internal handle for the exchange + */ +static void +get_exchange_keys (void *cls, + const char *url, + const struct TMH_Exchange *exchange) +{ + struct OrderContext *oc = cls; + struct RekeyExchange *rx; + + rx = GNUNET_new (struct RekeyExchange); + rx->oc = oc; + rx->url = GNUNET_strdup (url); + GNUNET_CONTAINER_DLL_insert (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + if (oc->set_exchanges.forced_reload) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Forcing download of %skeys\n", + url); + rx->fo = TMH_EXCHANGES_keys4exchange (url, + oc->set_exchanges.forced_reload, + &keys_cb, + rx); +} + + +/** + * Task run when we are timing out on /keys and will just + * proceed with what we got. + * + * @param cls our `struct OrderContext *` to resume + */ +static void +wakeup_timeout (void *cls) +{ + struct OrderContext *oc = cls; + + oc->set_exchanges.wakeup_task = NULL; + GNUNET_assert (GNUNET_YES == oc->suspended); + GNUNET_CONTAINER_DLL_remove (oc_head, + oc_tail, + oc); + MHD_resume_connection (oc->connection); + oc->suspended = GNUNET_NO; + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ +} + + +/** + * Set list of acceptable exchanges in @a oc. Upon success, continues + * processing with add_payment_details(). + * + * @param[in,out] oc order context + * @return true to suspend execution + */ +static bool +phase_set_exchanges (struct OrderContext *oc) +{ + if (NULL != oc->set_exchanges.wakeup_task) + { + GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); + oc->set_exchanges.wakeup_task = NULL; + } + + if (! oc->add_payment_details.need_exchange) + { + /* Total amount is zero, so we don't actually need exchanges! */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order total is zero, no need for exchanges\n"); + oc->select_wire_method.exchanges = json_array (); + GNUNET_assert (NULL != oc->select_wire_method.exchanges); + /* Pick first one, doesn't matter as the amount is zero */ + oc->select_wire_method.wm = oc->hc->instance->wm_head; + oc->phase = ORDER_PHASE_SET_MAX_FEE; + return false; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Trying to find exchanges\n"); + if (NULL == oc->set_exchanges.pending_reload_head) + { + if (! oc->set_exchanges.exchanges_tried) + { + oc->set_exchanges.exchanges_tried = true; + oc->set_exchanges.keys_timeout + = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); + TMH_exchange_get_trusted (&get_exchange_keys, + oc); + } + else if ( (! oc->set_exchanges.forced_reload) && + (oc->set_exchanges.promising_exchange) && + (! oc->set_exchanges.exchange_ok) ) + { + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + GNUNET_break (0 == + json_array_clear (wmc->exchanges)); + /* Try one more time with forcing /keys download */ + oc->set_exchanges.forced_reload = true; + TMH_exchange_get_trusted (&get_exchange_keys, + oc); + } + } + if (GNUNET_TIME_absolute_is_past (oc->set_exchanges.keys_timeout)) + { + struct RekeyExchange *rx; + + while (NULL != (rx = oc->set_exchanges.pending_reload_head)) + { + GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, + oc->set_exchanges.pending_reload_tail, + rx); + TMH_EXCHANGES_keys4exchange_cancel (rx->fo); + GNUNET_free (rx->url); + GNUNET_free (rx); + } + } + if (NULL != oc->set_exchanges.pending_reload_head) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Still trying to (re)load %skeys\n", + oc->set_exchanges.pending_reload_head->url); + oc->set_exchanges.wakeup_task + = GNUNET_SCHEDULER_add_at (oc->set_exchanges.keys_timeout, + &wakeup_timeout, + oc); + MHD_suspend_connection (oc->connection); + oc->suspended = GNUNET_YES; + GNUNET_CONTAINER_DLL_insert (oc_head, + oc_tail, + oc); + return true; /* reloads pending */ + } + oc->phase++; + return false; +} + + +/* ***************** ORDER_PHASE_ADD_PAYMENT_DETAILS **************** */ + +/** + * Process the @a payment_target and add the details of how the + * order could be paid to @a order. On success, continue + * processing with add_payment_fees(). + * + * @param[in,out] oc order context + */ +static void +phase_add_payment_details (struct OrderContext *oc) +{ + /* First, determine the maximum amounts that could be paid per currency */ + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + GNUNET_array_append (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + oc->parse_order.details.v0.brutto); + if (! TALER_amount_is_zero ( + &oc->parse_order.details.v0.brutto)) + { + oc->add_payment_details.need_exchange = true; + } + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + const struct TALER_Amount *amount + = &oc->parse_choices.choices[i].amount; + bool found = false; + + if (! TALER_amount_is_zero (amount)) + { + oc->add_payment_details.need_exchange = true; + } + for (unsigned int j = 0; j<oc->add_payment_details.num_max_choice_limits; + j++) + { + struct TALER_Amount *mx = &oc->add_payment_details.max_choice_limits[j]; + if (GNUNET_YES == + TALER_amount_cmp_currency (mx, + amount)) + { + TALER_amount_max (mx, + mx, + amount); + found = true; + break; + } + } + if (! found) + { + GNUNET_array_append (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + *amount); + } + } + break; + default: + GNUNET_assert (0); + } + + /* Then, create a candidate for each available wire method */ + for (struct TMH_WireMethod *wm = oc->hc->instance->wm_head; + NULL != wm; + wm = wm->next) + { + struct WireMethodCandidate *wmc; + + /* Locate wire method that has a matching payment target */ + if (! wm->active) + continue; /* ignore inactive methods */ + if ( (NULL != oc->parse_request.payment_target) && + (0 != strcasecmp (oc->parse_request.payment_target, + wm->wire_method) ) ) + continue; /* honor client preference */ + wmc = GNUNET_new (struct WireMethodCandidate); + wmc->wm = wm; + wmc->exchanges = json_array (); + GNUNET_assert (NULL != wmc->exchanges); + GNUNET_CONTAINER_DLL_insert (oc->add_payment_details.wmc_head, + oc->add_payment_details.wmc_tail, + wmc); + } + + if (NULL == oc->add_payment_details.wmc_head) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "No wire method available for instance '%s'\n", + oc->hc->instance->settings.id); + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, + oc->parse_request.payment_target); + return; + } + + /* next, we'll evaluate available exchanges */ + oc->phase++; +} + + +/* ***************** ORDER_PHASE_MERGE_INVENTORY **************** */ + + +/** + * Helper function to sort uint64_t array with qsort(). + * + * @param a pointer to element to compare + * @param b pointer to element to compare + * @return 0 on equal, -1 on smaller, 1 on larger + */ +static int +uint64_cmp (const void *a, + const void *b) +{ + uint64_t ua = *(const uint64_t *) a; + uint64_t ub = *(const uint64_t *) b; + + if (ua < ub) + return -1; + if (ua > ub) + return 1; + return 0; +} + + +/** + * Merge the inventory products into products, querying the + * database about the details of those products. Upon success, + * continue processing by calling add_payment_details(). + * + * @param[in,out] oc order context to process + */ +static void +phase_merge_inventory (struct OrderContext *oc) +{ + uint64_t pots[oc->parse_order.products_len + 1]; + size_t pots_off = 0; + + if (0 != oc->parse_order.order_default_money_pot) + pots[pots_off++] = oc->parse_order.order_default_money_pot; + /** + * parse_request.inventory_products => instructions to add products to contract terms + * parse_order.products => contains products that are not from the backend-managed inventory. + */ + oc->merge_inventory.products = json_array (); + for (size_t i = 0; i<oc->parse_order.products_len; i++) + { + GNUNET_assert ( + 0 == + json_array_append_new ( + oc->merge_inventory.products, + TALER_MERCHANT_product_sold_serialize (&oc->parse_order.products[i]))); + if (0 != oc->parse_order.products[i].product_money_pot) + pots[pots_off++] = oc->parse_order.products[i].product_money_pot; + } + + /* make sure pots array only has distinct elements */ + qsort (pots, + pots_off, + sizeof (uint64_t), + &uint64_cmp); + { + size_t e = 0; + + for (size_t i = 1; i<pots_off; i++) + { + if (pots[e] != pots[i]) + pots[++e] = pots[i]; + } + if (pots_off > 0) + e++; + pots_off = e; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found %u unique money pots in order\n", + (unsigned int) pots_off); + + /* check if all money pots exist; note that we do NOT treat + the inventory products to this check, as (1) the foreign key + constraint should ensure this, and (2) if the money pot + were deleted (concurrently), the value is specified to be + considered 0 (aka none) and so we can proceed anyway. */ + if (pots_off > 0) + { + enum GNUNET_DB_QueryStatus qs; + uint64_t pot_missing; + + qs = TMH_db->check_money_pots (TMH_db->cls, + oc->hc->instance->settings.id, + pots_off, + pots, + &pot_missing); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check_money_pots"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* great, good case! */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "All money pots exist\n"); + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + char mstr[32]; + + GNUNET_snprintf (mstr, + sizeof (mstr), + "%llu", + (unsigned long long) pot_missing); + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + mstr); + return; + } + } + } + + /* Populate products from inventory product array and database */ + { + GNUNET_assert (NULL != oc->merge_inventory.products); + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) + { + struct InventoryProduct *ip + = &oc->parse_request.inventory_products[i]; + struct TALER_MERCHANTDB_ProductDetails pd; + enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; + + qs = TMH_db->lookup_product (TMH_db->cls, + oc->hc->instance->settings.id, + ip->product_id, + &pd, + &num_categories, + &categories); + if (qs <= 0) + { + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + unsigned int http_status = 0; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Product %s from order unknown\n", + ip->product_id); + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); + } + reply_with_error (oc, + http_status, + ec, + ip->product_id); + return; + } + GNUNET_free (categories); + oc->parse_order.minimum_age + = GNUNET_MAX (oc->parse_order.minimum_age, + pd.minimum_age); + { + const char *eparam; + + if ( (! ip->quantity_missing) && + (ip->quantity > (uint64_t) INT64_MAX) ) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "quantity"); + TALER_MERCHANTDB_product_details_free (&pd); + return; + } + if (GNUNET_OK != + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_QUANTITY, + pd.allow_fractional_quantity, + ip->quantity_missing, + (int64_t) ip->quantity, + ip->unit_quantity_missing, + ip->unit_quantity, + &ip->quantity, + &ip->quantity_frac, + &eparam)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + TALER_MERCHANTDB_product_details_free (&pd); + return; + } + } + { + struct TALER_MERCHANT_ProductSold ps = { + .product_id = (char *) ip->product_id, + .product_name = pd.product_name, + .description = pd.description, + .description_i18n = pd.description_i18n, + .unit_quantity.integer = ip->quantity, + .unit_quantity.fractional = ip->quantity_frac, + .prices_length = pd.price_array_length, + .prices = GNUNET_new_array (pd.price_array_length, + struct TALER_Amount), + .prices_are_net = pd.price_is_net, + .image = pd.image, + .taxes = pd.taxes, + .delivery_date = oc->parse_order.delivery_date, + .product_money_pot = pd.money_pot_id, + .unit = pd.unit, + + }; + json_t *p; + char unit_quantity_buf[64]; + + for (size_t j = 0; j<pd.price_array_length; j++) + { + struct TALER_Amount atomic_amount; + + TALER_amount_set_zero (pd.price_array[j].currency, + &atomic_amount); + atomic_amount.fraction = 1; + GNUNET_assert ( + GNUNET_OK == + TALER_MERCHANT_amount_multiply_by_quantity ( + &ps.prices[j], + &pd.price_array[j], + &ps.unit_quantity, + TALER_MERCHANT_ROUND_UP, + &atomic_amount)); + } + + TALER_MERCHANT_vk_format_fractional_string ( + TALER_MERCHANT_VK_QUANTITY, + ip->quantity, + ip->quantity_frac, + sizeof (unit_quantity_buf), + unit_quantity_buf); + if (0 != pd.money_pot_id) + pots[pots_off++] = pd.money_pot_id; + p = TALER_MERCHANT_product_sold_serialize (&ps); + GNUNET_assert (NULL != p); + GNUNET_free (ps.prices); + GNUNET_assert (0 == + json_array_append_new (oc->merge_inventory.products, + p)); + } + TALER_MERCHANTDB_product_details_free (&pd); + } + } + + /* check if final product list is well-formed */ + if (! TMH_products_array_valid (oc->merge_inventory.products)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order:products"); + return; + } + oc->phase++; +} + + +/* ***************** ORDER_PHASE_PARSE_CHOICES **************** */ + +#ifdef HAVE_DONAU_DONAU_SERVICE_H +/** + * Callback function that is called for each donau instance. + * It simply adds the provided donau_url to the json. + * + * @param cls closure with our `struct TALER_MERCHANT_ContractOutput *` + * @param donau_url the URL of the donau instance + */ +static void +add_donau_url (void *cls, + const char *donau_url) +{ + struct TALER_MERCHANT_ContractOutput *output = cls; + + GNUNET_array_append (output->details.donation_receipt.donau_urls, + output->details.donation_receipt.donau_urls_len, + GNUNET_strdup (donau_url)); +} + + +/** + * Add the donau output to the contract output. + * + * @param oc order context + * @param output contract output to add donau URLs to + */ +static bool +add_donau_output (struct OrderContext *oc, + struct TALER_MERCHANT_ContractOutput *output) +{ + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->select_donau_instances_filtered ( + TMH_db->cls, + output->details.donation_receipt.amount.currency, + &add_donau_url, + output); + if (qs < 0) + { + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "donau url parsing db call"); + for (unsigned int i = 0; + i < output->details.donation_receipt.donau_urls_len; + i++) + GNUNET_free (output->details.donation_receipt.donau_urls[i]); + GNUNET_array_grow (output->details.donation_receipt.donau_urls, + output->details.donation_receipt.donau_urls_len, + 0); + return false; + } + return true; +} + + +#endif + +/** + * Parse contract choices. Upon success, continue + * processing with merge_inventory(). + * + * @param[in,out] oc order context + */ +static void +phase_parse_choices (struct OrderContext *oc) +{ + const json_t *jchoices; + + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + oc->phase++; + return; + case TALER_MERCHANT_CONTRACT_VERSION_1: + /* handle below */ + break; + default: + GNUNET_assert (0); + } + + jchoices = oc->parse_order.details.v1.choices; + + if (! json_is_array (jchoices)) + GNUNET_assert (0); + if (0 == json_array_size (jchoices)) + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choices"); + return; + } + GNUNET_array_grow (oc->parse_choices.choices, + oc->parse_choices.choices_len, + json_array_size (jchoices)); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + struct TALER_MERCHANT_ContractChoice *choice + = &oc->parse_choices.choices[i]; + const char *error_name; + unsigned int error_line; + const json_t *jinputs; + const json_t *joutputs; + bool no_fee; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &choice->amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &choice->tip), + &choice->no_tip), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("max_fee", + &choice->max_fee), + &no_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("description", + &choice->description), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("description_i18n", + &choice->description_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + ret = GNUNET_JSON_parse (json_array_get (jchoices, + i), + spec, + &error_name, + &error_line); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Choice parsing failed: %s:%u\n", + error_name, + error_line); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice"); + return; + } + if ( (! no_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&choice->amount, + &choice->max_fee)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "different currencies used for 'max_fee' and 'amount' currency"); + return; + } + if ( (! choice->no_tip) && + (GNUNET_OK != + TALER_amount_cmp_currency (&choice->amount, + &choice->tip)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "tip and amount"); + return; + } + + if (! TMH_test_exchange_configured_for_currency ( + choice->amount.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, + choice->amount.currency); + return; + } + + if (NULL != jinputs) + { + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MERCHANT_ContractInput input = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_input ((json_t *) jinput, + &input, + idx, + true)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "input"); + return; + } + + switch (input.type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == input.details.token.count) + continue; + + if (GNUNET_OK != + add_input_token_family (oc, + input.details.token.token_family_slug)) + + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + input.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + continue; + } + GNUNET_assert (0); + } + } + + if (NULL != joutputs) + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MERCHANT_ContractOutput output = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_output ((json_t *) joutput, + &output, + idx, + true)) + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "output"); + return; + } + + switch (output.type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: +#ifdef HAVE_DONAU_DONAU_SERVICE_H + output.details.donation_receipt.amount = choice->amount; + if (! add_donau_output (oc, + &output)) + { + GNUNET_break (0); + return; + } + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); +#endif + continue; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == output.details.token.count) + continue; + + if (0 == output.details.token.valid_at.abs_time.abs_value_us) + output.details.token.valid_at + = GNUNET_TIME_timestamp_get (); + if (GNUNET_OK != + add_output_token_family (oc, + output.details.token.token_family_slug, + output.details.token.valid_at, + &output.details.token.key_index)) + + { + /* note: reply_with_error() was already called */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Could not handle output token family `%s'\n", + output.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + continue; + } + GNUNET_assert (0); + } + } + } + oc->phase++; +} + + +/* ***************** ORDER_PHASE_PARSE_ORDER **************** */ + + +/** + * Parse the order field of the request. Upon success, continue + * processing with parse_choices(). + * + * @param[in,out] oc order context + */ +static void +phase_parse_order (struct OrderContext *oc) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + const char *merchant_base_url = NULL; + uint64_t version = 0; + const json_t *jmerchant = NULL; + const json_t *products = NULL; + const char *order_id = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("version", + &version), + NULL), + GNUNET_JSON_spec_string ("summary", + &oc->parse_order.summary), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("products", + &products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("summary_i18n", + &oc->parse_order.summary_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("order_id", + &order_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("fulfillment_message", + &oc->parse_order.fulfillment_message), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("fulfillment_message_i18n", + &oc->parse_order.fulfillment_message_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("fulfillment_url", + &oc->parse_order.fulfillment_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("public_reorder_url", + &oc->parse_order.public_reorder_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("merchant_base_url", + &merchant_base_url), + NULL), + /* For sanity check, this field must NOT be present */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("merchant", + &jmerchant), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("timestamp", + &oc->parse_order.timestamp), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("refund_deadline", + &oc->parse_order.refund_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("pay_deadline", + &oc->parse_order.pay_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &oc->parse_order.wire_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("delivery_location", + &oc->parse_order.delivery_location), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("delivery_date", + &oc->parse_order.delivery_date), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("minimum_age", + &oc->parse_order.minimum_age), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("auto_refund", + &oc->parse_order.auto_refund), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("extra", + &oc->parse_order.extra), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("order_default_money_pot", + &oc->parse_order.order_default_money_pot), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + bool computed_refund_deadline; + + oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + if ( (NULL != products) && + (0 != (oc->parse_order.products_len = json_array_size (products))) ) + { + size_t i; + json_t *p; + + oc->parse_order.products + = GNUNET_new_array (oc->parse_order.products_len, + struct TALER_MERCHANT_ProductSold); + json_array_foreach (products, i, p) + { + if (GNUNET_OK != + TALER_MERCHANT_parse_product_sold (p, + &oc->parse_order.products[i])) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order.products"); + return; + } + } + } + if (NULL != order_id) + { + size_t len = strlen (order_id); + + for (size_t i = 0; i<len; i++) + { + char c = order_id[i]; + + if (! ( ( (c >= 'A') && + (c <= 'Z') ) || + ( (c >= 'a') && + (c <= 'z') ) || + ( (c >= '0') && + (c <= '9') ) || + (c == '-') || + (c == '_') || + (c == '.') || + (c == ':') ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid character `%c' in order ID `%s'\n", + c, + order_id); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "Invalid character in order_id"); + return; + } + } + } + switch (version) + { + case 0: + { + bool no_fee; + const json_t *choices = NULL; + struct GNUNET_JSON_Specification specv0[] = { + TALER_JSON_spec_amount_any ( + "amount", + &oc->parse_order.details.v0.brutto), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ( + "tip", + &oc->parse_order.details.v0.tip), + &oc->parse_order.details.v0.no_tip), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ( + "max_fee", + &oc->parse_order.details.v0.max_fee), + &no_fee), + /* for sanity check, must be *absent*! */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("choices", + &choices), + NULL), + GNUNET_JSON_spec_end () + }; + + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, + specv0); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + if ( (! no_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, + &oc->parse_order.details.v0.max_fee)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "different currencies used for 'max_fee' and 'amount' currency"); + return; + } + if ( (! oc->parse_order.details.v0.no_tip) && + (GNUNET_OK != + TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, + &oc->parse_order.details.v0.tip)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "tip and amount"); + return; + } + if (! TMH_test_exchange_configured_for_currency ( + oc->parse_order.details.v0.brutto.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, + oc->parse_order.details.v0.brutto.currency); + return; + } + if (NULL != choices) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, + "choices array must be null for v0 contracts"); + return; + } + oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0; + break; + } + case 1: + { + struct GNUNET_JSON_Specification specv1[] = { + GNUNET_JSON_spec_array_const ( + "choices", + &oc->parse_order.details.v1.choices), + GNUNET_JSON_spec_end () + }; + + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, + specv1); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1; + break; + } + default: + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_VERSION_MALFORMED, + "invalid version specified in order, supported are null, '0' or '1'"); + return; + } + + /* Add order_id if it doesn't exist. */ + if (NULL != order_id) + { + oc->parse_order.order_id = GNUNET_strdup (order_id); + } + else + { + char buf[256]; + time_t timer; + struct tm *tm_info; + size_t off; + uint64_t rand; + char *last; + + time (&timer); + tm_info = localtime (&timer); + if (NULL == tm_info) + { + GNUNET_JSON_parse_free (spec); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME, + NULL); + return; + } + off = strftime (buf, + sizeof (buf) - 1, + "%Y.%j", + tm_info); + /* Check for error state of strftime */ + GNUNET_assert (0 != off); + buf[off++] = '-'; + rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, + UINT64_MAX); + last = GNUNET_STRINGS_data_to_string (&rand, + sizeof (uint64_t), + &buf[off], + sizeof (buf) - off); + GNUNET_assert (NULL != last); + *last = '\0'; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Assigning order ID `%s' server-side\n", + buf); + + oc->parse_order.order_id = GNUNET_strdup (buf); + GNUNET_assert (NULL != oc->parse_order.order_id); + } + + /* Patch fulfillment URL with order_id (implements #6467). */ + if (NULL != oc->parse_order.fulfillment_url) + { + const char *pos; + + pos = strstr (oc->parse_order.fulfillment_url, + "${ORDER_ID}"); + if (NULL != pos) + { + /* replace ${ORDER_ID} with the real order_id */ + char *nurl; + + /* We only allow one placeholder */ + if (strstr (pos + strlen ("${ORDER_ID}"), + "${ORDER_ID}")) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "fulfillment_url"); + return; + } + + GNUNET_asprintf (&nurl, + "%.*s%s%s", + /* first output URL until ${ORDER_ID} */ + (int) (pos - oc->parse_order.fulfillment_url), + oc->parse_order.fulfillment_url, + /* replace ${ORDER_ID} with the right order_id */ + oc->parse_order.order_id, + /* append rest of original URL */ + pos + strlen ("${ORDER_ID}")); + + oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); + + GNUNET_free (nurl); + } + } + + if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) || + (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) ) + { + oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp ( + settings->default_pay_delay); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Pay deadline was zero (or never), setting to %s\n", + GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); + } + else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST, + NULL); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Pay deadline is %s\n", + GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); + + /* Check soundness of refund deadline, and that a timestamp + * is actually present. */ + { + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + + /* Add timestamp if it doesn't exist (or is zero) */ + if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time)) + { + oc->parse_order.timestamp = now; + } + + /* If no refund_deadline given, set one based on refund_delay. */ + if (GNUNET_TIME_absolute_is_never ( + oc->parse_order.refund_deadline.abs_time)) + { + if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Refund delay is zero, no refunds are possible for this order\n"); + oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; + } + else + { + computed_refund_deadline = true; + oc->parse_order.refund_deadline + = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_absolute_add (oc->parse_order.pay_deadline.abs_time, + oc->parse_request.refund_delay)); + } + } + + if ( (! GNUNET_TIME_absolute_is_zero ( + oc->parse_order.delivery_date.abs_time)) && + (GNUNET_TIME_absolute_is_past ( + oc->parse_order.delivery_date.abs_time)) ) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST, + NULL); + return; + } + } + + if ( (! GNUNET_TIME_absolute_is_zero ( + oc->parse_order.refund_deadline.abs_time)) && + (GNUNET_TIME_absolute_is_past ( + oc->parse_order.refund_deadline.abs_time)) ) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST, + NULL); + return; + } + + if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) + { + struct GNUNET_TIME_Absolute start; + + start = GNUNET_TIME_absolute_max ( + oc->parse_order.refund_deadline.abs_time, + oc->parse_order.pay_deadline.abs_time); + oc->parse_order.wire_deadline + = GNUNET_TIME_absolute_to_timestamp ( + GNUNET_TIME_round_up ( + GNUNET_TIME_absolute_add ( + start, + settings->default_wire_transfer_delay), + settings->default_wire_transfer_rounding_interval)); + if (GNUNET_TIME_absolute_is_never ( + oc->parse_order.wire_deadline.abs_time)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER, + "order:wire_transfer_deadline"); + return; + } + } + else if (computed_refund_deadline) + { + /* if we computed the refund_deadline from default settings + and did have a configured wire_deadline, make sure that + the refund_deadline is at or below the wire_deadline. */ + oc->parse_order.refund_deadline + = GNUNET_TIME_timestamp_min (oc->parse_order.refund_deadline, + oc->parse_order.wire_deadline); + } + if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, + <, + oc->parse_order.refund_deadline)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE, + "order:wire_transfer_deadline;order:refund_deadline"); + return; + } + + if (NULL != merchant_base_url) + { + if (('\0' == *merchant_base_url) || + ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, + "merchant_base_url is not valid"); + return; + } + oc->parse_order.merchant_base_url + = GNUNET_strdup (merchant_base_url); + } + else + { + char *url; + + url = make_merchant_base_url (oc->connection, + settings->id); + if (NULL == url) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "order:merchant_base_url"); + return; + } + oc->parse_order.merchant_base_url = url; + } + + /* Merchant information must not already be present */ + if (NULL != jmerchant) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, + "'merchant' field already set, but must be provided by backend"); + return; + } + + if ( (NULL != oc->parse_order.delivery_location) && + (! TMH_location_object_valid (oc->parse_order.delivery_location)) ) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "delivery_location"); + return; + } + + oc->phase++; +} + + +/* ***************** ORDER_PHASE_PARSE_REQUEST **************** */ + +/** + * Parse the client request. Upon success, + * continue processing by calling parse_order(). + * + * @param[in,out] oc order context to process + */ +static void +phase_parse_request (struct OrderContext *oc) +{ + const json_t *ip = NULL; + const json_t *uuid = NULL; + const char *otp_id = NULL; + bool create_token = true; /* default */ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_json ("order", + &oc->parse_request.order), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("refund_delay", + &oc->parse_request.refund_delay), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("payment_target", + &oc->parse_request.payment_target), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("inventory_products", + &ip), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("session_id", + &oc->parse_request.session_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("lock_uuids", + &uuid), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("create_token", + &create_token), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("otp_id", + &otp_id), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + oc->parse_request.refund_delay + = oc->hc->instance->settings.default_refund_delay; + ret = TALER_MHD_parse_json_data (oc->connection, + oc->hc->request_body, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Refund delay is %s\n", + GNUNET_TIME_relative2s (oc->parse_request.refund_delay, + false)); + TMH_db->expire_locks (TMH_db->cls); + if (NULL != otp_id) + { + struct TALER_MERCHANTDB_OtpDeviceDetails td; + enum GNUNET_DB_QueryStatus qs; + + memset (&td, + 0, + sizeof (td)); + qs = TMH_db->select_otp (TMH_db->cls, + oc->hc->instance->settings.id, + otp_id, + &td); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_otp"); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "select_otp"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, + otp_id); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + oc->parse_request.pos_key = td.otp_key; + oc->parse_request.pos_algorithm = td.otp_algorithm; + GNUNET_free (td.otp_description); + } + if (create_token) + { + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &oc->parse_request.claim_token, + sizeof (oc->parse_request.claim_token)); + } + /* Compute h_post_data (for idempotency check) */ + { + char *req_body_enc; + + /* Dump normalized JSON to string. */ + if (NULL == (req_body_enc + = json_dumps (oc->hc->request_body, + JSON_ENCODE_ANY + | JSON_COMPACT + | JSON_SORT_KEYS))) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + "request body normalization for hashing"); + return; + } + GNUNET_CRYPTO_hash (req_body_enc, + strlen (req_body_enc), + &oc->parse_request.h_post_data.hash); + GNUNET_free (req_body_enc); + } + + /* parse the inventory_products (optionally given) */ + if (NULL != ip) + { + unsigned int ipl = (unsigned int) json_array_size (ip); + + if ( (json_array_size (ip) != (size_t) ipl) || + (ipl > MAX_PRODUCTS) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_products (too many)"); + return; + } + GNUNET_array_grow (oc->parse_request.inventory_products, + oc->parse_request.inventory_products_length, + (unsigned int) json_array_size (ip)); + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) + { + struct InventoryProduct *ipr = &oc->parse_request.inventory_products[i]; + const char *error_name; + unsigned int error_line; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("product_id", + &ipr->product_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("quantity", + &ipr->quantity), + &ipr->quantity_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_quantity", + &ipr->unit_quantity), + &ipr->unit_quantity_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("product_money_pot", + &ipr->product_money_pot), + NULL), + GNUNET_JSON_spec_end () + }; + + ret = GNUNET_JSON_parse (json_array_get (ip, + i), + ispec, + &error_name, + &error_line); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Product parsing failed at #%u: %s:%u\n", + i, + error_name, + error_line); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_products"); + return; + } + if (ipr->quantity_missing && ipr->unit_quantity_missing) + { + ipr->quantity = 1; + ipr->quantity_missing = false; + } + } + } + + /* parse the lock_uuids (optionally given) */ + if (NULL != uuid) + { + GNUNET_array_grow (oc->parse_request.uuids, + oc->parse_request.uuids_length, + json_array_size (uuid)); + for (unsigned int i = 0; i<oc->parse_request.uuids_length; i++) + { + json_t *ui = json_array_get (uuid, + i); + + if (! json_is_string (ui)) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "UUID parsing failed at #%u\n", + i); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "lock_uuids"); + return; + } + TMH_uuid_from_string (json_string_value (ui), + &oc->parse_request.uuids[i]); + } + } + oc->phase++; +} + + +/* ***************** Main handler **************** */ + + +MHD_RESULT +TMH_private_post_orders ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct OrderContext *oc = hc->ctx; + + if (NULL == oc) + { + oc = GNUNET_new (struct OrderContext); + hc->ctx = oc; + hc->cc = &clean_order; + oc->connection = connection; + oc->hc = hc; + } + while (1) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Processing order in phase %d\n", + oc->phase); + switch (oc->phase) + { + case ORDER_PHASE_PARSE_REQUEST: + phase_parse_request (oc); + break; + case ORDER_PHASE_PARSE_ORDER: + phase_parse_order (oc); + break; + case ORDER_PHASE_PARSE_CHOICES: + phase_parse_choices (oc); + break; + case ORDER_PHASE_MERGE_INVENTORY: + phase_merge_inventory (oc); + break; + case ORDER_PHASE_ADD_PAYMENT_DETAILS: + phase_add_payment_details (oc); + break; + case ORDER_PHASE_SET_EXCHANGES: + if (phase_set_exchanges (oc)) + return MHD_YES; + break; + case ORDER_PHASE_SELECT_WIRE_METHOD: + phase_select_wire_method (oc); + break; + case ORDER_PHASE_SET_MAX_FEE: + phase_set_max_fee (oc); + break; + case ORDER_PHASE_SERIALIZE_ORDER: + phase_serialize_order (oc); + break; + case ORDER_PHASE_CHECK_CONTRACT: + phase_check_contract (oc); + break; + case ORDER_PHASE_SALT_FORGETTABLE: + phase_salt_forgettable (oc); + break; + case ORDER_PHASE_EXECUTE_ORDER: + phase_execute_order (oc); + break; + case ORDER_PHASE_FINISHED_MHD_YES: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Finished processing order (1)\n"); + return MHD_YES; + case ORDER_PHASE_FINISHED_MHD_NO: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Finished processing order (0)\n"); + return MHD_NO; + } + } +} + + +/* end of taler-merchant-httpd_post-private-orders.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-orders.h b/src/backend/taler-merchant-httpd_post-private-orders.h @@ -0,0 +1,50 @@ +/* + This file is part of TALER + (C) 2014, 2015, 2019 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/> +*/ +/** + * @file taler-merchant-httpd_post-private-orders.h + * @brief headers for POST /orders handler + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_H + +#include "taler-merchant-httpd.h" + + +/** + * Force resuming all suspended orders on shutdown. + */ +void +TMH_force_orders_resume (void); + + +/** + * Generate an order. We add the fields 'exchanges', 'merchant_pub', and + * 'H_wire' to the order gotten from the frontend, as well as possibly other + * fields if the frontend did not provide them. Returns the order_id. + * + * @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_orders (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-otp-devices.c b/src/backend/taler-merchant-httpd_post-private-otp-devices.c @@ -0,0 +1,199 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_post-private-otp-devices.c + * @brief implementing POST /otp-devices request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-otp-devices.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Check if the two otp-devices are identical. + * + * @param t1 device to compare + * @param t2 other device to compare + * @return true if they are 'equal', false if not or of payto_uris is not an array + */ +static bool +otp_devices_equal (const struct TALER_MERCHANTDB_OtpDeviceDetails *t1, + const struct TALER_MERCHANTDB_OtpDeviceDetails *t2) +{ + return ( (0 == strcmp (t1->otp_description, + t2->otp_description)) && + (0 == strcmp (t1->otp_key, + t2->otp_key) ) && + (t1->otp_ctr == t2->otp_ctr) && + (t1->otp_algorithm == t2->otp_algorithm) ); +} + + +MHD_RESULT +TMH_private_post_otp_devices (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_OtpDeviceDetails tp = { 0 }; + const char *device_id; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("otp_device_id", + &device_id), + GNUNET_JSON_spec_string ("otp_device_description", + (const char **) &tp.otp_description), + TALER_JSON_spec_otp_type ("otp_algorithm", + &tp.otp_algorithm), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("otp_ctr", + &tp.otp_ctr), + NULL), + TALER_JSON_spec_otp_key ("otp_key", + (const char **) &tp.otp_key), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + { + 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; + } + } + + /* finally, interact with DB until no serialization error */ + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + /* Test if a OTP device of this id is known */ + struct TALER_MERCHANTDB_OtpDeviceDetails etp; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "/post otp-devices")) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->select_otp (TMH_db->cls, + mi->settings.id, + device_id, + &etp); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + /* restart transaction */ + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Good, we can proceed! */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* idempotency check: is etp == tp? */ + { + bool eq; + + eq = otp_devices_equal (&tp, + &etp); + GNUNET_free (etp.otp_description); + GNUNET_free (etp.otp_key); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return eq + ? TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0) + : TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS, + device_id); + } + } /* end switch (qs) */ + + qs = TMH_db->insert_otp (TMH_db->cls, + mi->settings.id, + device_id, + &tp); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + TMH_db->rollback (TMH_db->cls); + break; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } +retry: + GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); + TMH_db->rollback (TMH_db->cls); + } /* for RETRIES loop */ + GNUNET_JSON_parse_free (spec); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + (GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? TALER_EC_GENERIC_DB_SOFT_FAILURE + : TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_post-private-otp-devices.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-otp-devices.h b/src/backend/taler-merchant-httpd_post-private-otp-devices.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_post-private-otp-devices.h + * @brief implementing POST /otp-devices request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_OTP_DEVICES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_OTP_DEVICES_H + +#include "taler-merchant-httpd.h" + + +/** + * Generate an OTP device. + * + * @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_otp_devices (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-pots.c b/src/backend/taler-merchant-httpd_post-private-pots.c @@ -0,0 +1,91 @@ +/* + 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 taler-merchant-httpd_post-private-pots.c + * @brief implementation of POST /private/pots + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-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; + 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_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; + } + } + + qs = TMH_db->insert_money_pot (TMH_db->cls, + hc->instance->settings.id, + pot_name, + description, + &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_post-private-pots.h b/src/backend/taler-merchant-httpd_post-private-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 taler-merchant-httpd_post-private-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-httpd_post-private-products-PRODUCT_ID-lock.c b/src/backend/taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.c @@ -0,0 +1,207 @@ +/* + This file is part of TALER + (C) 2020, 2021 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-httpd_post-private-products-PRODUCT_ID-lock.c + * @brief implementing POST /products/$ID/lock request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_post_products_ID_lock ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + const char *product_id = hc->infix; + enum GNUNET_DB_QueryStatus qs; + const char *uuids; + struct GNUNET_Uuid uuid; + uint64_t quantity; + bool quantity_missing; + const char *unit_quantity = NULL; + bool unit_quantity_missing = true; + struct GNUNET_TIME_Relative duration; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("lock_uuid", + &uuids), + GNUNET_JSON_spec_relative_time ("duration", + &duration), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("quantity", + &quantity), + &quantity_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_quantity", + &unit_quantity), + &unit_quantity_missing), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != product_id); + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + TMH_uuid_from_string (uuids, + &uuid); + TMH_db->expire_locks (TMH_db->cls); + { + struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; + size_t num_categories; + uint64_t *categories; + uint64_t normalized_quantity = 0; + uint32_t normalized_quantity_frac = 0; + + if (quantity_missing && unit_quantity_missing) + { + quantity = 1; + quantity_missing = false; + } + else if ( (! quantity_missing) && + (quantity > (uint64_t) INT64_MAX) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "quantity"); + } + + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + product_id, + &pd, + &num_categories, + &categories); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + TALER_MERCHANTDB_product_details_free (&pd); + GNUNET_free (categories); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + TALER_MERCHANTDB_product_details_free (&pd); + GNUNET_free (categories); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "lookup_product"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break_op (0); + TALER_MERCHANTDB_product_details_free (&pd); + GNUNET_free (categories); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + product_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + GNUNET_free (categories); + { + const char *eparam; + if (GNUNET_OK != + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_QUANTITY, + pd.allow_fractional_quantity, + quantity_missing, + (int64_t) quantity, + unit_quantity_missing, + unit_quantity, + &normalized_quantity, + &normalized_quantity_frac, + &eparam)) + { + TALER_MERCHANTDB_product_details_free (&pd); + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + } + } + quantity = normalized_quantity; + qs = TMH_db->lock_product (TMH_db->cls, + mi->settings.id, + product_id, + &uuid, + quantity, + normalized_quantity_frac, + GNUNET_TIME_relative_to_timestamp (duration)); + TALER_MERCHANTDB_product_details_free (&pd); + } + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "Serialization error for single-statment request"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_GONE, + TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS, + product_id); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_assert (0); + return MHD_NO; +} + + +/* end of taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.h b/src/backend/taler-merchant-httpd_post-private-products-PRODUCT_ID-lock.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_post-private-products-PRODUCT_ID-lock.h + * @brief implementing POST /products/$ID/lock request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_ID_LOCK_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_ID_LOCK_H +#include "taler-merchant-httpd.h" + + +/** + * Lock an existing product. + * + * @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_products_ID_lock (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-products.c b/src/backend/taler-merchant-httpd_post-private-products.c @@ -0,0 +1,435 @@ +/* + This file is part of TALER + (C) 2020-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-httpd_post-private-products.c + * @brief implementing POST /products request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-products.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + +MHD_RESULT +TMH_private_post_products (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; + const json_t *categories = NULL; + const char *product_id; + int64_t total_stock; + const char *unit_total_stock = NULL; + bool unit_total_stock_missing; + bool total_stock_missing; + bool unit_price_missing; + bool unit_allow_fraction; + bool unit_allow_fraction_missing; + uint32_t unit_precision_level; + bool unit_precision_missing; + struct TALER_Amount price; + bool price_missing; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("product_id", + &product_id), + /* new in protocol v20, thus optional for backwards-compatibility */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("product_name", + (const char **) &pd.product_name), + NULL), + GNUNET_JSON_spec_string ("description", + (const char **) &pd.description), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("description_i18n", + &pd.description_i18n), + NULL), + GNUNET_JSON_spec_string ("unit", + (const char **) &pd.unit), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("price", + &price), + &price_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("image", + (const char **) &pd.image), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("taxes", + &pd.taxes), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("categories", + &categories), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_total_stock", + &unit_total_stock), + &unit_total_stock_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_int64 ("total_stock", + &total_stock), + &total_stock_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("unit_allow_fraction", + &unit_allow_fraction), + &unit_allow_fraction_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("unit_precision_level", + &unit_precision_level), + &unit_precision_missing), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any_array ("unit_price", + &pd.price_array_length, + &pd.price_array), + &unit_price_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("address", + &pd.address), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("next_restock", + &pd.next_restock), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("minimum_age", + &pd.minimum_age), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("money_pot_id", + &pd.money_pot_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("product_group_id", + &pd.product_group_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("price_is_net", + &pd.price_is_net), + NULL), + GNUNET_JSON_spec_end () + }; + size_t num_cats = 0; + uint64_t *cats = NULL; + bool conflict; + bool no_instance; + ssize_t no_cat; + bool no_group; + bool no_pot; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; + + GNUNET_assert (NULL != mi); + { + 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; + } + /* For pre-v20 clients, we use the description given as the + product name; remove once we make product_name mandatory. */ + if (NULL == pd.product_name) + pd.product_name = pd.description; + + if (! unit_price_missing) + { + if (! price_missing) + { + if (0 != TALER_amount_cmp (&price, + &pd.price_array[0])) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "price,unit_price mismatch"); + goto cleanup; + } + } + if (GNUNET_OK != + TMH_validate_unit_price_array (pd.price_array, + pd.price_array_length)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_price"); + goto cleanup; + } + } + else + { + if (price_missing) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "price and unit_price missing"); + goto cleanup; + } + pd.price_array = GNUNET_new_array (1, + struct TALER_Amount); + pd.price_array[0] = price; + pd.price_array_length = 1; + } + } + if (! unit_precision_missing) + { + if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_precision_level"); + goto cleanup; + } + } + { + bool default_allow_fractional; + uint32_t default_precision_level; + + if (GNUNET_OK != + TMH_unit_defaults_for_instance (mi, + pd.unit, + &default_allow_fractional, + &default_precision_level)) + { + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "unit defaults"); + goto cleanup; + } + if (unit_allow_fraction_missing) + unit_allow_fraction = default_allow_fractional; + if (unit_precision_missing) + unit_precision_level = default_precision_level; + } + if (! unit_allow_fraction) + unit_precision_level = 0; + pd.fractional_precision_level = unit_precision_level; + { + const char *eparam; + + if (GNUNET_OK != + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_STOCK, + unit_allow_fraction, + total_stock_missing, + total_stock, + unit_total_stock_missing, + unit_total_stock, + &pd.total_stock, + &pd.total_stock_frac, + &eparam)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + goto cleanup; + } + pd.allow_fractional_quantity = unit_allow_fraction; + } + num_cats = json_array_size (categories); + cats = GNUNET_new_array (num_cats, + uint64_t); + { + size_t idx; + json_t *val; + + json_array_foreach (categories, idx, val) + { + if (! json_is_integer (val)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "categories"); + goto cleanup; + } + cats[idx] = json_integer_value (val); + } + } + + if (NULL == pd.address) + pd.address = json_object (); + if (NULL == pd.description_i18n) + pd.description_i18n = json_object (); + if (NULL == pd.taxes) + pd.taxes = json_array (); + + /* check taxes is well-formed */ + if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "taxes"); + goto cleanup; + } + + if (! TMH_location_object_valid (pd.address)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + goto cleanup; + } + + if (! TALER_JSON_check_i18n (pd.description_i18n)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + goto cleanup; + } + + if (NULL == pd.image) + pd.image = (char *) ""; + if (! TALER_MERCHANT_image_data_url_valid (pd.image)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "image"); + goto cleanup; + } + + qs = TMH_db->insert_product (TMH_db->cls, + mi->settings.id, + product_id, + &pd, + num_cats, + cats, + &no_instance, + &conflict, + &no_cat, + &no_group, + &no_pot); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + (GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? TALER_EC_GENERIC_DB_SOFT_FAILURE + : TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + if (no_instance) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + goto cleanup; + } + if (no_group) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, + NULL); + goto cleanup; + } + if (no_pot) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, + NULL); + goto cleanup; + } + if (conflict) + { + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, + product_id); + goto cleanup; + } + if (-1 != no_cat) + { + char nocats[24]; + + GNUNET_break_op (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_snprintf (nocats, + sizeof (nocats), + "%llu", + (unsigned long long) no_cat); + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + nocats); + goto cleanup; + } + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +cleanup: + GNUNET_JSON_parse_free (spec); + GNUNET_free (pd.price_array); + GNUNET_free (cats); + return ret; +} + + +/* end of taler-merchant-httpd_post-private-products.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-products.h b/src/backend/taler-merchant-httpd_post-private-products.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2020 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-httpd_post-private-products.h + * @brief implementing POST /products request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_H +#include "taler-merchant-httpd.h" + + +/** + * Generate a product entry in our inventory. + * + * @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_products (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-reports.c b/src/backend/taler-merchant-httpd_post-private-reports.c @@ -0,0 +1,147 @@ +/* + 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 taler-merchant-httpd_post-private-reports.c + * @brief implementation of POST /private/reports + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-reports.h" +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> + +MHD_RESULT +TMH_private_post_reports (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *description; + const char *program_section; + const char *mime_type; + const char *data_source; + const char *target_address; + struct GNUNET_TIME_Relative frequency; + struct GNUNET_TIME_Relative frequency_shift + = GNUNET_TIME_UNIT_ZERO; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("description", + &description), + GNUNET_JSON_spec_string ("program_section", + &program_section), + GNUNET_JSON_spec_string ("mime_type", + &mime_type), + GNUNET_JSON_spec_string ("data_source", + &data_source), + GNUNET_JSON_spec_string ("target_address", + &target_address), + GNUNET_JSON_spec_relative_time ("report_frequency", + &frequency), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("report_frequency_shift", + &frequency_shift), + NULL), + GNUNET_JSON_spec_end () + }; + uint64_t report_id; + + (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; + } + } + { + char *section; + + /* Check program_section exists in config! */ + GNUNET_asprintf (&section, + "report-generator-%s", + program_section); + if (GNUNET_YES != + GNUNET_CONFIGURATION_have_value (TMH_cfg, + section, + "BINARY")) + { + GNUNET_free (section); + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_MERCHANT_GENERIC_REPORT_GENERATOR_UNCONFIGURED, + program_section); + } + GNUNET_free (section); + } + if ('/' != data_source[0]) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "data_source"); + + } + qs = TMH_db->insert_report (TMH_db->cls, + hc->instance->settings.id, + program_section, + description, + mime_type, + data_source, + target_address, + frequency, + frequency_shift, + &report_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, + "insert_report"); + } + + /* FIXME-Optimization: do trigger inside of transaction above... */ + { + struct GNUNET_DB_EventHeaderP ev = { + .size = htons (sizeof (ev)), + .type = htons (TALER_DBEVENT_MERCHANT_REPORT_UPDATE) + }; + + TMH_db->event_notify (TMH_db->cls, + &ev, + NULL, + 0); + } + + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_uint64 ("report_serial_id", + report_id)); +} diff --git a/src/backend/taler-merchant-httpd_post-private-reports.h b/src/backend/taler-merchant-httpd_post-private-reports.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 taler-merchant-httpd_post-private-reports.h + * @brief HTTP serving layer for creating reports + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_REPORTS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_REPORTS_H + +#include "taler-merchant-httpd.h" + +/** + * Handle POST /private/reports 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_reports (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-templates.c b/src/backend/taler-merchant-httpd_post-private-templates.c @@ -0,0 +1,252 @@ +/* + This file is part of TALER + (C) 2022-2024 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-httpd_post-private-templates.c + * @brief implementing POST /templates request handling + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-templates.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * Check if the two templates are identical. + * + * @param t1 template to compare + * @param t2 other template to compare + * @return true if they are 'equal', false if not or of payto_uris is not an array + */ +static bool +templates_equal (const struct TALER_MERCHANTDB_TemplateDetails *t1, + const struct TALER_MERCHANTDB_TemplateDetails *t2) +{ + return ( (0 == strcmp (t1->template_description, + t2->template_description)) && + ( ( (NULL == t1->otp_id) && + (NULL == t2->otp_id) ) || + ( (NULL != t1->otp_id) && + (NULL != t2->otp_id) && + (0 == strcmp (t1->otp_id, + t2->otp_id))) ) && + ( ( (NULL == t1->editable_defaults) && + (NULL == t2->editable_defaults) ) || + ( (NULL != t1->editable_defaults) && + (NULL != t2->editable_defaults) && + (1 == json_equal (t1->editable_defaults, + t2->editable_defaults))) ) && + (1 == json_equal (t1->template_contract, + t2->template_contract)) ); +} + + +MHD_RESULT +TMH_private_post_templates (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; + const char *template_id; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("template_id", + &template_id), + GNUNET_JSON_spec_string ("template_description", + (const char **) &tp.template_description), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("otp_id", + (const char **) &tp.otp_id), + NULL), + GNUNET_JSON_spec_json ("template_contract", + &tp.template_contract), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("editable_defaults", + &tp.editable_defaults), + NULL), + GNUNET_JSON_spec_end () + }; + uint64_t otp_serial = 0; + + GNUNET_assert (NULL != mi); + { + 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; + } + } + if (! TALER_MERCHANT_template_contract_valid (tp.template_contract)) + { + GNUNET_break_op (0); + json_dumpf (tp.template_contract, + stderr, + JSON_INDENT (2)); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "template_contract"); + } + + if (NULL != tp.editable_defaults) + { + const char *key; + json_t *val; + + json_object_foreach (tp.editable_defaults, key, val) + { + if (NULL != + json_object_get (tp.template_contract, + key)) + { + char *msg; + MHD_RESULT ret; + + GNUNET_break_op (0); + GNUNET_asprintf (&msg, + "editable_defaults::%s conflicts with template_contract", + key); + GNUNET_JSON_parse_free (spec); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + msg); + GNUNET_free (msg); + return ret; + } + } + } + + if (NULL != tp.otp_id) + { + qs = TMH_db->select_otp_serial (TMH_db->cls, + mi->settings.id, + tp.otp_id, + &otp_serial); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "select_otp_serial"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + qs = TMH_db->insert_template (TMH_db->cls, + mi->settings.id, + template_id, + otp_serial, + &tp); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + break; + } + + { + /* Test if a template of this id is known */ + struct TALER_MERCHANTDB_TemplateDetails etp; + + qs = TMH_db->lookup_template (TMH_db->cls, + mi->settings.id, + template_id, + &etp); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "logic error"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + /* idempotency check: is etp == tp? */ + { + bool eq; + + eq = templates_equal (&tp, + &etp); + TALER_MERCHANTDB_template_details_free (&etp); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return eq + ? TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0) + : TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS, + template_id); + } + } +} + + +/* end of taler-merchant-httpd_post-private-templates.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-templates.h b/src/backend/taler-merchant-httpd_post-private-templates.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_post-private-templates.h + * @brief implementing POST /templates request handling + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_TEMPLATES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_TEMPLATES_H +#include "taler-merchant-httpd.h" + + +/** + * Generate a template entry. + * + * @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_templates (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-token.c b/src/backend/taler-merchant-httpd_post-private-token.c @@ -0,0 +1,192 @@ +/* + This file is part of GNU Taler + (C) 2023, 2025 Taler Systems SA + + GNU 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. + + GNU 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-httpd_post-private-token.c + * @brief implementing POST /instances/$ID/token request handling + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-token.h" +#include "taler-merchant-httpd_auth.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_mfa.h" +#include <taler/taler_json_lib.h> + + +/** + * Default duration for the validity of a login token. + */ +#define DEFAULT_DURATION GNUNET_TIME_UNIT_DAYS + + +MHD_RESULT +TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + json_t *jtoken = hc->request_body; + const char *scope; + const char *description; + enum TMH_AuthScope iscope = TMH_AS_NONE; + bool refreshable = false; + struct TALER_MERCHANTDB_LoginTokenP btoken; + struct GNUNET_TIME_Relative duration + = DEFAULT_DURATION; + struct GNUNET_TIME_Timestamp expiration_time; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("scope", + &scope), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("duration", + &duration), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("refreshable", + &refreshable), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("description", + &description), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + + { + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + jtoken, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + } + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &btoken, + sizeof (btoken)); + expiration_time = GNUNET_TIME_relative_to_timestamp (duration); + { + char *tmp_scope; + char *scope_prefix; + char *scope_suffix; + + tmp_scope = GNUNET_strdup (scope); + scope_prefix = strtok (tmp_scope, + ":"); + scope_suffix = strtok (NULL, + ":"); + /* We allow <SCOPE>:REFRESHABLE syntax */ + if ( (NULL != scope_suffix) && + (0 == strcasecmp (scope_suffix, + "refreshable"))) + refreshable = true; + iscope = TMH_get_scope_by_name (scope_prefix); + if (TMH_AS_NONE == iscope) + { + GNUNET_break_op (0); + GNUNET_free (tmp_scope); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "scope"); + } + GNUNET_free (tmp_scope); + } + if (refreshable) + iscope |= TMH_AS_REFRESHABLE; + if (! TMH_scope_is_subset (hc->auth_scope, + iscope)) + { + /* more permissions requested for the new token, not allowed */ + GNUNET_break_op (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT, + NULL); + } + if (NULL == description) + { + description = ""; + } + + { + enum GNUNET_GenericReturnValue ret = + TMH_mfa_check_simple (hc, + TALER_MERCHANT_MFA_CO_AUTH_TOKEN_CREATION, + mi); + + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + qs = TMH_db->insert_login_token (TMH_db->cls, + mi->settings.id, + &btoken, + GNUNET_TIME_timestamp_get (), + expiration_time, + iscope, + description); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_login_token"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + { + char *tok; + MHD_RESULT ret; + char *val; + + val = GNUNET_STRINGS_data_to_string_alloc (&btoken, + sizeof (btoken)); + GNUNET_asprintf (&tok, + RFC_8959_PREFIX "%s", + val); + GNUNET_free (val); + ret = TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_string ("access_token", + tok), + GNUNET_JSON_pack_string ("token", + tok), + GNUNET_JSON_pack_string ("scope", + scope), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_timestamp ("expiration", + expiration_time)); + GNUNET_free (tok); + return ret; + } +} + + +/* end of taler-merchant-httpd_post-private-token.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-token.h b/src/backend/taler-merchant-httpd_post-private-token.h @@ -0,0 +1,45 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems SA + + GNU 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. + + GNU 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-httpd_post-private-token.h + * @brief implements POST /instances/$ID/token request handling + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_TOKEN_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_TOKEN_H + +#include "taler-merchant-httpd.h" + + +/** + * Obtain a login token for an instance. + * + * @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_instances_ID_token (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-tokenfamilies.c b/src/backend/taler-merchant-httpd_post-private-tokenfamilies.c @@ -0,0 +1,384 @@ +/* + This file is part of TALER + (C) 2023, 2024 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-httpd_post-private-tokenfamilies.c + * @brief implementing POST /tokenfamilies request handling + * @author Christian Blättler + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-tokenfamilies.h" +#include "taler-merchant-httpd_helper.h" +#include <gnunet/gnunet_time_lib.h> +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Check if the two token families are identical. + * + * @param tf1 token family to compare + * @param tf2 other token family to compare + * @return true if they are 'equal', false if not + */ +static bool +token_families_equal (const struct TALER_MERCHANTDB_TokenFamilyDetails *tf1, + const struct TALER_MERCHANTDB_TokenFamilyDetails *tf2) +{ + /* Note: we're not comparing 'cipher', as that is selected + in the database to some default value and we currently + do not allow the SPA to change it. As a result, it should + always be "NULL" in tf1 and the DB-default in tf2. */ + return ( (0 == strcmp (tf1->slug, + tf2->slug)) && + (0 == strcmp (tf1->name, + tf2->name)) && + (0 == strcmp (tf1->description, + tf2->description)) && + (1 == json_equal (tf1->description_i18n, + tf2->description_i18n)) && + ( (tf1->extra_data == tf2->extra_data) || + (1 == json_equal (tf1->extra_data, + tf2->extra_data)) ) && + (GNUNET_TIME_timestamp_cmp (tf1->valid_after, + ==, + tf2->valid_after)) && + (GNUNET_TIME_timestamp_cmp (tf1->valid_before, + ==, + tf2->valid_before)) && + (GNUNET_TIME_relative_cmp (tf1->duration, + ==, + tf2->duration)) && + (GNUNET_TIME_relative_cmp (tf1->validity_granularity, + ==, + tf2->validity_granularity)) && + (GNUNET_TIME_relative_cmp (tf1->start_offset, + ==, + tf2->start_offset)) && + (tf1->kind == tf2->kind) ); +} + + +MHD_RESULT +TMH_private_post_token_families (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 }; + const char *kind = NULL; + bool no_valid_after = false; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("slug", + (const char **) &details.slug), + GNUNET_JSON_spec_string ("name", + (const char **) &details.name), + GNUNET_JSON_spec_string ("description", + (const char **) &details.description), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("description_i18n", + &details.description_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("extra_data", + &details.extra_data), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("valid_after", + &details.valid_after), + &no_valid_after), + GNUNET_JSON_spec_timestamp ("valid_before", + &details.valid_before), + GNUNET_JSON_spec_relative_time ("duration", + &details.duration), + GNUNET_JSON_spec_relative_time ("validity_granularity", + &details.validity_granularity), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("start_offset", + &details.start_offset), + NULL), + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_end () + }; + struct GNUNET_TIME_Timestamp now + = GNUNET_TIME_timestamp_get (); + + GNUNET_assert (NULL != mi); + { + 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; + } + } + if (no_valid_after) + details.valid_after = now; + + /* Ensure that valid_after is before valid_before */ + if (GNUNET_TIME_timestamp_cmp (details.valid_after, + >=, + details.valid_before)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "valid_after >= valid_before"); + } + + /* Ensure that duration exceeds rounding plus start_offset */ + if (GNUNET_TIME_relative_cmp (details.duration, + <, + GNUNET_TIME_relative_add (details. + validity_granularity, + details.start_offset)) + ) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "duration below validity_granularity plus start_offset"); + } + + if (0 == + strcmp (kind, + "discount")) + details.kind = TALER_MERCHANTDB_TFK_Discount; + else if (0 == + strcmp (kind, + "subscription")) + details.kind = TALER_MERCHANTDB_TFK_Subscription; + else + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + } + + if (NULL == details.description_i18n) + details.description_i18n = json_object (); + + if (! TALER_JSON_check_i18n (details.description_i18n)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "description_i18n"); + } + + if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, + !=, + details.validity_granularity) && + GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply ( + GNUNET_TIME_UNIT_DAYS, + 90), + !=, + details.validity_granularity) && + GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, + !=, + details.validity_granularity) && + GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS, + !=, + details.validity_granularity) && + GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, + !=, + details.validity_granularity) && + GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, + !=, + details.validity_granularity) && + GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, + !=, + details.validity_granularity) + ) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received invalid validity_granularity value: %s\n", + GNUNET_STRINGS_relative_time_to_string (details. + validity_granularity, + false)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "validity_granularity"); + } + + /* finally, interact with DB until no serialization error */ + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + /* Test if a token family of this id is known */ + struct TALER_MERCHANTDB_TokenFamilyDetails existing; + + TMH_db->preflight (TMH_db->cls); + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "/post tokenfamilies")) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->insert_token_family (TMH_db->cls, + mi->settings.id, + details.slug, + &details); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "insert_token_family returned %d\n", + (int) qs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_token_family"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + qs = TMH_db->lookup_token_family (TMH_db->cls, + mi->settings.id, + details.slug, + &existing); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "lookup_token_family returned %d\n", + (int) qs); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_token_family"); + case GNUNET_DB_STATUS_SOFT_ERROR: + TMH_db->rollback (TMH_db->cls); + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "lookup_token_family failed after insert_token_family failed"); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + { + bool eq; + + eq = token_families_equal (&details, + &existing); + TALER_MERCHANTDB_token_family_details_free (&existing); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return eq + ? TALER_MHD_reply_static ( + connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0) + : TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT, + details.slug); + } + } + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + qs = TMH_db->commit (TMH_db->cls); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + "insert_token_family"); + case GNUNET_DB_STATUS_SOFT_ERROR: + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + break; + } + if (GNUNET_DB_STATUS_SOFT_ERROR == qs) + TMH_db->rollback (TMH_db->cls); + else + break; + } /* for(i... MAX_RETRIES) */ + + GNUNET_JSON_parse_free (spec); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_post-private-tokenfamilies.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-tokenfamilies.h b/src/backend/taler-merchant-httpd_post-private-tokenfamilies.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2023 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-httpd_post-private-tokenfamilies.h + * @brief implementing POST /tokenfamilies request handling + * @author Christian Blättler + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_TOKEN_FAMILIES_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_TOKEN_FAMILIES_H +#include "taler-merchant-httpd.h" + + +/** + * Create a new token family. + * + * @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_token_families (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-transfers.c b/src/backend/taler-merchant-httpd_post-private-transfers.c @@ -0,0 +1,144 @@ +/* + This file is part of TALER + (C) 2014-2023, 2025, 2026 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-httpd_post-private-transfers.c + * @brief implement API for registering wire transfers + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include <jansson.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_dbevents.h> +#include "taler-merchant-httpd_get-exchanges.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_post-private-transfers.h" + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 5 + + +MHD_RESULT +TMH_private_post_transfers (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_FullPayto payto_uri; + const char *exchange_url; + struct TALER_WireTransferIdentifierRawP wtid; + struct TALER_Amount amount; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("credit_amount", + &amount), + GNUNET_JSON_spec_fixed_auto ("wtid", + &wtid), + TALER_JSON_spec_full_payto_uri ("payto_uri", + &payto_uri), + TALER_JSON_spec_web_url ("exchange_url", + &exchange_url), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + enum GNUNET_DB_QueryStatus qs; + bool no_instance; + bool no_account; + bool conflict; + + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + if (GNUNET_OK != res) + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "New inbound wire transfer over %s to %s from %s\n", + TALER_amount2s (&amount), + payto_uri.full_payto, + exchange_url); + + /* Check if transfer data is in database, if not, add it. */ + qs = TMH_db->insert_transfer (TMH_db->cls, + hc->instance->settings.id, + exchange_url, + &wtid, + &amount, + payto_uri, + 0 /* no bank serial known! */, + &no_instance, + &no_account, + &conflict); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_transfer"); + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "insert_transfer"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_assert (0); /* should be impossible */ + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + if (no_instance) + { + /* should be only possible if instance was concurrently deleted, + that's so theoretical we rather log as error... */ + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->instance->settings.id); + } + if (no_account) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_ACCOUNT_UNKNOWN, + payto_uri.full_payto); + } + if (conflict) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION, + NULL); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_post-private-transfers.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-transfers.h b/src/backend/taler-merchant-httpd_post-private-transfers.h @@ -0,0 +1,44 @@ +/* + This file is part of TALER + (C) 2014-2023 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/> +*/ +/** + * @file taler-merchant-httpd_post-private-transfers.h + * @brief headers for /track/transfer handler + * @author Christian Grothoff + * @author Marcello Stanisci + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_TRANSFERS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_TRANSFERS_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Manages a POST /private/transfers call. It calls the GET /transfers/$WTID + * offered by the exchange in order to obtain the set of transfers + * (of coins) associated with a given wire transfer. + * + * @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_transfers (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-units.c b/src/backend/taler-merchant-httpd_post-private-units.c @@ -0,0 +1,219 @@ +/* + 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-httpd_post-private-units.c + * @brief implement POST /private/units + * @author Bohdan Potuzhnyi + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-units.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_private_post_units (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_UnitDetails nud = { 0 }; + bool allow_fraction_missing = true; + bool unit_precision_missing = true; + bool unit_active_missing = true; + enum GNUNET_GenericReturnValue res; + enum GNUNET_DB_QueryStatus qs; + MHD_RESULT ret; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("unit", + (const char **) &nud.unit), + GNUNET_JSON_spec_string ("unit_name_long", + (const char **) &nud.unit_name_long), + GNUNET_JSON_spec_string ("unit_name_short", + (const char **) &nud.unit_name_short), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("unit_name_long_i18n", + &nud.unit_name_long_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_json ("unit_name_short_i18n", + &nud.unit_name_short_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("unit_allow_fraction", + &nud.unit_allow_fraction), + &allow_fraction_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("unit_precision_level", + &nud.unit_precision_level), + &unit_precision_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("unit_active", + &nud.unit_active), + &unit_active_missing), + GNUNET_JSON_spec_end () + }; + + + GNUNET_assert (NULL != mi); + res = TALER_MHD_parse_json_data (connection, + hc->request_body, + spec); + (void) rh; + + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return (GNUNET_NO == res) + ? MHD_YES + : MHD_NO; + } + + if (allow_fraction_missing) + { + nud.unit_allow_fraction = false; + nud.unit_precision_level = 0; + } + else + { + if (! nud.unit_allow_fraction) + { + nud.unit_precision_level = 0; + unit_precision_missing = false; + } + else if (unit_precision_missing) + { + nud.unit_precision_level = 0; + } + } + if (nud.unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_precision_level"); + goto cleanup; + } + if (unit_active_missing) + nud.unit_active = true; + + if (NULL == nud.unit_name_long_i18n) + nud.unit_name_long_i18n = json_object (); + if (NULL == nud.unit_name_short_i18n) + nud.unit_name_short_i18n = json_object (); + + if (! TALER_JSON_check_i18n (nud.unit_name_long_i18n)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_name_long_i18n"); + goto cleanup; + } + if (! TALER_JSON_check_i18n (nud.unit_name_short_i18n)) + { + GNUNET_break_op (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "unit_name_short_i18n"); + goto cleanup; + } + + nud.unit_builtin = false; + + { + bool no_instance = false; + bool conflict = false; + uint64_t unit_serial = 0; + + qs = TMH_db->insert_unit (TMH_db->cls, + mi->settings.id, + &nud, + &no_instance, + &conflict, + &unit_serial); + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_break (0); + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "insert_unit"); + goto cleanup; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + + if (no_instance) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + goto cleanup; + } + if (conflict) + { + ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN, + nud.unit); + goto cleanup; + } + + ret = TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + +cleanup: + if (NULL != nud.unit_name_long_i18n) + { + json_decref (nud.unit_name_long_i18n); + nud.unit_name_long_i18n = NULL; + } + if (NULL != nud.unit_name_short_i18n) + { + json_decref (nud.unit_name_short_i18n); + nud.unit_name_short_i18n = NULL; + } + GNUNET_JSON_parse_free (spec); + return ret; +} + + +/* end of taler-merchant-httpd_post-private-units.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-units.h b/src/backend/taler-merchant-httpd_post-private-units.h @@ -0,0 +1,33 @@ +/* + 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-httpd_post-private-units.h + * @brief implement POST /private/units + * @author Bohdan Potuzhnyi + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_UNITS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_UNITS_H + +#include "taler-merchant-httpd.h" + + +MHD_RESULT +TMH_private_post_units (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_post-private-units.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_post-private-webhooks.c b/src/backend/taler-merchant-httpd_post-private-webhooks.c @@ -0,0 +1,215 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_post-private-webhooks.c + * @brief implementing POST /webhooks request handling + * @author Priscilla HUANG + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_post-private-webhooks.h" +#include "taler-merchant-httpd_helper.h" +#include <taler/taler_json_lib.h> + + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + + +/** + * Check if the two webhooks are identical. + * + * @param w1 webhook to compare + * @param w2 other webhook to compare + * @return true if they are 'equal', false if not or of payto_uris is not an array + */ +static bool +webhooks_equal (const struct TALER_MERCHANTDB_WebhookDetails *w1, + const struct TALER_MERCHANTDB_WebhookDetails *w2) +{ + return ( (0 == strcmp (w1->event_type, + w2->event_type)) && + (0 == strcmp (w1->url, + w2->url)) && + (0 == strcmp (w1->http_method, + w2->http_method)) && + ( ( (NULL == w1->header_template) && + (NULL == w2->header_template) ) || + ( (NULL != w1->header_template) && + (NULL != w2->header_template) && + (0 == strcmp (w1->header_template, + w2->header_template)) ) ) && + ( ( (NULL == w1->body_template) && + (NULL == w2->body_template) ) || + ( (NULL != w1->body_template) && + (NULL != w2->body_template) && + (0 == strcmp (w1->body_template, + w2->body_template)) ) ) ); +} + + +MHD_RESULT +TMH_private_post_webhooks (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + struct TALER_MERCHANTDB_WebhookDetails wb = { 0 }; + const char *webhook_id; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("webhook_id", + &webhook_id), + GNUNET_JSON_spec_string ("event_type", + (const char **) &wb.event_type), + TALER_JSON_spec_web_url ("url", + (const char **) &wb.url), + GNUNET_JSON_spec_string ("http_method", + (const char **) &wb.http_method), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("header_template", + (const char **) &wb.header_template), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("body_template", + (const char **) &wb.body_template), + NULL), + GNUNET_JSON_spec_end () + }; + + GNUNET_assert (NULL != mi); + { + 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; + } + } + + + /* finally, interact with DB until no serialization error */ + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + /* Test if a webhook of this id is known */ + struct TALER_MERCHANTDB_WebhookDetails ewb; + + if (GNUNET_OK != + TMH_db->start (TMH_db->cls, + "/post webhooks")) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_START_FAILED, + NULL); + } + qs = TMH_db->lookup_webhook (TMH_db->cls, + mi->settings.id, + webhook_id, + &ewb); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + case GNUNET_DB_STATUS_SOFT_ERROR: + /* restart transaction */ + goto retry; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Good, we can proceed! */ + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* idempotency check: is ewb == wb? */ + { + bool eq; + + eq = webhooks_equal (&wb, + &ewb); + TALER_MERCHANTDB_webhook_details_free (&ewb); + TMH_db->rollback (TMH_db->cls); + GNUNET_JSON_parse_free (spec); + return eq + ? TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0) + : TALER_MHD_reply_with_error (connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS, + webhook_id); + } + } /* end switch (qs) */ + + qs = TMH_db->insert_webhook (TMH_db->cls, + mi->settings.id, + webhook_id, + &wb); + if (GNUNET_DB_STATUS_HARD_ERROR == qs) + { + TMH_db->rollback (TMH_db->cls); + break; + } + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + qs = TMH_db->commit (TMH_db->cls); + if (GNUNET_DB_STATUS_SOFT_ERROR != qs) + break; + } +retry: + GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); + TMH_db->rollback (TMH_db->cls); + } /* for RETRIES loop */ + GNUNET_JSON_parse_free (spec); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + (GNUNET_DB_STATUS_SOFT_ERROR == qs) + ? TALER_EC_GENERIC_DB_SOFT_FAILURE + : TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL); + } + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); +} + + +/* end of taler-merchant-httpd_post-private-webhooks.c */ diff --git a/src/backend/taler-merchant-httpd_post-private-webhooks.h b/src/backend/taler-merchant-httpd_post-private-webhooks.h @@ -0,0 +1,43 @@ +/* + This file is part of TALER + (C) 2022 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-httpd_post-private-webhooks.h + * @brief implementing POST /webhooks request handling + * @author Priscilla HUANG + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_WEBHOOKS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_POST_WEBHOOKS_H +#include "taler-merchant-httpd.h" + + +/** + * Generate a webhook entry. + * + * @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_webhooks (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-reports-ID.c b/src/backend/taler-merchant-httpd_post-reports-ID.c @@ -1,189 +0,0 @@ -/* - 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_post-reports-ID.c - * @brief implementation of POST /reports/$REPORT_ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_dispatcher.h" -#include "taler-merchant-httpd_post-reports-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_post_reports_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *report_id_str = hc->infix; - unsigned long long report_id; - const char *mime_type; - struct TALER_MERCHANT_ReportToken report_token; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ("report_token", - &report_token), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - char *instance_id; - char *data_source; - - { - char dummy; - - if (1 != sscanf (report_id_str, - "%llu%c", - &report_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "report_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; - } - } - - mime_type = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT); - if (NULL == mime_type) - mime_type = "application/json"; - qs = TMH_db->check_report (TMH_db->cls, - report_id, - &report_token, - mime_type, - &instance_id, - &data_source); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "check_report"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, - report_id_str); - } - - { - struct TMH_MerchantInstance *mi; - - mi = TMH_lookup_instance (instance_id); - if (NULL == mi) - { - /* Strange, we found the report but the instance of the - report is not known. This should basically be impossible - modulo maybe some transactional issue where the - instance was created, the report added *and* triggered - before this process was able to process the notification - about the new instance. Wild. */ - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, - report_id_str); - } - /* Rewrite request: force instance to match report */ - mi->rc++; - if (NULL != hc->instance) - TMH_instance_decref (hc->instance); - hc->instance = mi; - } - { - enum GNUNET_GenericReturnValue ret; - bool is_public; /* ignored: access control never applies for reports */ - char *q; - - /* This rewrites the request: force request handler to match report! */ - q = strchr (data_source, - '?'); - if (NULL != q) - { - /* Terminate URI at '?' for dispatching, parse - query parameters and add them to this connection */ - *q = '\0'; - for (char *tok = strtok (q + 1, - "&"); - NULL != tok; - tok = strtok (NULL, - "&")) - { - char *eq; - - eq = strchr (tok, - '='); - if (NULL == eq) - { - GNUNET_break (MHD_YES == - MHD_set_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - tok, - NULL)); - } - else - { - *eq = '\0'; - GNUNET_break (MHD_YES == - MHD_set_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - tok, - eq + 1)); - } - } - } - - hc->rh = NULL; /* just to make it clear: current one will NOT remain */ - ret = TMH_dispatch_request (hc, - data_source, - MHD_HTTP_METHOD_GET, - 0 == strcmp ("admin", - instance_id), - &is_public); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; - GNUNET_break (NULL != hc->rh); - } - GNUNET_free (instance_id); - GNUNET_free (data_source); - /* MHD will call the main handler again, which will now - dispatch into the _new_ handler callback we just installed! */ - return MHD_YES; -} diff --git a/src/backend/taler-merchant-httpd_post-reports-ID.h b/src/backend/taler-merchant-httpd_post-reports-ID.h @@ -1,41 +0,0 @@ -/* - 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 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-httpd_post-reports-ID.h - * @brief headers for POST /reports handler - * @author Christian Grothoff - */ -#ifndef TALER_EXCHANGE_HTTPD_POST_REPORTS_ID_H -#define TALER_EXCHANGE_HTTPD_POST_REPORTS_ID_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Handles a POST /reports/$REPORT_ID request. - * - * @param rc request context - * @param root uploaded JSON data - * @param args array of additional options (first must be the report_id) - * @return MHD result code - */ -MHD_RESULT -TMH_post_reports_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_post-reports-REPORT_ID.c b/src/backend/taler-merchant-httpd_post-reports-REPORT_ID.c @@ -0,0 +1,189 @@ +/* + 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 taler-merchant-httpd_post-reports-REPORT_ID.c + * @brief implementation of POST /reports/$REPORT_ID + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_dispatcher.h" +#include "taler-merchant-httpd_post-reports-REPORT_ID.h" +#include <taler/taler_json_lib.h> + + +MHD_RESULT +TMH_post_reports_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + const char *report_id_str = hc->infix; + unsigned long long report_id; + const char *mime_type; + struct TALER_MERCHANT_ReportToken report_token; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("report_token", + &report_token), + GNUNET_JSON_spec_end () + }; + enum GNUNET_DB_QueryStatus qs; + char *instance_id; + char *data_source; + + { + char dummy; + + if (1 != sscanf (report_id_str, + "%llu%c", + &report_id, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "report_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; + } + } + + mime_type = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT); + if (NULL == mime_type) + mime_type = "application/json"; + qs = TMH_db->check_report (TMH_db->cls, + report_id, + &report_token, + mime_type, + &instance_id, + &data_source); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check_report"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, + report_id_str); + } + + { + struct TMH_MerchantInstance *mi; + + mi = TMH_lookup_instance (instance_id); + if (NULL == mi) + { + /* Strange, we found the report but the instance of the + report is not known. This should basically be impossible + modulo maybe some transactional issue where the + instance was created, the report added *and* triggered + before this process was able to process the notification + about the new instance. Wild. */ + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, + report_id_str); + } + /* Rewrite request: force instance to match report */ + mi->rc++; + if (NULL != hc->instance) + TMH_instance_decref (hc->instance); + hc->instance = mi; + } + { + enum GNUNET_GenericReturnValue ret; + bool is_public; /* ignored: access control never applies for reports */ + char *q; + + /* This rewrites the request: force request handler to match report! */ + q = strchr (data_source, + '?'); + if (NULL != q) + { + /* Terminate URI at '?' for dispatching, parse + query parameters and add them to this connection */ + *q = '\0'; + for (char *tok = strtok (q + 1, + "&"); + NULL != tok; + tok = strtok (NULL, + "&")) + { + char *eq; + + eq = strchr (tok, + '='); + if (NULL == eq) + { + GNUNET_break (MHD_YES == + MHD_set_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + tok, + NULL)); + } + else + { + *eq = '\0'; + GNUNET_break (MHD_YES == + MHD_set_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + tok, + eq + 1)); + } + } + } + + hc->rh = NULL; /* just to make it clear: current one will NOT remain */ + ret = TMH_dispatch_request (hc, + data_source, + MHD_HTTP_METHOD_GET, + 0 == strcmp ("admin", + instance_id), + &is_public); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + GNUNET_break (NULL != hc->rh); + } + GNUNET_free (instance_id); + GNUNET_free (data_source); + /* MHD will call the main handler again, which will now + dispatch into the _new_ handler callback we just installed! */ + return MHD_YES; +} diff --git a/src/backend/taler-merchant-httpd_post-reports-REPORT_ID.h b/src/backend/taler-merchant-httpd_post-reports-REPORT_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 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-httpd_post-reports-REPORT_ID.h + * @brief headers for POST /reports handler + * @author Christian Grothoff + */ +#ifndef TALER_EXCHANGE_HTTPD_POST_REPORTS_ID_H +#define TALER_EXCHANGE_HTTPD_POST_REPORTS_ID_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Handles a POST /reports/$REPORT_ID request. + * + * @param rc request context + * @param root uploaded JSON data + * @param args array of additional options (first must be the report_id) + * @return MHD result code + */ +MHD_RESULT +TMH_post_reports_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +#endif diff --git a/src/backend/taler-merchant-httpd_post-templates-ID.c b/src/backend/taler-merchant-httpd_post-templates-ID.c @@ -1,1782 +0,0 @@ -/* - This file is part of TALER - (C) 2022-2026 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-httpd_post-using-templates.c - * @brief implementing POST /using-templates request handling - * @author Priscilla HUANG - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_post-templates-ID.h" -#include "taler-merchant-httpd_private-post-orders.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler/taler_merchant_util.h" -#include <taler/taler_json_lib.h> -#include <regex.h> - - -/** - * Item selected from inventory_selection. - */ -struct InventoryTemplateItemContext -{ - /** - * Product ID as referenced in inventory. - */ - const char *product_id; - - /** - * Unit quantity string as provided by the client. - */ - const char *unit_quantity; - - /** - * Parsed integer quantity. - */ - uint64_t quantity_value; - - /** - * Parsed fractional quantity. - */ - uint32_t quantity_frac; - - /** - * Product details from the DB (includes price array). - */ - struct TALER_MERCHANTDB_ProductDetails pd; - - /** - * Categories referenced by the product. - */ - uint64_t *categories; - - /** - * Length of @e categories. - */ - size_t num_categories; -}; - - -/** - * Our context. - */ -enum UsePhase -{ - /** - * Parse request payload into context fields. - */ - USE_PHASE_PARSE_REQUEST, - - /** - * Fetch template details from the database. - */ - USE_PHASE_LOOKUP_TEMPLATE, - - /** - * Parse template. - */ - USE_PHASE_PARSE_TEMPLATE, - - /** - * Load additional details (like products and - * categories) needed for verification and - * price computation. - */ - USE_PHASE_DB_FETCH, - - /** - * Validate request and template compatibility. - */ - USE_PHASE_VERIFY, - - /** - * Compute price of the order. - */ - USE_PHASE_COMPUTE_PRICE, - - /** - * Handle tip. - */ - USE_PHASE_CHECK_TIP, - - /** - * Check if client-supplied total amount matches - * our calculation (if we did any). - */ - USE_PHASE_CHECK_TOTAL, - - /** - * Construct the internal order request body. - */ - USE_PHASE_CREATE_ORDER, - - /** - * Submit the order to the shared order handler. - */ - USE_PHASE_SUBMIT_ORDER, - - /** - * Finished successfully with MHD_YES. - */ - USE_PHASE_FINISHED_MHD_YES, - - /** - * Finished with MHD_NO. - */ - USE_PHASE_FINISHED_MHD_NO -}; - -struct UseContext -{ - /** - * Context for our handler. - */ - struct TMH_HandlerContext *hc; - - /** - * Internal handler context we are passing into the - * POST /private/orders handler. - */ - struct TMH_HandlerContext ihc; - - /** - * Phase we are currently in. - */ - enum UsePhase phase; - - /** - * Template type from the contract. - */ - enum TALER_MERCHANT_TemplateType template_type; - - /** - * Information set in the #USE_PHASE_PARSE_REQUEST phase. - */ - struct - { - /** - * Summary override from request, if any. - */ - const char *summary; - - /** - * Amount provided by the client. - */ - struct TALER_Amount amount; - - /** - * Tip provided by the client. - */ - struct TALER_Amount tip; - - /** - * True if @e amount was not provided. - */ - bool no_amount; - - /** - * True if @e tip was not provided. - */ - bool no_tip; - - /** - * Parsed fields for inventory templates. - */ - struct - { - /** - * Selected products from inventory_selection. - */ - struct InventoryTemplateItemContext *items; - - /** - * Length of @e items. - */ - unsigned int items_len; - - } inventory; - - /** - * Request details if this is a paivana instantiation. - */ - struct - { - - /** - * Target website for the request. - */ - const char *website; - - /** - * Unique client identifier, consisting of - * current time, "-", and the hash of a nonce, - * the website and the current time. - */ - const char *paivana_id; - - } paivana; - - } parse_request; - - /** - * Information set in the #USE_PHASE_LOOKUP_TEMPLATE phase. - */ - struct - { - - /** - * Our template details from the DB. - */ - struct TALER_MERCHANTDB_TemplateDetails etp; - - } lookup_template; - - /** - * Information set in the #USE_PHASE_PARSE_TEMPLATE phase. - */ - struct TALER_MERCHANT_TemplateContract template_contract; - - /** - * Information set in the #USE_PHASE_COMPUTE_PRICE phase. - */ - struct - { - - /** - * Per-currency totals across selected products (without tips). - */ - struct TALER_Amount *totals; - - /** - * Length of @e totals. - */ - unsigned int totals_len; - - /** - * Array of payment choices, used with Paviana. - */ - json_t *choices; - - } compute_price; - -}; - - -/** - * Clean up inventory items. - * - * @param items_len length of @a items - * @param[in] items item array to free - */ -static void -cleanup_inventory_items (unsigned int items_len, - struct InventoryTemplateItemContext items[static - items_len]) -{ - for (unsigned int i = 0; i < items_len; i++) - { - struct InventoryTemplateItemContext *item = &items[i]; - - TALER_MERCHANTDB_product_details_free (&item->pd); - GNUNET_free (item->categories); - } - GNUNET_free (items); -} - - -/** - * Clean up a `struct UseContext *` - * - * @param[in] cls a `struct UseContext *` - */ -static void -cleanup_use_context (void *cls) -{ - struct UseContext *uc = cls; - - TALER_MERCHANTDB_template_details_free (&uc->lookup_template.etp); - if (NULL != - uc->parse_request.inventory.items) - cleanup_inventory_items (uc->parse_request.inventory.items_len, - uc->parse_request.inventory.items); - GNUNET_free (uc->compute_price.totals); - uc->compute_price.totals_len = 0; - json_decref (uc->compute_price.choices); - if (NULL != uc->ihc.cc) - uc->ihc.cc (uc->ihc.ctx); - GNUNET_free (uc->ihc.infix); - json_decref (uc->ihc.request_body); - GNUNET_free (uc); -} - - -/** - * Finalize a template use request. - * - * @param[in,out] uc use context - * @param ret handler return value - */ -static void -use_finalize (struct UseContext *uc, - MHD_RESULT ret) -{ - uc->phase = (MHD_YES == ret) - ? USE_PHASE_FINISHED_MHD_YES - : USE_PHASE_FINISHED_MHD_NO; -} - - -/** - * Finalize after JSON parsing result. - * - * @param[in,out] uc use context - * @param res parse result - */ -static void -use_finalize_parse (struct UseContext *uc, - enum GNUNET_GenericReturnValue res) -{ - GNUNET_assert (GNUNET_OK != res); - use_finalize (uc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); -} - - -/** - * Reply with error and finalize the request. - * - * @param[in,out] uc use context - * @param http_status HTTP status code - * @param ec error code - * @param detail error detail - */ -static void -use_reply_with_error (struct UseContext *uc, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *detail) -{ - MHD_RESULT mret; - - mret = TALER_MHD_reply_with_error (uc->hc->connection, - http_status, - ec, - detail); - use_finalize (uc, - mret); -} - - -/* ***************** USE_PHASE_PARSE_REQUEST **************** */ - -/** - * Parse request data for inventory templates. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -parse_using_templates_inventory_request ( - struct UseContext *uc) -{ - const json_t *inventory_selection; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("inventory_selection", - &inventory_selection), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - GNUNET_assert (NULL == uc->ihc.request_body); - res = TALER_MHD_parse_json_data (uc->hc->connection, - uc->hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_finalize_parse (uc, - res); - return GNUNET_SYSERR; - } - - if ( (! uc->parse_request.no_amount) && - (! TMH_test_exchange_configured_for_currency ( - uc->parse_request.amount.currency)) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - "Currency is not supported by backend"); - return GNUNET_SYSERR; - } - - for (size_t i = 0; i < json_array_size (inventory_selection); i++) - { - struct InventoryTemplateItemContext item = { 0 }; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("product_id", - &item.product_id), - GNUNET_JSON_spec_string ("quantity", - &item.unit_quantity), - GNUNET_JSON_spec_end () - }; - const char *err_name; - unsigned int err_line; - - res = GNUNET_JSON_parse (json_array_get (inventory_selection, - i), - ispec, - &err_name, - &err_line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_selection"); - return GNUNET_SYSERR; - } - - GNUNET_array_append (uc->parse_request.inventory.items, - uc->parse_request.inventory.items_len, - item); - } - return GNUNET_OK; -} - - -/** - * Parse request data for paivana templates. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -parse_using_templates_paivana_request ( - struct UseContext *uc) -{ - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("website", - &uc->parse_request.paivana.website), - GNUNET_JSON_spec_string ("paivana_id", - &uc->parse_request.paivana.paivana_id), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - struct GNUNET_HashCode sh; - unsigned long long tv; - const char *dash; - - GNUNET_assert (NULL == uc->ihc.request_body); - res = TALER_MHD_parse_json_data (uc->hc->connection, - uc->hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_finalize_parse (uc, - res); - return GNUNET_SYSERR; - } - if (1 != - sscanf (uc->parse_request.paivana.paivana_id, - "%llu-", - &tv)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "paivana_id"); - return GNUNET_SYSERR; - } - dash = strchr (uc->parse_request.paivana.paivana_id, - '-'); - GNUNET_assert (NULL != dash); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (dash + 1, - strlen (dash + 1), - &sh, - sizeof (sh))) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "paivana_id"); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Main function for the #USE_PHASE_PARSE_REQUEST. - * - * @param[in,out] uc context to update - */ -static void -handle_phase_parse_request ( - struct UseContext *uc) -{ - const char *template_type = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("template_type", - &template_type), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("tip", - &uc->parse_request.tip), - &uc->parse_request.no_tip), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("summary", - &uc->parse_request.summary), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("amount", - &uc->parse_request.amount), - &uc->parse_request.no_amount), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (uc->hc->connection, - uc->hc->request_body, - spec); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - use_finalize_parse (uc, - res); - return; - } - if (NULL == template_type) - template_type = "fixed-order"; - uc->template_type - = TALER_MERCHANT_template_type_from_string ( - template_type); - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - /* nothig left to do */ - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - res = parse_using_templates_paivana_request (uc); - if (GNUNET_OK == res) - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - res = parse_using_templates_inventory_request (uc); - if (GNUNET_OK == res) - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - break; - } - GNUNET_break (0); - use_reply_with_error ( - uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "template_type"); -} - - -/* ***************** USE_PHASE_LOOKUP_TEMPLATE **************** */ - -/** - * Main function for the #USE_PHASE_LOOKUP_TEMPLATE. - * - * @param[in,out] uc context to update - */ -static void -handle_phase_lookup_template ( - struct UseContext *uc) -{ - struct TMH_MerchantInstance *mi = uc->hc->instance; - const char *template_id = uc->hc->infix; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_template (TMH_db->cls, - mi->settings.id, - template_id, - &uc->lookup_template.etp); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_template"); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* this should be impossible (single select) */ - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_template"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* template not found! */ - use_reply_with_error (uc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - template_id); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* all good */ - break; - } - if (uc->template_type != - TALER_MERCHANT_template_type_from_contract ( - uc->lookup_template.etp.template_contract)) - { - GNUNET_break_op (0); - use_reply_with_error ( - uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_TYPE, - "template_contract has different type"); - return; - } - uc->phase++; -} - - -/* ***************** USE_PHASE_PARSE_TEMPLATE **************** */ - - -/** - * Parse template. - * - * @param[in,out] uc use context - */ -static void -handle_phase_template_contract (struct UseContext *uc) -{ - const char *err_name; - enum GNUNET_GenericReturnValue res; - - res = TALER_MERCHANT_template_contract_parse ( - uc->lookup_template.etp.template_contract, - &uc->template_contract, - &err_name); - if (GNUNET_OK != res) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - err_name); - return; - } - uc->phase++; -} - - -/* ***************** USE_PHASE_DB_FETCH **************** */ - -/** - * Fetch DB data for inventory templates. - * - * @param[in,out] uc use context - */ -static void -handle_phase_db_fetch (struct UseContext *uc) -{ - struct TMH_MerchantInstance *mi = uc->hc->instance; - - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - item->product_id, - &item->pd, - &item->num_categories, - &item->categories); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product"); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - use_reply_with_error (uc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - item->product_id); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - uc->phase++; -} - - -/* *************** Helpers for USE_PHASE_VERIFY ***************** */ - -/** - * Check if the given product ID appears in the array of allowed_products. - * - * @param allowed_products JSON array of product IDs allowed by the template, may be NULL - * @param product_id product ID to check - * @return true if the product ID is in the list - */ -static bool -product_id_allowed (const json_t *allowed_products, - const char *product_id) -{ - const json_t *entry; - size_t idx; - - if (NULL == allowed_products) - return false; - json_array_foreach ((json_t *) allowed_products, idx, entry) - { - if (! json_is_string (entry)) - { - GNUNET_break (0); - continue; - } - if (0 == strcmp (json_string_value (entry), - product_id)) - return true; - } - return false; -} - - -/** - * Check if any product category is in the selected_categories list. - * - * @param allowed_categories JSON array of categories allowed by the template, may be NULL - * @param num_categories length of @a categories - * @param categories list of categories of the selected product - * @return true if any category of the product is in the list of allowed categories matches - */ -static bool -category_allowed (const json_t *allowed_categories, - size_t num_categories, - const uint64_t categories[num_categories]) -{ - const json_t *entry; - size_t idx; - - if (NULL == allowed_categories) - return false; - json_array_foreach ((json_t *) allowed_categories, - idx, - entry) - { - uint64_t selected_id; - - if (! json_is_integer (entry)) - { - GNUNET_break (0); - continue; - } - if (0 > json_integer_value (entry)) - { - GNUNET_break (0); - continue; - } - selected_id = (uint64_t) json_integer_value (entry); - for (size_t i = 0; i < num_categories; i++) - { - if (categories[i] == selected_id) - return true; - } - } - return false; -} - - -/** - * Verify request data for inventory templates. - * Checks that the selected products are allowed - * for this template. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -verify_using_templates_inventory (struct UseContext *uc) -{ - if (uc->template_contract.details.inventory.choose_one && - (1 != uc->parse_request.inventory.items_len)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_selection"); - return GNUNET_SYSERR; - } - if (uc->template_contract.details.inventory.selected_all) - return GNUNET_OK; - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - const char *eparam = NULL; - - if (GNUNET_OK != - TALER_MERCHANT_vk_process_quantity_inputs ( - TALER_MERCHANT_VK_QUANTITY, - item->pd.allow_fractional_quantity, - true, - 0, - false, - item->unit_quantity, - &item->quantity_value, - &item->quantity_frac, - &eparam)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - eparam); - return GNUNET_SYSERR; - } - - if (0 == item->pd.price_array_length) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "price_array"); - return GNUNET_SYSERR; - } - } - - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - - if (product_id_allowed (uc->template_contract.details.inventory. - selected_products, - item->product_id)) - continue; - if (category_allowed ( - uc->template_contract.details.inventory.selected_categories, - item->num_categories, - item->categories)) - continue; - GNUNET_break_op (0); - use_reply_with_error ( - uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_PRODUCT, - item->product_id); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Verify request data for fixed-order templates. - * As here we cannot compute the total amount, either - * the template or the client request must provide it. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -verify_using_templates_fixed ( - struct UseContext *uc) -{ - if ( (! uc->parse_request.no_amount) && - (! uc->template_contract.no_amount) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, - NULL); - return GNUNET_SYSERR; - } - if (uc->parse_request.no_amount && - uc->template_contract.no_amount) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT, - NULL); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Verify request data for paivana templates. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -verify_using_templates_paivana ( - struct UseContext *uc) -{ - if (NULL != uc->template_contract.details.paivana.website_regex) - { - regex_t ex; - bool allowed = false; - - if (0 != regcomp (&ex, - uc->template_contract.details.paivana.website_regex, - REG_NOSUB | REG_EXTENDED)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 == - regexec (&ex, - uc->parse_request.paivana.website, - 0, NULL, - 0)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Website `%s' allowed by template\n", - uc->parse_request.paivana.website); - allowed = true; - } - regfree (&ex); - if (! allowed) - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Verify that the client request is structurally acceptable for the specified - * template. Does NOT check the total amount being reasonable. - * - * @param[in,out] uc use context - */ -static void -handle_phase_verify ( - struct UseContext *uc) -{ - enum GNUNET_GenericReturnValue res = GNUNET_SYSERR; - - if ( (NULL != uc->parse_request.summary) && - (NULL != uc->template_contract.summary) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT, - NULL); - return; - } - if ( (NULL == uc->parse_request.summary) && - (NULL == uc->template_contract.summary) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY, - NULL); - return; - } - if ( (! uc->parse_request.no_amount) && - (NULL != uc->template_contract.currency) && - (0 != strcasecmp (uc->template_contract.currency, - uc->parse_request.amount.currency)) ) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - uc->template_contract.currency); - return; - } - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - res = verify_using_templates_fixed (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - res = verify_using_templates_paivana (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - res = verify_using_templates_inventory (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - if (GNUNET_OK == res) - uc->phase++; -} - - -/* ***************** USE_PHASE_COMPUTE_PRICE **************** */ - - -/** - * Compute the line total for a product based on quantity. - * - * @param unit_price price per unit - * @param quantity integer quantity - * @param quantity_frac fractional quantity (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) - * @param[out] line_total resulting line total - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -compute_line_total (const struct TALER_Amount *unit_price, - uint64_t quantity, - uint32_t quantity_frac, - struct TALER_Amount *line_total) -{ - struct TALER_Amount tmp; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (unit_price->currency, - line_total)); - if ( (0 != quantity) && - (0 > - TALER_amount_multiply (line_total, - unit_price, - (uint32_t) quantity)) ) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - if (0 == quantity_frac) - return GNUNET_OK; - if (0 > - TALER_amount_multiply (&tmp, - unit_price, - quantity_frac)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - TALER_amount_divide (&tmp, - &tmp, - TALER_MERCHANT_UNIT_FRAC_BASE); - if (0 > - TALER_amount_add (line_total, - line_total, - &tmp)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Find the price of the given @a item in the specified - * @a currency. - * - * @param currency currency to search price in - * @param item item to check prices of - * @return NULL if a suitable price was not found - */ -static const struct TALER_Amount * -find_item_price_in_currency ( - const char *currency, - const struct InventoryTemplateItemContext *item) -{ - for (size_t j = 0; j < item->pd.price_array_length; j++) - { - if (0 == strcasecmp (item->pd.price_array[j].currency, - currency)) - return &item->pd.price_array[j]; - } - return NULL; -} - - -/** - * Compute totals for all currencies shared across selected products. - * - * @param[in,out] uc use context - * @return #GNUNET_OK on success (including no price due to no items) - * #GNUNET_NO if we could not find a price in any accepted currency - * for all selected products - * #GNUNET_SYSERR on arithmetic issues (internal error) - */ -static enum GNUNET_GenericReturnValue -compute_totals_per_currency (struct UseContext *uc) -{ - const struct InventoryTemplateItemContext *items - = uc->parse_request.inventory.items; - unsigned int items_len = uc->parse_request.inventory.items_len; - - if (0 == items_len) - return GNUNET_OK; - for (size_t i = 0; i < items[0].pd.price_array_length; i++) - { - const struct TALER_Amount *price - = &items[0].pd.price_array[i]; - struct TALER_Amount zero; - - if (! TMH_test_exchange_configured_for_currency (price->currency)) - continue; - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (price->currency, - &zero)); - GNUNET_array_append (uc->compute_price.totals, - uc->compute_price.totals_len, - zero); - } - if (0 == uc->compute_price.totals_len) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No currency supported by our configuration in which we have prices for first selected product!\n"); - return GNUNET_NO; - } - /* Loop through items, ensure each currency exists and sum totals. */ - for (unsigned int i = 0; i < items_len; i++) - { - const struct InventoryTemplateItemContext *item = &items[i]; - unsigned int c = 0; - - while (c < uc->compute_price.totals_len) - { - struct TALER_Amount *total = &uc->compute_price.totals[c]; - const struct TALER_Amount *unit_price; - struct TALER_Amount line_total; - - unit_price = find_item_price_in_currency (total->currency, - item); - if (NULL == unit_price) - { - /* Drop the currency: we have no price in one of - the selected products */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Product `%s' has no price in %s: dropping currency\n", - item->product_id, - total->currency); - *total = uc->compute_price.totals[--uc->compute_price.totals_len]; - continue; - } - if (GNUNET_OK != - compute_line_total (unit_price, - item->quantity_value, - item->quantity_frac, - &line_total)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (total, - total, - &line_total)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - c++; - } - } - if (0 == uc->compute_price.totals_len) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No currency available in which we have prices for all selected products!\n"); - GNUNET_free (uc->compute_price.totals); - } - return (0 == uc->compute_price.totals_len) - ? GNUNET_NO - : GNUNET_OK; -} - - -/** - * Compute total for only the given @a currency. - * - * @param items_len length of @a items - * @param items inventory items - * @param currency currency to total - * @param[out] total computed total - * @return #GNUNET_OK on success - * #GNUNET_NO if we could not find a price in any accepted currency - * for all selected products - * #GNUNET_SYSERR on arithmetic issues (internal error) - */ -static enum GNUNET_GenericReturnValue -compute_inventory_total (unsigned int items_len, - const struct InventoryTemplateItemContext *items, - const char *currency, - struct TALER_Amount *total) -{ - GNUNET_assert (NULL != currency); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (currency, - total)); - for (unsigned int i = 0; i < items_len; i++) - { - const struct InventoryTemplateItemContext *item = &items[i]; - const struct TALER_Amount *unit_price; - struct TALER_Amount line_total; - - unit_price = find_item_price_in_currency (currency, - item); - if (NULL == unit_price) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "compute_inventory_total: no price in %s for product `%s'\n", - currency, - item->product_id); - return GNUNET_NO; - } - if (GNUNET_OK != - compute_line_total (unit_price, - item->quantity_value, - item->quantity_frac, - &line_total)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "compute_inventory_total: line total failed for %s in %s\n", - item->product_id, - currency); - return GNUNET_SYSERR; - } - if (0 > - TALER_amount_add (total, - total, - &line_total)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - } - return GNUNET_OK; -} - - -/** - * Compute total price. - * - * @param[in,out] uc use context - */ -static void -handle_phase_compute_price (struct UseContext *uc) -{ - const char *primary_currency; - enum GNUNET_GenericReturnValue ret; - - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - uc->compute_price.totals - = GNUNET_new (struct TALER_Amount); - uc->compute_price.totals_len - = 1; - if (uc->parse_request.no_amount) - { - GNUNET_assert (! uc->template_contract.no_amount); - *uc->compute_price.totals - = uc->template_contract.amount; - } - else - { - GNUNET_assert (uc->template_contract.no_amount); - *uc->compute_price.totals - = uc->parse_request.amount; - } - uc->phase++; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - /* handled below */ - break; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - { - const struct TALER_MERCHANT_TemplateContractPaivana *tcp - = &uc->template_contract.details.paivana; - json_t *choices; - - choices = json_array (); - GNUNET_assert (NULL != choices); - for (size_t i = 0; i < tcp->choices_len; i++) - { - /* Make deep copy, we're going to MODIFY it! */ - struct TALER_MERCHANT_ContractChoice choice - = tcp->choices[i]; - - choice.no_tip = uc->parse_request.no_tip; - if (! uc->parse_request.no_tip) - { - if (GNUNET_YES != - TALER_amount_cmp_currency (&choice.amount, - &uc->parse_request.tip)) - continue; /* tip does not match choice currency */ - choice.tip = uc->parse_request.tip; - if (0 > - TALER_amount_add (&choice.amount, - &choice.amount, - &uc->parse_request.tip)) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "tip"); - return; - } - } - GNUNET_assert (0 == - json_array_append_new ( - choices, - TALER_MERCHANT_json_from_contract_choice (&choice, - true))); - } - if (0 == json_array_size (choices)) - { - GNUNET_break_op (0); - json_decref (choices); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, - "tip"); - return; - } - uc->compute_price.choices = choices; - } - /* Note: we already did the tip and pricing - fully here, so we skip these phases. */ - uc->phase = USE_PHASE_CREATE_ORDER; - return; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - primary_currency = uc->template_contract.currency; - if (! uc->parse_request.no_amount) - primary_currency = uc->parse_request.amount.currency; - if (! uc->parse_request.no_tip) - primary_currency = uc->parse_request.tip.currency; - if (NULL == primary_currency) - { - ret = compute_totals_per_currency (uc); - } - else - { - uc->compute_price.totals - = GNUNET_new (struct TALER_Amount); - uc->compute_price.totals_len - = 1; - ret = compute_inventory_total ( - uc->parse_request.inventory.items_len, - uc->parse_request.inventory.items, - primary_currency, - uc->compute_price.totals); - } - if (GNUNET_SYSERR == ret) - { - use_reply_with_error ( - uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "calculation of currency totals failed"); - return; - } - if (GNUNET_NO == ret) - { - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, - NULL); - return; - } - - uc->phase++; -} - - -/* ***************** USE_PHASE_CHECK_TIP **************** */ - - -/** - * Check that tip specified is reasonable and add to total. - * - * @param[in,out] uc use context - */ -static void -handle_phase_check_tip (struct UseContext *uc) -{ - struct TALER_Amount *total_amount; - - if (uc->parse_request.no_tip) - { - uc->phase++; - return; - } - if (0 == uc->compute_price.totals_len) - { - if (! TMH_test_exchange_configured_for_currency ( - uc->parse_request.tip.currency)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - "Tip currency is not supported by backend"); - return; - } - uc->compute_price.totals - = GNUNET_new (struct TALER_Amount); - uc->compute_price.totals_len - = 1; - *uc->compute_price.totals - = uc->parse_request.tip; - uc->phase++; - return; - } - GNUNET_assert (1 == uc->compute_price.totals_len); - total_amount = &uc->compute_price.totals[0]; - if (GNUNET_YES != - TALER_amount_cmp_currency (&uc->parse_request.tip, - total_amount)) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - uc->parse_request.tip.currency); - return; - } - if (0 > - TALER_amount_add (total_amount, - total_amount, - &uc->parse_request.tip)) - { - GNUNET_break (0); - use_reply_with_error (uc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, - "tip"); - return; - } - uc->phase++; -} - - -/* ***************** USE_PHASE_CHECK_TOTAL **************** */ - -/** - * Check that if the client specified a total, - * it matches our own calculation. - * - * @param[in,out] uc use context - */ -static void -handle_phase_check_total (struct UseContext *uc) -{ - GNUNET_assert (1 <= uc->compute_price.totals_len); - if (! uc->parse_request.no_amount) - { - GNUNET_assert (1 == uc->compute_price.totals_len); - GNUNET_assert (GNUNET_YES == - TALER_amount_cmp_currency (&uc->parse_request.amount, - &uc->compute_price.totals[0])); - if (0 != - TALER_amount_cmp (&uc->parse_request.amount, - &uc->compute_price.totals[0])) - { - GNUNET_break_op (0); - use_reply_with_error (uc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, - TALER_amount2s (&uc->compute_price.totals[0])); - return; - } - } - uc->phase++; -} - - -/* ***************** USE_PHASE_CREATE_ORDER **************** */ - - -/** - * Create order request for inventory templates. - * - * @param[in,out] uc use context - */ -static void -create_using_templates_inventory (struct UseContext *uc) -{ - json_t *inventory_products; - json_t *choices; - - inventory_products = json_array (); - GNUNET_assert (NULL != inventory_products); - for (unsigned int i = 0; - i < uc->parse_request.inventory.items_len; - i++) - { - const struct InventoryTemplateItemContext *item = - &uc->parse_request.inventory.items[i]; - - GNUNET_assert (0 == - json_array_append_new ( - inventory_products, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("product_id", - item->product_id), - GNUNET_JSON_pack_string ("unit_quantity", - item->unit_quantity)))); - } - choices = json_array (); - GNUNET_assert (NULL != choices); - for (unsigned int i = 0; - i < uc->compute_price.totals_len; - i++) - { - GNUNET_assert (0 == - json_array_append_new ( - choices, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &uc->compute_price.totals[i]), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - uc->parse_request.no_tip - ? NULL - : &uc->parse_request.tip)) - ))); - } - - uc->ihc.request_body - = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_id", - uc->lookup_template.etp.otp_id)), - GNUNET_JSON_pack_array_steal ("inventory_products", - inventory_products), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("version", - 1), - GNUNET_JSON_pack_array_steal ("choices", - choices), - GNUNET_JSON_pack_string ("summary", - NULL == uc->parse_request.summary - ? uc->template_contract.summary - : uc->parse_request.summary)))); -} - - -/** - * Create order request for fixed-order templates. - * - * @param[in,out] uc use context - */ -static void -create_using_templates_fixed (struct UseContext *uc) -{ - uc->ihc.request_body - = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_id", - uc->lookup_template.etp.otp_id)), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ( - "amount", - &uc->compute_price.totals[0]), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - uc->parse_request.no_tip - ? NULL - : &uc->parse_request.tip)), - GNUNET_JSON_pack_string ( - "summary", - NULL == uc->parse_request.summary - ? uc->template_contract.summary - : uc->parse_request.summary)))); -} - - -/** - * Create order request for paivana templates. - * - * @param[in,out] uc use context - */ -static void -create_using_templates_paivana (struct UseContext *uc) -{ - uc->ihc.request_body - = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ( - "session_id", - uc->parse_request.paivana.paivana_id), - GNUNET_JSON_pack_object_steal ( - "order", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("version", - 1), - GNUNET_JSON_pack_array_incref ("choices", - uc->compute_price.choices), - GNUNET_JSON_pack_string ( - "summary", - NULL == uc->parse_request.summary - ? uc->template_contract.summary - : uc->parse_request.summary), - GNUNET_JSON_pack_string ("fulfillment_url", - uc->parse_request.paivana.website)))); -} - - -static void -handle_phase_create_order (struct UseContext *uc) -{ - GNUNET_assert (NULL == uc->ihc.request_body); - switch (uc->template_type) - { - case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: - create_using_templates_fixed (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - create_using_templates_paivana (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - create_using_templates_inventory (uc); - break; - case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: - GNUNET_assert (0); - } - uc->phase++; -} - - -/* ***************** Main handler **************** */ - -MHD_RESULT -TMH_post_using_templates_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct UseContext *uc = hc->ctx; - - (void) rh; - if (NULL == uc) - { - uc = GNUNET_new (struct UseContext); - uc->hc = hc; - hc->ctx = uc; - hc->cc = &cleanup_use_context; - uc->ihc.instance = hc->instance; - uc->phase = USE_PHASE_PARSE_REQUEST; - uc->template_type = TALER_MERCHANT_TEMPLATE_TYPE_INVALID; - } - - while (1) - { - switch (uc->phase) - { - case USE_PHASE_PARSE_REQUEST: - handle_phase_parse_request (uc); - break; - case USE_PHASE_LOOKUP_TEMPLATE: - handle_phase_lookup_template (uc); - break; - case USE_PHASE_PARSE_TEMPLATE: - handle_phase_template_contract (uc); - break; - case USE_PHASE_DB_FETCH: - handle_phase_db_fetch (uc); - break; - case USE_PHASE_VERIFY: - handle_phase_verify (uc); - break; - case USE_PHASE_COMPUTE_PRICE: - handle_phase_compute_price (uc); - break; - case USE_PHASE_CHECK_TIP: - handle_phase_check_tip (uc); - break; - case USE_PHASE_CHECK_TOTAL: - handle_phase_check_total (uc); - break; - case USE_PHASE_CREATE_ORDER: - handle_phase_create_order (uc); - break; - case USE_PHASE_SUBMIT_ORDER: - return TMH_private_post_orders ( - NULL, /* not even used */ - connection, - &uc->ihc); - case USE_PHASE_FINISHED_MHD_YES: - return MHD_YES; - case USE_PHASE_FINISHED_MHD_NO: - return MHD_NO; - } - } -} diff --git a/src/backend/taler-merchant-httpd_post-templates-ID.h b/src/backend/taler-merchant-httpd_post-templates-ID.h @@ -1,40 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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/> -*/ -/** - * @file taler-merchant-httpd_post-using-templates.h - * @brief headers for POST /using-templates handler - * @author Priscilla Huang - */ -#ifndef TALER_MERCHANT_HTTPD_POST_TEMPLATES_ID_H -#define TALER_MERCHANT_HTTPD_POST_TEMPLATES_ID_H - -#include "taler-merchant-httpd.h" - -/** - * Generate a template that customer can use it. Returns an MHD_RESULT. - * - * @param rh details about this request 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_post_using_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c @@ -0,0 +1,1783 @@ +/* + This file is part of TALER + (C) 2022-2026 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-httpd_post-templates-TEMPLATE_ID.c + * @brief implementing POST /using-templates request handling + * @author Priscilla HUANG + * @author Christian Grothoff + */ +#include "taler/platform.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_post-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_post-private-orders.h" +#include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_get-exchanges.h" +#include "taler/taler_merchant_util.h" +#include <taler/taler_json_lib.h> +#include <regex.h> + + +/** + * Item selected from inventory_selection. + */ +struct InventoryTemplateItemContext +{ + /** + * Product ID as referenced in inventory. + */ + const char *product_id; + + /** + * Unit quantity string as provided by the client. + */ + const char *unit_quantity; + + /** + * Parsed integer quantity. + */ + uint64_t quantity_value; + + /** + * Parsed fractional quantity. + */ + uint32_t quantity_frac; + + /** + * Product details from the DB (includes price array). + */ + struct TALER_MERCHANTDB_ProductDetails pd; + + /** + * Categories referenced by the product. + */ + uint64_t *categories; + + /** + * Length of @e categories. + */ + size_t num_categories; +}; + + +/** + * Our context. + */ +enum UsePhase +{ + /** + * Parse request payload into context fields. + */ + USE_PHASE_PARSE_REQUEST, + + /** + * Fetch template details from the database. + */ + USE_PHASE_LOOKUP_TEMPLATE, + + /** + * Parse template. + */ + USE_PHASE_PARSE_TEMPLATE, + + /** + * Load additional details (like products and + * categories) needed for verification and + * price computation. + */ + USE_PHASE_DB_FETCH, + + /** + * Validate request and template compatibility. + */ + USE_PHASE_VERIFY, + + /** + * Compute price of the order. + */ + USE_PHASE_COMPUTE_PRICE, + + /** + * Handle tip. + */ + USE_PHASE_CHECK_TIP, + + /** + * Check if client-supplied total amount matches + * our calculation (if we did any). + */ + USE_PHASE_CHECK_TOTAL, + + /** + * Construct the internal order request body. + */ + USE_PHASE_CREATE_ORDER, + + /** + * Submit the order to the shared order handler. + */ + USE_PHASE_SUBMIT_ORDER, + + /** + * Finished successfully with MHD_YES. + */ + USE_PHASE_FINISHED_MHD_YES, + + /** + * Finished with MHD_NO. + */ + USE_PHASE_FINISHED_MHD_NO +}; + +struct UseContext +{ + /** + * Context for our handler. + */ + struct TMH_HandlerContext *hc; + + /** + * Internal handler context we are passing into the + * POST /private/orders handler. + */ + struct TMH_HandlerContext ihc; + + /** + * Phase we are currently in. + */ + enum UsePhase phase; + + /** + * Template type from the contract. + */ + enum TALER_MERCHANT_TemplateType template_type; + + /** + * Information set in the #USE_PHASE_PARSE_REQUEST phase. + */ + struct + { + /** + * Summary override from request, if any. + */ + const char *summary; + + /** + * Amount provided by the client. + */ + struct TALER_Amount amount; + + /** + * Tip provided by the client. + */ + struct TALER_Amount tip; + + /** + * True if @e amount was not provided. + */ + bool no_amount; + + /** + * True if @e tip was not provided. + */ + bool no_tip; + + /** + * Parsed fields for inventory templates. + */ + struct + { + /** + * Selected products from inventory_selection. + */ + struct InventoryTemplateItemContext *items; + + /** + * Length of @e items. + */ + unsigned int items_len; + + } inventory; + + /** + * Request details if this is a paivana instantiation. + */ + struct + { + + /** + * Target website for the request. + */ + const char *website; + + /** + * Unique client identifier, consisting of + * current time, "-", and the hash of a nonce, + * the website and the current time. + */ + const char *paivana_id; + + } paivana; + + } parse_request; + + /** + * Information set in the #USE_PHASE_LOOKUP_TEMPLATE phase. + */ + struct + { + + /** + * Our template details from the DB. + */ + struct TALER_MERCHANTDB_TemplateDetails etp; + + } lookup_template; + + /** + * Information set in the #USE_PHASE_PARSE_TEMPLATE phase. + */ + struct TALER_MERCHANT_TemplateContract template_contract; + + /** + * Information set in the #USE_PHASE_COMPUTE_PRICE phase. + */ + struct + { + + /** + * Per-currency totals across selected products (without tips). + */ + struct TALER_Amount *totals; + + /** + * Length of @e totals. + */ + unsigned int totals_len; + + /** + * Array of payment choices, used with Paviana. + */ + json_t *choices; + + } compute_price; + +}; + + +/** + * Clean up inventory items. + * + * @param items_len length of @a items + * @param[in] items item array to free + */ +static void +cleanup_inventory_items (unsigned int items_len, + struct InventoryTemplateItemContext items[static + items_len]) +{ + for (unsigned int i = 0; i < items_len; i++) + { + struct InventoryTemplateItemContext *item = &items[i]; + + TALER_MERCHANTDB_product_details_free (&item->pd); + GNUNET_free (item->categories); + } + GNUNET_free (items); +} + + +/** + * Clean up a `struct UseContext *` + * + * @param[in] cls a `struct UseContext *` + */ +static void +cleanup_use_context (void *cls) +{ + struct UseContext *uc = cls; + + TALER_MERCHANTDB_template_details_free (&uc->lookup_template.etp); + if (NULL != + uc->parse_request.inventory.items) + cleanup_inventory_items (uc->parse_request.inventory.items_len, + uc->parse_request.inventory.items); + GNUNET_free (uc->compute_price.totals); + uc->compute_price.totals_len = 0; + json_decref (uc->compute_price.choices); + if (NULL != uc->ihc.cc) + uc->ihc.cc (uc->ihc.ctx); + GNUNET_free (uc->ihc.infix); + json_decref (uc->ihc.request_body); + GNUNET_free (uc); +} + + +/** + * Finalize a template use request. + * + * @param[in,out] uc use context + * @param ret handler return value + */ +static void +use_finalize (struct UseContext *uc, + MHD_RESULT ret) +{ + uc->phase = (MHD_YES == ret) + ? USE_PHASE_FINISHED_MHD_YES + : USE_PHASE_FINISHED_MHD_NO; +} + + +/** + * Finalize after JSON parsing result. + * + * @param[in,out] uc use context + * @param res parse result + */ +static void +use_finalize_parse (struct UseContext *uc, + enum GNUNET_GenericReturnValue res) +{ + GNUNET_assert (GNUNET_OK != res); + use_finalize (uc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); +} + + +/** + * Reply with error and finalize the request. + * + * @param[in,out] uc use context + * @param http_status HTTP status code + * @param ec error code + * @param detail error detail + */ +static void +use_reply_with_error (struct UseContext *uc, + unsigned int http_status, + enum TALER_ErrorCode ec, + const char *detail) +{ + MHD_RESULT mret; + + mret = TALER_MHD_reply_with_error (uc->hc->connection, + http_status, + ec, + detail); + use_finalize (uc, + mret); +} + + +/* ***************** USE_PHASE_PARSE_REQUEST **************** */ + +/** + * Parse request data for inventory templates. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_using_templates_inventory_request ( + struct UseContext *uc) +{ + const json_t *inventory_selection; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("inventory_selection", + &inventory_selection), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + GNUNET_assert (NULL == uc->ihc.request_body); + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return GNUNET_SYSERR; + } + + if ( (! uc->parse_request.no_amount) && + (! TMH_test_exchange_configured_for_currency ( + uc->parse_request.amount.currency)) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Currency is not supported by backend"); + return GNUNET_SYSERR; + } + + for (size_t i = 0; i < json_array_size (inventory_selection); i++) + { + struct InventoryTemplateItemContext item = { 0 }; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("product_id", + &item.product_id), + GNUNET_JSON_spec_string ("quantity", + &item.unit_quantity), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; + + res = GNUNET_JSON_parse (json_array_get (inventory_selection, + i), + ispec, + &err_name, + &err_line); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + return GNUNET_SYSERR; + } + + GNUNET_array_append (uc->parse_request.inventory.items, + uc->parse_request.inventory.items_len, + item); + } + return GNUNET_OK; +} + + +/** + * Parse request data for paivana templates. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_using_templates_paivana_request ( + struct UseContext *uc) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("website", + &uc->parse_request.paivana.website), + GNUNET_JSON_spec_string ("paivana_id", + &uc->parse_request.paivana.paivana_id), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + struct GNUNET_HashCode sh; + unsigned long long tv; + const char *dash; + + GNUNET_assert (NULL == uc->ihc.request_body); + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return GNUNET_SYSERR; + } + if (1 != + sscanf (uc->parse_request.paivana.paivana_id, + "%llu-", + &tv)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paivana_id"); + return GNUNET_SYSERR; + } + dash = strchr (uc->parse_request.paivana.paivana_id, + '-'); + GNUNET_assert (NULL != dash); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (dash + 1, + strlen (dash + 1), + &sh, + sizeof (sh))) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "paivana_id"); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Main function for the #USE_PHASE_PARSE_REQUEST. + * + * @param[in,out] uc context to update + */ +static void +handle_phase_parse_request ( + struct UseContext *uc) +{ + const char *template_type = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("template_type", + &template_type), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &uc->parse_request.tip), + &uc->parse_request.no_tip), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("summary", + &uc->parse_request.summary), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("amount", + &uc->parse_request.amount), + &uc->parse_request.no_amount), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (uc->hc->connection, + uc->hc->request_body, + spec); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + use_finalize_parse (uc, + res); + return; + } + if (NULL == template_type) + template_type = "fixed-order"; + uc->template_type + = TALER_MERCHANT_template_type_from_string ( + template_type); + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + /* nothig left to do */ + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + res = parse_using_templates_paivana_request (uc); + if (GNUNET_OK == res) + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + res = parse_using_templates_inventory_request (uc); + if (GNUNET_OK == res) + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + break; + } + GNUNET_break (0); + use_reply_with_error ( + uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "template_type"); +} + + +/* ***************** USE_PHASE_LOOKUP_TEMPLATE **************** */ + +/** + * Main function for the #USE_PHASE_LOOKUP_TEMPLATE. + * + * @param[in,out] uc context to update + */ +static void +handle_phase_lookup_template ( + struct UseContext *uc) +{ + struct TMH_MerchantInstance *mi = uc->hc->instance; + const char *template_id = uc->hc->infix; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_template (TMH_db->cls, + mi->settings.id, + template_id, + &uc->lookup_template.etp); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + /* Clean up and fail hard */ + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_template"); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* this should be impossible (single select) */ + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_template"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* template not found! */ + use_reply_with_error (uc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + template_id); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* all good */ + break; + } + if (uc->template_type != + TALER_MERCHANT_template_type_from_contract ( + uc->lookup_template.etp.template_contract)) + { + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_TYPE, + "template_contract has different type"); + return; + } + uc->phase++; +} + + +/* ***************** USE_PHASE_PARSE_TEMPLATE **************** */ + + +/** + * Parse template. + * + * @param[in,out] uc use context + */ +static void +handle_phase_template_contract (struct UseContext *uc) +{ + const char *err_name; + enum GNUNET_GenericReturnValue res; + + res = TALER_MERCHANT_template_contract_parse ( + uc->lookup_template.etp.template_contract, + &uc->template_contract, + &err_name); + if (GNUNET_OK != res) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + err_name); + return; + } + uc->phase++; +} + + +/* ***************** USE_PHASE_DB_FETCH **************** */ + +/** + * Fetch DB data for inventory templates. + * + * @param[in,out] uc use context + */ +static void +handle_phase_db_fetch (struct UseContext *uc) +{ + struct TMH_MerchantInstance *mi = uc->hc->instance; + + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + enum GNUNET_DB_QueryStatus qs; + + qs = TMH_db->lookup_product (TMH_db->cls, + mi->settings.id, + item->product_id, + &item->pd, + &item->num_categories, + &item->categories); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product"); + return; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup_product"); + return; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + use_reply_with_error (uc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, + item->product_id); + return; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + uc->phase++; +} + + +/* *************** Helpers for USE_PHASE_VERIFY ***************** */ + +/** + * Check if the given product ID appears in the array of allowed_products. + * + * @param allowed_products JSON array of product IDs allowed by the template, may be NULL + * @param product_id product ID to check + * @return true if the product ID is in the list + */ +static bool +product_id_allowed (const json_t *allowed_products, + const char *product_id) +{ + const json_t *entry; + size_t idx; + + if (NULL == allowed_products) + return false; + json_array_foreach ((json_t *) allowed_products, idx, entry) + { + if (! json_is_string (entry)) + { + GNUNET_break (0); + continue; + } + if (0 == strcmp (json_string_value (entry), + product_id)) + return true; + } + return false; +} + + +/** + * Check if any product category is in the selected_categories list. + * + * @param allowed_categories JSON array of categories allowed by the template, may be NULL + * @param num_categories length of @a categories + * @param categories list of categories of the selected product + * @return true if any category of the product is in the list of allowed categories matches + */ +static bool +category_allowed (const json_t *allowed_categories, + size_t num_categories, + const uint64_t categories[num_categories]) +{ + const json_t *entry; + size_t idx; + + if (NULL == allowed_categories) + return false; + json_array_foreach ((json_t *) allowed_categories, + idx, + entry) + { + uint64_t selected_id; + + if (! json_is_integer (entry)) + { + GNUNET_break (0); + continue; + } + if (0 > json_integer_value (entry)) + { + GNUNET_break (0); + continue; + } + selected_id = (uint64_t) json_integer_value (entry); + for (size_t i = 0; i < num_categories; i++) + { + if (categories[i] == selected_id) + return true; + } + } + return false; +} + + +/** + * Verify request data for inventory templates. + * Checks that the selected products are allowed + * for this template. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_using_templates_inventory (struct UseContext *uc) +{ + if (uc->template_contract.details.inventory.choose_one && + (1 != uc->parse_request.inventory.items_len)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inventory_selection"); + return GNUNET_SYSERR; + } + if (uc->template_contract.details.inventory.selected_all) + return GNUNET_OK; + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + const char *eparam = NULL; + + if (GNUNET_OK != + TALER_MERCHANT_vk_process_quantity_inputs ( + TALER_MERCHANT_VK_QUANTITY, + item->pd.allow_fractional_quantity, + true, + 0, + false, + item->unit_quantity, + &item->quantity_value, + &item->quantity_frac, + &eparam)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + eparam); + return GNUNET_SYSERR; + } + + if (0 == item->pd.price_array_length) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "price_array"); + return GNUNET_SYSERR; + } + } + + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + + if (product_id_allowed (uc->template_contract.details.inventory. + selected_products, + item->product_id)) + continue; + if (category_allowed ( + uc->template_contract.details.inventory.selected_categories, + item->num_categories, + item->categories)) + continue; + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_WRONG_PRODUCT, + item->product_id); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Verify request data for fixed-order templates. + * As here we cannot compute the total amount, either + * the template or the client request must provide it. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_using_templates_fixed ( + struct UseContext *uc) +{ + if ( (! uc->parse_request.no_amount) && + (! uc->template_contract.no_amount) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + NULL); + return GNUNET_SYSERR; + } + if (uc->parse_request.no_amount && + uc->template_contract.no_amount) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT, + NULL); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Verify request data for paivana templates. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +verify_using_templates_paivana ( + struct UseContext *uc) +{ + if (NULL != uc->template_contract.details.paivana.website_regex) + { + regex_t ex; + bool allowed = false; + + if (0 != regcomp (&ex, + uc->template_contract.details.paivana.website_regex, + REG_NOSUB | REG_EXTENDED)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == + regexec (&ex, + uc->parse_request.paivana.website, + 0, NULL, + 0)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Website `%s' allowed by template\n", + uc->parse_request.paivana.website); + allowed = true; + } + regfree (&ex); + if (! allowed) + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Verify that the client request is structurally acceptable for the specified + * template. Does NOT check the total amount being reasonable. + * + * @param[in,out] uc use context + */ +static void +handle_phase_verify ( + struct UseContext *uc) +{ + enum GNUNET_GenericReturnValue res = GNUNET_SYSERR; + + if ( (NULL != uc->parse_request.summary) && + (NULL != uc->template_contract.summary) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT, + NULL); + return; + } + if ( (NULL == uc->parse_request.summary) && + (NULL == uc->template_contract.summary) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY, + NULL); + return; + } + if ( (! uc->parse_request.no_amount) && + (NULL != uc->template_contract.currency) && + (0 != strcasecmp (uc->template_contract.currency, + uc->parse_request.amount.currency)) ) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + uc->template_contract.currency); + return; + } + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + res = verify_using_templates_fixed (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + res = verify_using_templates_paivana (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + res = verify_using_templates_inventory (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + if (GNUNET_OK == res) + uc->phase++; +} + + +/* ***************** USE_PHASE_COMPUTE_PRICE **************** */ + + +/** + * Compute the line total for a product based on quantity. + * + * @param unit_price price per unit + * @param quantity integer quantity + * @param quantity_frac fractional quantity (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) + * @param[out] line_total resulting line total + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +compute_line_total (const struct TALER_Amount *unit_price, + uint64_t quantity, + uint32_t quantity_frac, + struct TALER_Amount *line_total) +{ + struct TALER_Amount tmp; + + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (unit_price->currency, + line_total)); + if ( (0 != quantity) && + (0 > + TALER_amount_multiply (line_total, + unit_price, + (uint32_t) quantity)) ) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 == quantity_frac) + return GNUNET_OK; + if (0 > + TALER_amount_multiply (&tmp, + unit_price, + quantity_frac)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + TALER_amount_divide (&tmp, + &tmp, + TALER_MERCHANT_UNIT_FRAC_BASE); + if (0 > + TALER_amount_add (line_total, + line_total, + &tmp)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +/** + * Find the price of the given @a item in the specified + * @a currency. + * + * @param currency currency to search price in + * @param item item to check prices of + * @return NULL if a suitable price was not found + */ +static const struct TALER_Amount * +find_item_price_in_currency ( + const char *currency, + const struct InventoryTemplateItemContext *item) +{ + for (size_t j = 0; j < item->pd.price_array_length; j++) + { + if (0 == strcasecmp (item->pd.price_array[j].currency, + currency)) + return &item->pd.price_array[j]; + } + return NULL; +} + + +/** + * Compute totals for all currencies shared across selected products. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success (including no price due to no items) + * #GNUNET_NO if we could not find a price in any accepted currency + * for all selected products + * #GNUNET_SYSERR on arithmetic issues (internal error) + */ +static enum GNUNET_GenericReturnValue +compute_totals_per_currency (struct UseContext *uc) +{ + const struct InventoryTemplateItemContext *items + = uc->parse_request.inventory.items; + unsigned int items_len = uc->parse_request.inventory.items_len; + + if (0 == items_len) + return GNUNET_OK; + for (size_t i = 0; i < items[0].pd.price_array_length; i++) + { + const struct TALER_Amount *price + = &items[0].pd.price_array[i]; + struct TALER_Amount zero; + + if (! TMH_test_exchange_configured_for_currency (price->currency)) + continue; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (price->currency, + &zero)); + GNUNET_array_append (uc->compute_price.totals, + uc->compute_price.totals_len, + zero); + } + if (0 == uc->compute_price.totals_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No currency supported by our configuration in which we have prices for first selected product!\n"); + return GNUNET_NO; + } + /* Loop through items, ensure each currency exists and sum totals. */ + for (unsigned int i = 0; i < items_len; i++) + { + const struct InventoryTemplateItemContext *item = &items[i]; + unsigned int c = 0; + + while (c < uc->compute_price.totals_len) + { + struct TALER_Amount *total = &uc->compute_price.totals[c]; + const struct TALER_Amount *unit_price; + struct TALER_Amount line_total; + + unit_price = find_item_price_in_currency (total->currency, + item); + if (NULL == unit_price) + { + /* Drop the currency: we have no price in one of + the selected products */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Product `%s' has no price in %s: dropping currency\n", + item->product_id, + total->currency); + *total = uc->compute_price.totals[--uc->compute_price.totals_len]; + continue; + } + if (GNUNET_OK != + compute_line_total (unit_price, + item->quantity_value, + item->quantity_frac, + &line_total)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (total, + total, + &line_total)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + c++; + } + } + if (0 == uc->compute_price.totals_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No currency available in which we have prices for all selected products!\n"); + GNUNET_free (uc->compute_price.totals); + } + return (0 == uc->compute_price.totals_len) + ? GNUNET_NO + : GNUNET_OK; +} + + +/** + * Compute total for only the given @a currency. + * + * @param items_len length of @a items + * @param items inventory items + * @param currency currency to total + * @param[out] total computed total + * @return #GNUNET_OK on success + * #GNUNET_NO if we could not find a price in any accepted currency + * for all selected products + * #GNUNET_SYSERR on arithmetic issues (internal error) + */ +static enum GNUNET_GenericReturnValue +compute_inventory_total (unsigned int items_len, + const struct InventoryTemplateItemContext *items, + const char *currency, + struct TALER_Amount *total) +{ + GNUNET_assert (NULL != currency); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (currency, + total)); + for (unsigned int i = 0; i < items_len; i++) + { + const struct InventoryTemplateItemContext *item = &items[i]; + const struct TALER_Amount *unit_price; + struct TALER_Amount line_total; + + unit_price = find_item_price_in_currency (currency, + item); + if (NULL == unit_price) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "compute_inventory_total: no price in %s for product `%s'\n", + currency, + item->product_id); + return GNUNET_NO; + } + if (GNUNET_OK != + compute_line_total (unit_price, + item->quantity_value, + item->quantity_frac, + &line_total)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "compute_inventory_total: line total failed for %s in %s\n", + item->product_id, + currency); + return GNUNET_SYSERR; + } + if (0 > + TALER_amount_add (total, + total, + &line_total)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + return GNUNET_OK; +} + + +/** + * Compute total price. + * + * @param[in,out] uc use context + */ +static void +handle_phase_compute_price (struct UseContext *uc) +{ + const char *primary_currency; + enum GNUNET_GenericReturnValue ret; + + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + uc->compute_price.totals + = GNUNET_new (struct TALER_Amount); + uc->compute_price.totals_len + = 1; + if (uc->parse_request.no_amount) + { + GNUNET_assert (! uc->template_contract.no_amount); + *uc->compute_price.totals + = uc->template_contract.amount; + } + else + { + GNUNET_assert (uc->template_contract.no_amount); + *uc->compute_price.totals + = uc->parse_request.amount; + } + uc->phase++; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + /* handled below */ + break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + { + const struct TALER_MERCHANT_TemplateContractPaivana *tcp + = &uc->template_contract.details.paivana; + json_t *choices; + + choices = json_array (); + GNUNET_assert (NULL != choices); + for (size_t i = 0; i < tcp->choices_len; i++) + { + /* Make deep copy, we're going to MODIFY it! */ + struct TALER_MERCHANT_ContractChoice choice + = tcp->choices[i]; + + choice.no_tip = uc->parse_request.no_tip; + if (! uc->parse_request.no_tip) + { + if (GNUNET_YES != + TALER_amount_cmp_currency (&choice.amount, + &uc->parse_request.tip)) + continue; /* tip does not match choice currency */ + choice.tip = uc->parse_request.tip; + if (0 > + TALER_amount_add (&choice.amount, + &choice.amount, + &uc->parse_request.tip)) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "tip"); + return; + } + } + GNUNET_assert (0 == + json_array_append_new ( + choices, + TALER_MERCHANT_json_from_contract_choice (&choice, + true))); + } + if (0 == json_array_size (choices)) + { + GNUNET_break_op (0); + json_decref (choices); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, + "tip"); + return; + } + uc->compute_price.choices = choices; + } + /* Note: we already did the tip and pricing + fully here, so we skip these phases. */ + uc->phase = USE_PHASE_CREATE_ORDER; + return; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + primary_currency = uc->template_contract.currency; + if (! uc->parse_request.no_amount) + primary_currency = uc->parse_request.amount.currency; + if (! uc->parse_request.no_tip) + primary_currency = uc->parse_request.tip.currency; + if (NULL == primary_currency) + { + ret = compute_totals_per_currency (uc); + } + else + { + uc->compute_price.totals + = GNUNET_new (struct TALER_Amount); + uc->compute_price.totals_len + = 1; + ret = compute_inventory_total ( + uc->parse_request.inventory.items_len, + uc->parse_request.inventory.items, + primary_currency, + uc->compute_price.totals); + } + if (GNUNET_SYSERR == ret) + { + use_reply_with_error ( + uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "calculation of currency totals failed"); + return; + } + if (GNUNET_NO == ret) + { + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY, + NULL); + return; + } + + uc->phase++; +} + + +/* ***************** USE_PHASE_CHECK_TIP **************** */ + + +/** + * Check that tip specified is reasonable and add to total. + * + * @param[in,out] uc use context + */ +static void +handle_phase_check_tip (struct UseContext *uc) +{ + struct TALER_Amount *total_amount; + + if (uc->parse_request.no_tip) + { + uc->phase++; + return; + } + if (0 == uc->compute_price.totals_len) + { + if (! TMH_test_exchange_configured_for_currency ( + uc->parse_request.tip.currency)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + "Tip currency is not supported by backend"); + return; + } + uc->compute_price.totals + = GNUNET_new (struct TALER_Amount); + uc->compute_price.totals_len + = 1; + *uc->compute_price.totals + = uc->parse_request.tip; + uc->phase++; + return; + } + GNUNET_assert (1 == uc->compute_price.totals_len); + total_amount = &uc->compute_price.totals[0]; + if (GNUNET_YES != + TALER_amount_cmp_currency (&uc->parse_request.tip, + total_amount)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + uc->parse_request.tip.currency); + return; + } + if (0 > + TALER_amount_add (total_amount, + total_amount, + &uc->parse_request.tip)) + { + GNUNET_break (0); + use_reply_with_error (uc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT, + "tip"); + return; + } + uc->phase++; +} + + +/* ***************** USE_PHASE_CHECK_TOTAL **************** */ + +/** + * Check that if the client specified a total, + * it matches our own calculation. + * + * @param[in,out] uc use context + */ +static void +handle_phase_check_total (struct UseContext *uc) +{ + GNUNET_assert (1 <= uc->compute_price.totals_len); + if (! uc->parse_request.no_amount) + { + GNUNET_assert (1 == uc->compute_price.totals_len); + GNUNET_assert (GNUNET_YES == + TALER_amount_cmp_currency (&uc->parse_request.amount, + &uc->compute_price.totals[0])); + if (0 != + TALER_amount_cmp (&uc->parse_request.amount, + &uc->compute_price.totals[0])) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + TALER_amount2s (&uc->compute_price.totals[0])); + return; + } + } + uc->phase++; +} + + +/* ***************** USE_PHASE_CREATE_ORDER **************** */ + + +/** + * Create order request for inventory templates. + * + * @param[in,out] uc use context + */ +static void +create_using_templates_inventory (struct UseContext *uc) +{ + json_t *inventory_products; + json_t *choices; + + inventory_products = json_array (); + GNUNET_assert (NULL != inventory_products); + for (unsigned int i = 0; + i < uc->parse_request.inventory.items_len; + i++) + { + const struct InventoryTemplateItemContext *item = + &uc->parse_request.inventory.items[i]; + + GNUNET_assert (0 == + json_array_append_new ( + inventory_products, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_id", + item->product_id), + GNUNET_JSON_pack_string ("unit_quantity", + item->unit_quantity)))); + } + choices = json_array (); + GNUNET_assert (NULL != choices); + for (unsigned int i = 0; + i < uc->compute_price.totals_len; + i++) + { + GNUNET_assert (0 == + json_array_append_new ( + choices, + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &uc->compute_price.totals[i]), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + uc->parse_request.no_tip + ? NULL + : &uc->parse_request.tip)) + ))); + } + + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + uc->lookup_template.etp.otp_id)), + GNUNET_JSON_pack_array_steal ("inventory_products", + inventory_products), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + 1), + GNUNET_JSON_pack_array_steal ("choices", + choices), + GNUNET_JSON_pack_string ("summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary)))); +} + + +/** + * Create order request for fixed-order templates. + * + * @param[in,out] uc use context + */ +static void +create_using_templates_fixed (struct UseContext *uc) +{ + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("otp_id", + uc->lookup_template.etp.otp_id)), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ( + "amount", + &uc->compute_price.totals[0]), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + uc->parse_request.no_tip + ? NULL + : &uc->parse_request.tip)), + GNUNET_JSON_pack_string ( + "summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary)))); +} + + +/** + * Create order request for paivana templates. + * + * @param[in,out] uc use context + */ +static void +create_using_templates_paivana (struct UseContext *uc) +{ + uc->ihc.request_body + = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ( + "session_id", + uc->parse_request.paivana.paivana_id), + GNUNET_JSON_pack_object_steal ( + "order", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + 1), + GNUNET_JSON_pack_array_incref ("choices", + uc->compute_price.choices), + GNUNET_JSON_pack_string ( + "summary", + NULL == uc->parse_request.summary + ? uc->template_contract.summary + : uc->parse_request.summary), + GNUNET_JSON_pack_string ("fulfillment_url", + uc->parse_request.paivana.website)))); +} + + +static void +handle_phase_create_order (struct UseContext *uc) +{ + GNUNET_assert (NULL == uc->ihc.request_body); + switch (uc->template_type) + { + case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: + create_using_templates_fixed (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: + create_using_templates_paivana (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: + create_using_templates_inventory (uc); + break; + case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_assert (0); + } + uc->phase++; +} + + +/* ***************** Main handler **************** */ + +MHD_RESULT +TMH_post_using_templates_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct UseContext *uc = hc->ctx; + + (void) rh; + if (NULL == uc) + { + uc = GNUNET_new (struct UseContext); + uc->hc = hc; + hc->ctx = uc; + hc->cc = &cleanup_use_context; + uc->ihc.instance = hc->instance; + uc->phase = USE_PHASE_PARSE_REQUEST; + uc->template_type = TALER_MERCHANT_TEMPLATE_TYPE_INVALID; + } + + while (1) + { + switch (uc->phase) + { + case USE_PHASE_PARSE_REQUEST: + handle_phase_parse_request (uc); + break; + case USE_PHASE_LOOKUP_TEMPLATE: + handle_phase_lookup_template (uc); + break; + case USE_PHASE_PARSE_TEMPLATE: + handle_phase_template_contract (uc); + break; + case USE_PHASE_DB_FETCH: + handle_phase_db_fetch (uc); + break; + case USE_PHASE_VERIFY: + handle_phase_verify (uc); + break; + case USE_PHASE_COMPUTE_PRICE: + handle_phase_compute_price (uc); + break; + case USE_PHASE_CHECK_TIP: + handle_phase_check_tip (uc); + break; + case USE_PHASE_CHECK_TOTAL: + handle_phase_check_total (uc); + break; + case USE_PHASE_CREATE_ORDER: + handle_phase_create_order (uc); + break; + case USE_PHASE_SUBMIT_ORDER: + return TMH_private_post_orders ( + NULL, /* not even used */ + connection, + &uc->ihc); + case USE_PHASE_FINISHED_MHD_YES: + return MHD_YES; + case USE_PHASE_FINISHED_MHD_NO: + return MHD_NO; + } + } +} diff --git a/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.h b/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.h @@ -0,0 +1,40 @@ +/* + This file is part of TALER + (C) 2022 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/> +*/ +/** + * @file taler-merchant-httpd_post-templates-TEMPLATE_ID.h + * @brief headers for POST /using-templates handler + * @author Priscilla Huang + */ +#ifndef TALER_MERCHANT_HTTPD_POST_TEMPLATES_ID_H +#define TALER_MERCHANT_HTTPD_POST_TEMPLATES_ID_H + +#include "taler-merchant-httpd.h" + +/** + * Generate a template that customer can use it. Returns an MHD_RESULT. + * + * @param rh details about this request 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_post_using_templates_ID (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-account-ID.c b/src/backend/taler-merchant-httpd_private-delete-account-ID.c @@ -1,94 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-delete-account-ID.c - * @brief implement DELETE /account/$H_WIRE - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-account-ID.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> - - -MHD_RESULT -TMH_private_delete_account_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MerchantWireHashP h_wire; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (hc->infix, - strlen (hc->infix), - &h_wire, - sizeof (h_wire))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "h_wire"); - } - GNUNET_assert (NULL != mi); - qs = TMH_db->inactivate_account (TMH_db->cls, - mi->settings.id, - &h_wire); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "inactivate_account"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT, - "account unknown"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - { - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - }; - - TMH_db->event_notify (TMH_db->cls, - &es, - NULL, - 0); - } - TMH_reload_instances (mi->settings.id); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_private-delete-account-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-account-ID.h b/src/backend/taler-merchant-httpd_private-delete-account-ID.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-delete-account-ID.h - * @brief implement DELETE /account/$PAYTO - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ACCOUNT_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ACCOUNT_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/private/account/$H_WIRE" 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_account_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/* end of taler-merchant-httpd_private-delete-account-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-categories-ID.c b/src/backend/taler-merchant-httpd_private-delete-categories-ID.c @@ -1,92 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-delete-categories-ID.c - * @brief implement DELETE /private/categories/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-categories-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/categories/$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_categories_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - unsigned long long cnum; - char dummy; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - if (1 != sscanf (hc->infix, - "%llu%c", - &cnum, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "category_id must be a number"); - } - qs = TMH_db->delete_category (TMH_db->cls, - mi->settings.id, - cnum); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_category"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "delete_category"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-categories-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-categories-ID.h b/src/backend/taler-merchant-httpd_private-delete-categories-ID.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-delete-categories-ID.h - * @brief implement DELETE /private/categories/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_CATEGORIES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_CATEGORIES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/categories/$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_categories_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-categories-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-donau-instance-ID.c b/src/backend/taler-merchant-httpd_private-delete-donau-instance-ID.c @@ -1,93 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-delete-donau-instance-ID.c - * @brief implement DELETE /private/donau/$donau_serial_id - * @author Bohdan Potuzhnyi - * @author Vlada Svirsh - */ - -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-donau-instance-ID.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> - -/** - * Handle a DELETE "/donau/$donau_serial_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_donau_instance_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - uint64_t donau_serial_id; - char dummy; - - GNUNET_assert (NULL != mi); - - if (1 != sscanf (hc->infix, - "%lu%c", - &donau_serial_id, - &dummy)) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - hc->infix); - } - - qs = TMH_db->delete_donau_instance (TMH_db->cls, - hc->instance->settings.id, - donau_serial_id); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_donau_instance"); - - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "delete_donau_instance (soft)"); - - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); - - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} -\ No newline at end of file diff --git a/src/backend/taler-merchant-httpd_private-delete-donau-instance-ID.h b/src/backend/taler-merchant-httpd_private-delete-donau-instance-ID.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-delete-donau-instance-ID.h - * @brief implement DELETE /private/donau/$charity_id/ - * @author Bohdan Potuzhnyi - * @author Vlada Svirsh - */ - -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_DONAU_INSTANCE_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_DONAU_INSTANCE_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/donau/$donau_serial_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_donau_instance_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-donau-instance-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-group-ID.c b/src/backend/taler-merchant-httpd_private-delete-group-ID.c @@ -1,74 +0,0 @@ -/* - 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-group-ID.c - * @brief implementation of DELETE /private/groups/$GROUP_ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-group-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_delete_group (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *group_id_str = hc->infix; - unsigned long long group_id; - enum GNUNET_DB_QueryStatus qs; - char dummy; - - (void) rh; - if (1 != sscanf (group_id_str, - "%llu%c", - &group_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "group_id"); - } - qs = TMH_db->delete_product_group (TMH_db->cls, - hc->instance->settings.id, - group_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_product_group"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, - group_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-group-ID.h b/src/backend/taler-merchant-httpd_private-delete-group-ID.h @@ -1,41 +0,0 @@ -/* - 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-group-ID.h - * @brief HTTP serving layer for deleting product groups - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_GROUP_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_GROUP_ID_H - -#include "taler-merchant-httpd.h" - -/** - * Handle DELETE /private/groups/$GROUP_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_group (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.c @@ -1,164 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2023 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-post-instances-ID-token.c - * @brief implementing DELETE /instances/$ID/token request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-instances-ID-token.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_delete_instances_ID_token_SERIAL (const struct TMH_RequestHandler * - rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - unsigned long long serial; - char dummy; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - if (1 != sscanf (hc->infix, - "%llu%c", - &serial, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "serial must be a number"); - } - - - qs = TMH_db->delete_login_token_serial (TMH_db->cls, - mi->settings.id, - serial); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_login_token_by_serial"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_break (0); - return MHD_NO; -} - - -MHD_RESULT -TMH_private_delete_instances_ID_token (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *bearer = "Bearer "; - struct TMH_MerchantInstance *mi = hc->instance; - const char *tok; - struct TALER_MERCHANTDB_LoginTokenP btoken; - enum GNUNET_DB_QueryStatus qs; - - tok = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_AUTHORIZATION); - /* This was presumably checked before... */ - if (0 != - strncmp (tok, - bearer, - strlen (bearer))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "login token (in 'Authorization' header)"); - } - tok += strlen (bearer); - while (' ' == *tok) - tok++; - if (0 != strncasecmp (tok, - RFC_8959_PREFIX, - strlen (RFC_8959_PREFIX))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "login token (in 'Authorization' header)"); - } - tok += strlen (RFC_8959_PREFIX); - - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (tok, - strlen (tok), - &btoken, - sizeof (btoken))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "login token (in 'Authorization' header)"); - } - qs = TMH_db->delete_login_token (TMH_db->cls, - mi->settings.id, - &btoken); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_login_token"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* No 404, as the login token must have existed - when we got the request as it was accepted as - valid. So we can only get here due to concurrent - modification, and then the client should still - simply see the success. Hence, fall-through */ - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_break (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-instances-ID-login.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.h b/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.h @@ -1,59 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2023 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-delete-instances-ID-token.h - * @brief implements DELETE /instances/$ID/token request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_TOKEN_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_TOKEN_H -#include "taler-merchant-httpd.h" - -/** - * Delete login token for an instance by serial. - * - * @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_instances_ID_token_SERIAL ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Delete login token for an instance. - * - * @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_instances_ID_token ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID.c b/src/backend/taler-merchant-httpd_private-delete-instances-ID.c @@ -1,163 +0,0 @@ -/* - This file is part of TALER - (C) 2020-2026 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-httpd_private-delete-instances-ID.c - * @brief implement DELETE /instances/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-instances-ID.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include "taler-merchant-httpd_mfa.h" - -/** - * Handle a DELETE "/instances/$ID" request. - * - * @param[in,out] hc http request context - * @param mfa_check true if a MFA check is required - * @param mi instance to delete - * @param connection the MHD connection to handle - * @return MHD result code - */ -static MHD_RESULT -delete_instances_ID (struct TMH_HandlerContext *hc, - bool mfa_check, - struct TMH_MerchantInstance *mi, - struct MHD_Connection *connection) -{ - bool purge; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - if (mfa_check) - { - enum GNUNET_GenericReturnValue ret = - TMH_mfa_check_simple (hc, - TALER_MERCHANT_MFA_CO_INSTANCE_DELETION, - mi); - - if (GNUNET_OK != ret) - { - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - } - - { - const char *purge_s; - - purge_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "purge"); - if (NULL == purge_s) - purge_s = "no"; - purge = (0 == strcasecmp (purge_s, - "yes")); - } - if (purge) - qs = TMH_db->purge_instance (TMH_db->cls, - mi->settings.id); - else - qs = TMH_db->delete_instance_private_key (TMH_db->cls, - mi->settings.id); - { - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - }; - - TMH_db->event_notify (TMH_db->cls, - &es, - NULL, - 0); - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete private key"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - purge - ? "Instance unknown" - : "Private key unknown"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - TMH_reload_instances (mi->settings.id); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -MHD_RESULT -TMH_private_delete_instances_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - - (void) rh; - return delete_instances_ID (hc, - true, - mi, - connection); -} - - -MHD_RESULT -TMH_private_delete_instances_default_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi; - - (void) rh; - mi = TMH_lookup_instance (hc->infix); - if (NULL == mi) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); - } - return delete_instances_ID (hc, - false, - mi, - connection); -} - - -/* end of taler-merchant-httpd_private-delete-instances-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID.h b/src/backend/taler-merchant-httpd_private-delete-instances-ID.h @@ -1,56 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-delete-instances-ID.h - * @brief implement DELETE /instances/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/instances/$ID/private" 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_instances_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Handle a DELETE "/management/instances/$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_instances_default_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/* end of taler-merchant-httpd_private-delete-instances-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-orders-ID.c b/src/backend/taler-merchant-httpd_private-delete-orders-ID.c @@ -1,133 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-delete-orders-ID.c - * @brief implement DELETE /orders/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-orders-ID.h" -#include <stdint.h> -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/orders/$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_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - const char *force_s; - bool force; - - (void) rh; - force_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "force"); - if (NULL == force_s) - force_s = "no"; - force = (0 == strcasecmp (force_s, - "yes")); - - GNUNET_assert (NULL != mi); - qs = TMH_db->delete_order (TMH_db->cls, - mi->settings.id, - hc->infix, - force); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - qs = TMH_db->delete_contract_terms (TMH_db->cls, - mi->settings.id, - hc->infix, - TMH_legal_expiration); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - struct TALER_MerchantPostDataHashP unused; - uint64_t order_serial; - bool paid = false; - bool wired = false; - bool matches = false; - int16_t choice_index; - - qs = TMH_db->lookup_order (TMH_db->cls, - mi->settings.id, - hc->infix, - NULL, - &unused, - NULL); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, - mi->settings.id, - hc->infix, - NULL, - NULL, - &order_serial, - &paid, - &wired, - &matches, - NULL, - &choice_index); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - hc->infix); - if (paid) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID, - hc->infix); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT, - hc->infix); - } - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-orders-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-orders-ID.h b/src/backend/taler-merchant-httpd_private-delete-orders-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-delete-orders-ID.h - * @brief implement DELETE /orders/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ORDERS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_ORDERS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/orders/$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_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-orders-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c b/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.c @@ -1,78 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-delete-otp-devices-ID.c - * @brief implement DELETE /otp-devices/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-otp-devices-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->delete_otp (TMH_db->cls, - mi->settings.id, - hc->infix); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_otp"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "delete_otp (soft)"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-otp-devices-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.h b/src/backend/taler-merchant-httpd_private-delete-otp-devices-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-delete-otp-devices-ID.h - * @brief implement DELETE /otp-devices/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_OTP_DEVICES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_OTP_DEVICES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-otp-devices-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-pot-ID.c b/src/backend/taler-merchant-httpd_private-delete-pot-ID.c @@ -1,75 +0,0 @@ -/* - 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 "taler/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 @@ -1,41 +0,0 @@ -/* - 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-delete-products-ID.c b/src/backend/taler-merchant-httpd_private-delete-products-ID.c @@ -1,103 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-delete-products-ID.c - * @brief implement DELETE /products/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-products-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/products/$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_products_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->delete_product (TMH_db->cls, - mi->settings.id, - hc->infix); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_product"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "delete_product (soft)"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - size_t num_categories = 0; - uint64_t *categories = NULL; - - /* check if deletion must have failed because of locks by - checking if the product exists */ - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - hc->infix, - NULL, - &num_categories, - &categories); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "lookup_product"); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - hc->infix); - GNUNET_free (categories); - } - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-products-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-products-ID.h b/src/backend/taler-merchant-httpd_private-delete-products-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-delete-products-ID.h - * @brief implement DELETE /products/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_PRODUCTS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_PRODUCTS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/products/$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_products_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-products-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-report-ID.c b/src/backend/taler-merchant-httpd_private-delete-report-ID.c @@ -1,76 +0,0 @@ -/* - 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-report-ID.c - * @brief implementation of DELETE /private/reports/$REPORT_ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-report-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_delete_report (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *report_id_str = hc->infix; - unsigned long long report_id; - enum GNUNET_DB_QueryStatus qs; - char dummy; - - (void) rh; - - if (1 != - sscanf (report_id_str, - "%llu%c", - &report_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "report_id"); - } - - qs = TMH_db->delete_report (TMH_db->cls, - hc->instance->settings.id, - (uint64_t) report_id); - if (qs < 0) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_report"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, - report_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-report-ID.h b/src/backend/taler-merchant-httpd_private-delete-report-ID.h @@ -1,40 +0,0 @@ -/* - 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-report-ID.h - * @brief HTTP serving layer for deleting reports - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_REPORT_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_REPORT_ID_H -#include "taler-merchant-httpd.h" - -/** - * Handle DELETE /private/reports/$REPORT_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_report (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-templates-ID.c b/src/backend/taler-merchant-httpd_private-delete-templates-ID.c @@ -1,78 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-delete-templates-ID.c - * @brief implement DELETE /templates/$ID - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-templates-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/templates/$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_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->delete_template (TMH_db->cls, - mi->settings.id, - hc->infix); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_template"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "delete_template (soft)"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-templates-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-templates-ID.h b/src/backend/taler-merchant-httpd_private-delete-templates-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-delete-templates-ID.h - * @brief implement DELETE /templates/$ID/ - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TEMPLATES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TEMPLATES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/templates/$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_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-templates-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-token-families-SLUG.c b/src/backend/taler-merchant-httpd_private-delete-token-families-SLUG.c @@ -1,75 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-delete-token-families-SLUG.c - * @brief implement DELETE /tokenfamilies/$SLUG - * @author Christian Blättler - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-token-families-SLUG.h" -#include <gnunet/gnunet_db_lib.h> -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/tokenfamilies/$SLUG" 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_token_families_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->delete_token_family (TMH_db->cls, - mi->settings.id, - hc->infix); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_token_family"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "delete_token_family (soft)"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-token-families-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-token-families-SLUG.h b/src/backend/taler-merchant-httpd_private-delete-token-families-SLUG.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-delete-token-families-SLUG.h - * @brief implement DELETE /tokenfamilies/$SLUG/ - * @author Christian Blättler - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TOKEN_FAMILIES_SLUG_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TOKEN_FAMILIES_SLUG_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/tokenfamilies/$SLUG" 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_token_families_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-token-families-SLUG.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-transfers-ID.c b/src/backend/taler-merchant-httpd_private-delete-transfers-ID.c @@ -1,90 +0,0 @@ -/* - This file is part of TALER - (C) 2021 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-httpd_private-delete-transfers-ID.c - * @brief implement DELETE /transfers/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-transfers-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_delete_transfers_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - unsigned long long serial; - char dummy; - - (void) rh; - GNUNET_assert (NULL != mi); - if (1 != - sscanf (hc->infix, - "%llu%c", - &serial, - &dummy)) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - hc->infix); - } - qs = TMH_db->delete_transfer (TMH_db->cls, - mi->settings.id, - serial); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - qs = TMH_db->check_transfer_exists (TMH_db->cls, - mi->settings.id, - serial); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TRANSFER_UNKNOWN, - hc->infix); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-transfers-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-transfers-ID.h b/src/backend/taler-merchant-httpd_private-delete-transfers-ID.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-delete-transfers-ID.h - * @brief implement DELETE /transfers/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TRANSFERS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_TRANSFERS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/transfers/$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_transfers_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-transfers-ID.h */ - -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-units-ID.c b/src/backend/taler-merchant-httpd_private-delete-units-ID.c @@ -1,83 +0,0 @@ -/* - 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-httpd_private-delete-units-ID.c - * @brief implement DELETE /private/units/$UNIT - * @author Bohdan Potuzhnyi - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-units-ID.h" - - -MHD_RESULT -TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - enum GNUNET_DB_QueryStatus qs; - bool no_instance = false; - bool no_unit = false; - bool builtin_conflict = false; - - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->delete_unit (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &no_instance, - &no_unit, - &builtin_conflict); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "delete_unit"); - case GNUNET_DB_STATUS_HARD_ERROR: - default: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_unit"); - } - if (no_instance) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->instance->settings.id); - if (no_unit) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, - hc->infix); - if (builtin_conflict) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN, - hc->infix); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_private-delete-units-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-units-ID.h b/src/backend/taler-merchant-httpd_private-delete-units-ID.h @@ -1,33 +0,0 @@ -/* - 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-httpd_private-delete-units-ID.h - * @brief implement DELETE /private/units/$UNIT - * @author Bohdan Potuzhnyi - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_UNITS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_UNITS_ID_H - -#include "taler-merchant-httpd.h" - - -MHD_RESULT -TMH_private_delete_units_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-units-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-delete-webhooks-ID.c b/src/backend/taler-merchant-httpd_private-delete-webhooks-ID.c @@ -1,78 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-delete-webhooks-ID.c - * @brief implement DELETE /webhooks/$ID - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-delete-webhooks-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a DELETE "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->delete_webhook (TMH_db->cls, - mi->settings.id, - hc->infix); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "delete_webhook"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "delete_webhook (soft)"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_WEBHOOK_UNKNOWN, - hc->infix); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-delete-webhooks-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-delete-webhooks-ID.h b/src/backend/taler-merchant-httpd_private-delete-webhooks-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-delete-webhooks-ID.h - * @brief implement DELETE /webhooks/$ID/ - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_DELETE_WEBHOOKS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_WEBHOOKS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a DELETE "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-delete-webhooks-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-accounts-ID.c b/src/backend/taler-merchant-httpd_private-get-accounts-ID.c @@ -1,109 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 2026 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-httpd_private-get-accounts-ID.c - * @brief implement GET /accounts/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-accounts-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a GET "/accounts/$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_accounts_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *h_wire_s = hc->infix; - struct TALER_MerchantWireHashP h_wire; - struct TALER_MERCHANTDB_AccountDetails tp = { 0 }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != h_wire_s); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (h_wire_s, - strlen (h_wire_s), - &h_wire, - sizeof (h_wire))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_H_WIRE_MALFORMED, - h_wire_s); - } - qs = TMH_db->select_account (TMH_db->cls, - mi->settings.id, - &h_wire, - &tp); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_account"); - } - if (0 == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ACCOUNT_UNKNOWN, - hc->infix); - } - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_bool ("active", - tp.active), - TALER_JSON_pack_full_payto ("payto_uri", - tp.payto_uri), - GNUNET_JSON_pack_data_auto ("h_wire", - &tp.h_wire), - GNUNET_JSON_pack_data_auto ("salt", - &tp.salt), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("extra_wire_subject_metadata", - tp.extra_wire_subject_metadata)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("credit_facade_url", - tp.credit_facade_url))); - /* We do not return the credentials, as they may - be sensitive */ - json_decref (tp.credit_facade_credentials); - GNUNET_free (tp.extra_wire_subject_metadata); - GNUNET_free (tp.payto_uri.full_payto); - GNUNET_free (tp.credit_facade_url); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-get-accounts-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-accounts-ID.h b/src/backend/taler-merchant-httpd_private-get-accounts-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-accounts-ID.h - * @brief implement GET /accounts/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/accounts/$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_accounts_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-accounts-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-accounts.c b/src/backend/taler-merchant-httpd_private-get-accounts.c @@ -1,84 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-accounts.c - * @brief implement GET /accounts - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-accounts.h" -#include <taler/taler_json_lib.h> - - -/** - * Add account details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param merchant_priv private key of the merchant instance - * @param ad details about the account - */ -static void -add_account (void *cls, - const struct TALER_MerchantPrivateKeyP *merchant_priv, - const struct TALER_MERCHANTDB_AccountDetails *ad) -{ - json_t *pa = cls; - - (void) merchant_priv; - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_bool ("active", - ad->active), - TALER_JSON_pack_full_payto ("payto_uri", - ad->payto_uri), - GNUNET_JSON_pack_data_auto ("h_wire", - &ad->h_wire)))); -} - - -MHD_RESULT -TMH_private_get_accounts (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *pa; - enum GNUNET_DB_QueryStatus qs; - - pa = json_array (); - GNUNET_assert (NULL != pa); - qs = TMH_db->select_accounts (TMH_db->cls, - hc->instance->settings.id, - &add_account, - pa); - if (0 > qs) - { - GNUNET_break (0); - json_decref (pa); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("accounts", - pa)); -} - - -/* end of taler-merchant-httpd_private-get-accounts.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-accounts.h b/src/backend/taler-merchant-httpd_private-get-accounts.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-accounts.h - * @brief implement GET /accounts - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ACCOUNTS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/accounts" 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_accounts (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-accounts.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-categories-ID.c b/src/backend/taler-merchant-httpd_private-get-categories-ID.c @@ -1,121 +0,0 @@ -/* - This file is part of TALER - (C) 2022-2024 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-httpd_private-get-categories-ID.c - * @brief implement GET /private/categories/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-categories-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a GET "/private/categories/$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_categories_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - unsigned long long cnum; - char dummy; - struct TALER_MERCHANTDB_CategoryDetails cd; - size_t num_products = 0; - char *products = NULL; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - if (1 != sscanf (hc->infix, - "%llu%c", - &cnum, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "category_id must be a number"); - } - - qs = TMH_db->select_category (TMH_db->cls, - mi->settings.id, - cnum, - &cd, - &num_products, - &products); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_category"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, - hc->infix); - } - { - MHD_RESULT ret; - json_t *jproducts; - const char *pos = products; - - jproducts = json_array (); - GNUNET_assert (NULL != jproducts); - for (unsigned int i = 0; i<num_products; i++) - { - const char *product_id = pos; - json_t *jprod; - - pos = pos + strlen (product_id) + 1; - jprod = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("product_id", - product_id)); - GNUNET_assert (0 == - json_array_append_new (jproducts, - jprod)); - } - GNUNET_free (products); - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("name", - cd.category_name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("name_i18n", - cd.category_name_i18n)), - GNUNET_JSON_pack_array_steal ("products", - jproducts)); - TALER_MERCHANTDB_category_details_free (&cd); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-get-categories-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-categories-ID.h b/src/backend/taler-merchant-httpd_private-get-categories-ID.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-get-categories-ID.h - * @brief implement GET /private/categories/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/private/categories/$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_categories_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-categories-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-categories.c b/src/backend/taler-merchant-httpd_private-get-categories.c @@ -1,93 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-get-categories.c - * @brief implement GET /categories - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-categories.h" - - -/** - * Add category details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param category_id ID of the category - * @param category_name name of the category - * @param category_name_i18n translations of the @a category_name - * @param product_count number of products in the category - */ -static void -add_category (void *cls, - uint64_t category_id, - const char *category_name, - const json_t *category_name_i18n, - uint64_t product_count) -{ - json_t *pa = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ( - "category_id", - category_id), - GNUNET_JSON_pack_string ( - "name", - category_name), - GNUNET_JSON_pack_object_incref ( - "name_i18n", - (json_t *) category_name_i18n), - GNUNET_JSON_pack_uint64 ( - "product_count", - product_count)))); -} - - -MHD_RESULT -TMH_private_get_categories (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *pa; - enum GNUNET_DB_QueryStatus qs; - - pa = json_array (); - GNUNET_assert (NULL != pa); - qs = TMH_db->lookup_categories (TMH_db->cls, - hc->instance->settings.id, - &add_category, - pa); - if (0 > qs) - { - GNUNET_break (0); - json_decref (pa); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("categories", - pa)); -} - - -/* end of taler-merchant-httpd_private-get-categories.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-categories.h b/src/backend/taler-merchant-httpd_private-get-categories.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-get-categories.h - * @brief implement GET /private/categories - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_CATEGORIES_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/private/categories" 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_categories ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-categories.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-donau-instances.c b/src/backend/taler-merchant-httpd_private-get-donau-instances.c @@ -1,122 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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-httpd_private-get-donau-instances.c - * @brief implementation of GET /donau - * @author Bohdan Potuzhnyi - * @author Vlada Svirsh - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include "taler/taler_merchant_donau.h" -#include "taler/taler_merchant_service.h" -#include "taler-merchant-httpd_private-get-donau-instances.h" - - -/** - * Add details about a Donau instance to the JSON array. - * - * @param cls json array to which the Donau instance details will be added - * @param donau_instance_serial the serial number of the Donau instance - * @param donau_url the URL of the Donau instance - * @param charity_name the name of the charity - * @param charity_pub_key the public key of the charity - * @param charity_id the ID of the charity - * @param charity_max_per_year the maximum donation amount per year - * @param charity_receipts_to_date the total donations received so far this year - * @param current_year the current year being tracked for donations - * @param donau_keys_json JSON object with key information specific to the Donau instance, NULL if not (yet) available. - */ -static void -add_donau_instance (void *cls, - uint64_t donau_instance_serial, - const char *donau_url, - const char *charity_name, - const struct DONAU_CharityPublicKeyP *charity_pub_key, - uint64_t charity_id, - const struct TALER_Amount *charity_max_per_year, - const struct TALER_Amount *charity_receipts_to_date, - int64_t current_year, - const json_t *donau_keys_json) -{ - json_t *json_instances = cls; - - GNUNET_assert ( - 0 == json_array_append_new ( - json_instances, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("donau_instance_serial", - donau_instance_serial), - GNUNET_JSON_pack_string ("donau_url", - donau_url), - GNUNET_JSON_pack_string ("charity_name", - charity_name), - GNUNET_JSON_pack_data_auto ("charity_pub_key", - charity_pub_key), - GNUNET_JSON_pack_uint64 ("charity_id", - charity_id), - TALER_JSON_pack_amount ("charity_max_per_year", - charity_max_per_year), - TALER_JSON_pack_amount ("charity_receipts_to_date", - charity_receipts_to_date), - GNUNET_JSON_pack_int64 ("current_year", - current_year), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("donau_keys_json", - (json_t *) donau_keys_json)) - ))); -} - - -/** - * Handle a GET "/donau" 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_donau_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *json_donau_instances = json_array (); - enum GNUNET_DB_QueryStatus qs; - - TMH_db->preflight (TMH_db->cls); - qs = TMH_db->select_donau_instances (TMH_db->cls, - hc->instance->settings.id, - &add_donau_instance, - json_donau_instances); - if (0 > qs) - { - GNUNET_break (0); - json_decref (json_donau_instances); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ( - "donau_instances", - json_donau_instances)); -} diff --git a/src/backend/taler-merchant-httpd_private-get-donau-instances.h b/src/backend/taler-merchant-httpd_private-get-donau-instances.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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-httpd_private-get-donau-instances.h - * @brief implementation of GET /donau - * @author Bohdan Potuzhnyi - * @author Vlada Svirsh - */ - -#ifndef TALER_MERCHANT_HTTPD_GET_DONAU_INSTANCES_H -#define TALER_MERCHANT_HTTPD_GET_DONAU_INSTANCES_H - -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - -/** - * Handle a GET "/donau" 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_donau_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-groups.c b/src/backend/taler-merchant-httpd_private-get-groups.c @@ -1,122 +0,0 @@ -/* - 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-groups.c - * @brief implementation of GET /private/groups - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-groups.h" -#include <taler/taler_json_lib.h> - -/** - * Sensible bound on the number of results to return - */ -#define MAX_DELTA 1024 - - -/** - * Callback for listing product groups. - * - * @param cls closure with a `json_t *` - * @param product_group_id unique identifier of the group - * @param group_name name of the group - * @param group_description human-readable description - */ -static void -add_group (void *cls, - uint64_t product_group_id, - const char *group_name, - const char *group_description) -{ - json_t *groups = cls; - json_t *entry; - - entry = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("group_serial", - product_group_id), - GNUNET_JSON_pack_string ("group_name", - group_name), - GNUNET_JSON_pack_string ("description", - group_description)); - GNUNET_assert (NULL != entry); - GNUNET_assert (0 == - json_array_append_new (groups, - entry)); -} - - -MHD_RESULT -TMH_private_get_groups (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - int64_t limit = -20; - uint64_t offset; - json_t *groups; - - (void) rh; - TALER_MHD_parse_request_snumber (connection, - "limit", - &limit); - 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"); - } - if (limit > 0) - offset = 0; - else - offset = INT64_MAX; - TALER_MHD_parse_request_number (connection, - "offset", - &offset); - - groups = json_array (); - GNUNET_assert (NULL != groups); - - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->select_product_groups (TMH_db->cls, - hc->instance->settings.id, - limit, - offset, - &add_group, - groups); - if (qs < 0) - { - GNUNET_break (0); - json_decref (groups); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_product_groups"); - } - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("groups", - groups)); -} diff --git a/src/backend/taler-merchant-httpd_private-get-groups.h b/src/backend/taler-merchant-httpd_private-get-groups.h @@ -1,41 +0,0 @@ -/* - 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-groups.h - * @brief HTTP serving layer for listing product groups - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_GROUPS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_GROUPS_H - -#include "taler-merchant-httpd.h" - -/** - * Handle GET /private/groups 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_groups (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-incoming-ID.c b/src/backend/taler-merchant-httpd_private-get-incoming-ID.c @@ -1,236 +0,0 @@ -/* - This file is part of TALER - (C) 2026 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-httpd_private-get-incoming-ID.c - * @brief implement API for obtaining details about an expected incoming wire transfer - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_private-get-incoming-ID.h" - - -/** - * Function called with information about orders aggregated into - * a wire transfer. - * Generate a response (array entry) based on the given arguments. - * - * @param cls closure with a `json_t *` array to build up the response - * @param order_id ID of the order that was paid and aggregated - * @param remaining_deposit deposited amount minus any refunds - * @param deposit_fee deposit fees paid to the exchange for the order - */ -static void -reconciliation_cb (void *cls, - const char *order_id, - const struct TALER_Amount *remaining_deposit, - const struct TALER_Amount *deposit_fee) -{ - json_t *rd = cls; - json_t *r; - - r = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_id", - order_id), - TALER_JSON_pack_amount ("remaining_deposit", - remaining_deposit), - TALER_JSON_pack_amount ("deposit_fee", - deposit_fee)); - GNUNET_assert (0 == - json_array_append_new (rd, - r)); -} - - -/** - * Manages a GET /private/incoming call. - * - * @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_incoming_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - unsigned long long serial_id; - struct TALER_Amount wire_fee; - bool no_fee; - struct GNUNET_TIME_Timestamp expected_time; - struct TALER_Amount expected_credit_amount; - struct TALER_WireTransferIdentifierRawP wtid; - struct TALER_FullPayto payto_uri; - char *exchange_url = NULL; - struct GNUNET_TIME_Timestamp execution_time; - bool confirmed; - - { - char dummy; - - if (1 != - sscanf (hc->infix, - "%llu%c", - &serial_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "transfer ID must be a number"); - } - } - - TMH_db->preflight (TMH_db->cls); - { - struct TALER_MasterPublicKeyP master_pub; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_expected_transfer (TMH_db->cls, - hc->instance->settings.id, - serial_id, - &expected_time, - &expected_credit_amount, - &wtid, - &payto_uri, - &exchange_url, - &execution_time, - &confirmed, - &master_pub); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_expected_transfer"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_EXPECTED_TRANSFER_UNKNOWN, - hc->infix); - } - - { - char *method; - struct GNUNET_TIME_Timestamp start_date; - struct GNUNET_TIME_Timestamp end_date; - struct TALER_MasterSignatureP master_sig; - struct TALER_WireFeeSet fees; - - method = TALER_payto_get_method (payto_uri.full_payto); - qs = TMH_db->lookup_wire_fee ( - TMH_db->cls, - &master_pub, - method, - expected_time, - &fees, - &start_date, - &end_date, - &master_sig); - GNUNET_free (method); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - GNUNET_free (exchange_url); - GNUNET_free (payto_uri.full_payto); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_wire_fee"); - } - no_fee = (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs); - if (! no_fee) - wire_fee = fees.wire; - } - - } - - { - enum GNUNET_DB_QueryStatus qs; - json_t *rd; - MHD_RESULT mret; - - rd = json_array (); - GNUNET_assert (NULL != rd); - qs = TMH_db->lookup_reconciliation_details (TMH_db->cls, - hc->instance->settings.id, - serial_id, - &reconciliation_cb, - rd); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - GNUNET_free (exchange_url); - GNUNET_free (payto_uri.full_payto); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_reconciliation_details"); - } - - mret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ( - "expected_credit_amount", - TALER_amount_is_valid (&expected_credit_amount) - ? &expected_credit_amount - : NULL)), - GNUNET_JSON_pack_data_auto ("wtid", - &wtid), - TALER_JSON_pack_full_payto ("payto_uri", - payto_uri), - GNUNET_JSON_pack_string ("exchange_url", - exchange_url), - GNUNET_JSON_pack_bool ("confirmed", - confirmed), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("execution_time", - execution_time)), - GNUNET_JSON_pack_timestamp ("expected_time", - expected_time), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("wire_fee", - no_fee ? NULL : &wire_fee)), - GNUNET_JSON_pack_array_steal ("reconciliation_details", - rd)); - GNUNET_free (exchange_url); - GNUNET_free (payto_uri.full_payto); - return mret; - } -} - - -/* end of taler-merchant-httpd_private-get-incoming-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-incoming-ID.h b/src/backend/taler-merchant-httpd_private-get-incoming-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2026 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/> -*/ -/** - * @file taler-merchant-httpd_private-get-incoming-ID.h - * @brief headers for GET /incoming/$ID handler - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_ID_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Manages a GET /private/incoming/$ID call. - * - * @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_incoming_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-incoming.c b/src/backend/taler-merchant-httpd_private-get-incoming.c @@ -1,194 +0,0 @@ -/* - 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-httpd_private-get-incoming.c - * @brief implement API for obtaining a list of expected incoming wire transfers - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_private-get-incoming.h" - - -/** - * Function called with information about a wire transfer. - * Generate a response (array entry) based on the given arguments. - * - * @param cls closure with a `json_t *` array to build up the response - * @param expected_credit_amount amount expected to be wired to the merchant (minus fees), NULL if unknown - * @param wtid wire transfer identifier - * @param payto_uri target account that received the wire transfer - * @param exchange_url base URL of the exchange that made the wire transfer - * @param expected_transfer_serial_id serial number identifying the transfer in the backend - * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS - * if it did not yet happen - * @param confirmed true if the merchant acknowledged the wire transfer reception - * @param validated true if the reconciliation succeeded - * @param last_http_status HTTP status of our last request to the exchange for this transfer - * @param last_ec last error code we got back (otherwise #TALER_EC_NONE) - * @param last_error_detail last detail we got back (or NULL for none) - */ -static void -incoming_cb (void *cls, - const struct TALER_Amount *expected_credit_amount, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct TALER_FullPayto payto_uri, - const char *exchange_url, - uint64_t expected_transfer_serial_id, - struct GNUNET_TIME_Timestamp execution_time, - bool confirmed, - bool validated, - unsigned int last_http_status, - enum TALER_ErrorCode last_ec, - const char *last_error_detail) -{ - json_t *ja = cls; - json_t *r; - - r = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("expected_credit_amount", - expected_credit_amount)), - GNUNET_JSON_pack_data_auto ("wtid", - wtid), - TALER_JSON_pack_full_payto ("payto_uri", - payto_uri), - GNUNET_JSON_pack_string ("exchange_url", - exchange_url), - GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", - expected_transfer_serial_id), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("execution_time", - execution_time)), - GNUNET_JSON_pack_bool ("validated", - validated), - GNUNET_JSON_pack_bool ("confirmed", - confirmed), - GNUNET_JSON_pack_uint64 ("last_http_status", - last_http_status), - GNUNET_JSON_pack_uint64 ("last_ec", - last_ec), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("last_error_detail", - last_error_detail))); - GNUNET_assert (0 == - json_array_append_new (ja, - r)); -} - - -/** - * Manages a GET /private/incoming call. - * - * @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_incoming (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TALER_FullPayto payto_uri = { - .full_payto = NULL - }; - struct GNUNET_TIME_Timestamp before = GNUNET_TIME_UNIT_FOREVER_TS; - struct GNUNET_TIME_Timestamp after = GNUNET_TIME_UNIT_ZERO_TS; - int64_t limit = -20; - uint64_t offset; - enum TALER_EXCHANGE_YesNoAll confirmed; - enum TALER_EXCHANGE_YesNoAll verified; - - (void) rh; - TALER_MHD_parse_request_snumber (connection, - "limit", - &limit); - if (limit < 0) - offset = INT64_MAX; - else - offset = 0; - TALER_MHD_parse_request_number (connection, - "offset", - &offset); - TALER_MHD_parse_request_yna (connection, - "verified", - TALER_EXCHANGE_YNA_ALL, - &verified); - TALER_MHD_parse_request_yna (connection, - "confirmed", - TALER_EXCHANGE_YNA_ALL, - &confirmed); - TALER_MHD_parse_request_timestamp (connection, - "before", - &before); - TALER_MHD_parse_request_timestamp (connection, - "after", - &after); - { - const char *esc_payto; - - esc_payto = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "payto_uri"); - if (NULL != esc_payto) - { - payto_uri.full_payto - = GNUNET_strdup (esc_payto); - (void) MHD_http_unescape (payto_uri.full_payto); - } - } - TMH_db->preflight (TMH_db->cls); - { - json_t *ja; - enum GNUNET_DB_QueryStatus qs; - - ja = json_array (); - GNUNET_assert (NULL != ja); - qs = TMH_db->lookup_expected_transfers (TMH_db->cls, - hc->instance->settings.id, - payto_uri, - before, - after, - limit, - offset, - confirmed, - verified, - &incoming_cb, - ja); - GNUNET_free (payto_uri.full_payto); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_expected_transfers"); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("incoming", - ja)); - } -} - - -/* end of taler-merchant-httpd_private-get-incoming.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-incoming.h b/src/backend/taler-merchant-httpd_private-get-incoming.h @@ -1,41 +0,0 @@ -/* - 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 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-httpd_private-get-incoming.h - * @brief headers for GET /incoming handler - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Manages a GET /private/incoming call. - * - * @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_incoming (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -1,1487 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2026 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-get-instances-ID-kyc.c - * @brief implementing GET /instances/$ID/kyc request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-instances-ID-kyc.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_exchanges.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include <regex.h> - -/** - * Information we keep per /kyc request. - */ -struct KycContext; - - -/** - * Structure for tracking requests to the exchange's - * ``/kyc-check`` API. - */ -struct ExchangeKycRequest -{ - /** - * Kept in a DLL. - */ - struct ExchangeKycRequest *next; - - /** - * Kept in a DLL. - */ - struct ExchangeKycRequest *prev; - - /** - * Find operation where we connect to the respective exchange. - */ - struct TMH_EXCHANGES_KeysOperation *fo; - - /** - * JSON array of payto-URIs with KYC auth wire transfer - * instructions. Provided if @e auth_ok is false and - * @e kyc_auth_conflict is false. - */ - json_t *pkaa; - - /** - * The keys of the exchange. - */ - struct TALER_EXCHANGE_Keys *keys; - - /** - * KYC request this exchange request is made for. - */ - struct KycContext *kc; - - /** - * JSON array of AccountLimits that apply, NULL if - * unknown (and likely defaults apply). - */ - json_t *jlimits; - - /** - * Our account's payto URI. - */ - struct TALER_FullPayto payto_uri; - - /** - * Base URL of the exchange. - */ - char *exchange_url; - - /** - * Hash of the wire account (with salt) we are checking. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * Current access token for the KYC SPA. Only set - * if @e auth_ok is true. - */ - struct TALER_AccountAccessTokenP access_token; - - /** - * Timestamp when we last got a reply from the exchange. - */ - struct GNUNET_TIME_Timestamp last_check; - - /** - * Last HTTP status code obtained via /kyc-check from the exchange. - */ - unsigned int last_http_status; - - /** - * Last Taler error code returned from /kyc-check. - */ - enum TALER_ErrorCode last_ec; - - /** - * True if this account cannot work at this exchange because KYC auth is - * impossible. - */ - bool kyc_auth_conflict; - - /** - * We could not get /keys from the exchange. - */ - bool no_keys; - - /** - * True if @e access_token is available. - */ - bool auth_ok; - - /** - * True if we believe no KYC is currently required - * for this account at this exchange. - */ - bool kyc_ok; - - /** - * True if the exchange exposed to us that the account - * is currently under AML review. - */ - bool in_aml_review; - -}; - - -/** - * Information we keep per /kyc request. - */ -struct KycContext -{ - /** - * Stored in a DLL. - */ - struct KycContext *next; - - /** - * Stored in a DLL. - */ - struct KycContext *prev; - - /** - * Connection we are handling. - */ - struct MHD_Connection *connection; - - /** - * Instance we are serving. - */ - struct TMH_MerchantInstance *mi; - - /** - * Our handler context. - */ - struct TMH_HandlerContext *hc; - - /** - * Response to return, NULL if we don't have one yet. - */ - struct MHD_Response *response; - - /** - * JSON array where we are building up the array with - * pending KYC operations. - */ - json_t *kycs_data; - - /** - * Head of DLL of requests we are making to an - * exchange to inquire about the latest KYC status. - */ - struct ExchangeKycRequest *exchange_pending_head; - - /** - * Tail of DLL of requests we are making to an - * exchange to inquire about the latest KYC status. - */ - struct ExchangeKycRequest *exchange_pending_tail; - - /** - * Notification handler from database on changes - * to the KYC status. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Set to the exchange URL, or NULL to not filter by - * exchange. "exchange_url" query parameter. - */ - const char *exchange_url; - - /** - * How long are we willing to wait for the exchange(s)? - * Based on "timeout_ms" query parameter. - */ - struct GNUNET_TIME_Absolute timeout; - - /** - * Set to the h_wire of the merchant account if - * @a have_h_wire is true, used to filter by account. - * Set from "h_wire" query parameter. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * Set to the Etag of a response already known to the - * client. We should only return from long-polling - * on timeout (with "Not Modified") or when the Etag - * of the response differs from what is given here. - * Only set if @a have_lp_not_etag is true. - * Set from "lp_etag" query parameter. - */ - struct GNUNET_ShortHashCode lp_not_etag; - - /** - * Specifies what status change we are long-polling for. If specified, the - * endpoint will only return once the status *matches* the given value. If - * multiple accounts or exchanges match the query, any account reaching the - * STATUS will cause the response to be returned. - */ - const char *lp_status; - - /** - * Specifies what status change we are long-polling for. If specified, the - * endpoint will only return once the status no longer matches the given - * value. If multiple accounts or exchanges *no longer matches* the given - * STATUS will cause the response to be returned. - */ - const char *lp_not_status; - - /** - * #GNUNET_NO if the @e connection was not suspended, - * #GNUNET_YES if the @e connection was suspended, - * #GNUNET_SYSERR if @e connection was resumed to as - * part of #MH_force_pc_resume during shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * What state are we long-polling for? "lpt" argument. - */ - enum TALER_EXCHANGE_KycLongPollTarget lpt; - - /** - * HTTP status code to use for the reply, i.e 200 for "OK". - * Special value UINT_MAX is used to indicate hard errors - * (no reply, return #MHD_NO). - */ - unsigned int response_code; - - /** - * True if @e h_wire was given. - */ - bool have_h_wire; - - /** - * True if @e lp_not_etag was given. - */ - bool have_lp_not_etag; - - /** - * We're still waiting on the exchange to determine - * the KYC status of our deposit(s). - */ - bool return_immediately; - -}; - - -/** - * Head of DLL. - */ -static struct KycContext *kc_head; - -/** - * Tail of DLL. - */ -static struct KycContext *kc_tail; - - -void -TMH_force_kyc_resume () -{ - for (struct KycContext *kc = kc_head; - NULL != kc; - kc = kc->next) - { - if (GNUNET_YES == kc->suspended) - { - kc->suspended = GNUNET_SYSERR; - MHD_resume_connection (kc->connection); - } - } -} - - -/** - * Custom cleanup routine for a `struct KycContext`. - * - * @param cls the `struct KycContext` to clean up. - */ -static void -kyc_context_cleanup (void *cls) -{ - struct KycContext *kc = cls; - struct ExchangeKycRequest *ekr; - - while (NULL != (ekr = kc->exchange_pending_head)) - { - GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, - kc->exchange_pending_tail, - ekr); - if (NULL != ekr->fo) - { - TMH_EXCHANGES_keys4exchange_cancel (ekr->fo); - ekr->fo = NULL; - } - json_decref (ekr->pkaa); - json_decref (ekr->jlimits); - if (NULL != ekr->keys) - TALER_EXCHANGE_keys_decref (ekr->keys); - GNUNET_free (ekr->exchange_url); - GNUNET_free (ekr->payto_uri.full_payto); - GNUNET_free (ekr); - } - if (NULL != kc->eh) - { - TMH_db->event_listen_cancel (kc->eh); - kc->eh = NULL; - } - if (NULL != kc->response) - { - MHD_destroy_response (kc->response); - kc->response = NULL; - } - GNUNET_CONTAINER_DLL_remove (kc_head, - kc_tail, - kc); - json_decref (kc->kycs_data); - GNUNET_free (kc); -} - - -/** - * We have found an exchange in status @a status. Clear any - * long-pollers that wait for us having (or not having) this - * status. - * - * @param[in,out] kc context - * @param status the status we encountered - */ -static void -clear_status (struct KycContext *kc, - const char *status) -{ - if ( (NULL != kc->lp_status) && - (0 == strcmp (kc->lp_status, - status)) ) - kc->lp_status = NULL; /* satisfied! */ - if ( (NULL != kc->lp_not_status) && - (0 != strcmp (kc->lp_not_status, - status) ) ) - kc->lp_not_status = NULL; /* satisfied! */ -} - - -/** - * Resume the given KYC context and send the final response. Stores the - * response in the @a kc and signals MHD to resume the connection. Also - * ensures MHD runs immediately. - * - * @param kc KYC context - */ -static void -resume_kyc_with_response (struct KycContext *kc) -{ - struct GNUNET_ShortHashCode sh; - bool not_modified; - char *can; - - if ( (! GNUNET_TIME_absolute_is_past (kc->timeout)) && - ( (NULL != kc->lp_not_status) || - (NULL != kc->lp_status) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Long-poll target status not reached, not returning response yet\n"); - if (GNUNET_NO == kc->suspended) - { - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - } - return; - } - can = TALER_JSON_canonicalize (kc->kycs_data); - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (&sh, - sizeof (sh), - "KYC-SALT", - strlen ("KYC-SALT"), - can, - strlen (can), - NULL, - 0)); - not_modified = kc->have_lp_not_etag && - (0 == GNUNET_memcmp (&sh, - &kc->lp_not_etag)); - if (not_modified && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Status unchanged, not returning response yet\n"); - if (GNUNET_NO == kc->suspended) - { - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - } - GNUNET_free (can); - return; - } - { - const char *inm; - - inm = MHD_lookup_connection_value (kc->connection, - MHD_GET_ARGUMENT_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if ( (NULL == inm) || - ('"' != inm[0]) || - ('"' != inm[strlen (inm) - 1]) || - (0 != strncmp (inm + 1, - can, - strlen (can))) ) - not_modified = false; /* must return full response */ - } - GNUNET_free (can); - kc->response_code = not_modified - ? MHD_HTTP_NOT_MODIFIED - : MHD_HTTP_OK; - kc->response = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_array_incref ("kyc_data", - kc->kycs_data)); - { - char *etag; - char *qetag; - - etag = GNUNET_STRINGS_data_to_string_alloc (&sh, - sizeof (sh)); - GNUNET_asprintf (&qetag, - "\"%s\"", - etag); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_ETAG, - qetag)); - GNUNET_free (qetag); - GNUNET_free (etag); - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming /kyc handling as exchange interaction is done (%u)\n", - MHD_HTTP_OK); - if (GNUNET_YES == kc->suspended) - { - kc->suspended = GNUNET_NO; - MHD_resume_connection (kc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - } -} - - -/** - * Handle a DB event about an update relevant - * for the processing of the kyc request. - * - * @param cls our `struct KycContext` - * @param extra additional event data provided - * @param extra_size number of bytes in @a extra - */ -static void -kyc_change_cb (void *cls, - const void *extra, - size_t extra_size) -{ - struct KycContext *kc = cls; - - if (GNUNET_YES == kc->suspended) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming KYC with gateway timeout\n"); - kc->suspended = GNUNET_NO; - MHD_resume_connection (kc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - } -} - - -/** - * Pack the given @a limit into the JSON @a limits array. - * - * @param limit account limit to pack - * @param[in,out] limits JSON array to extend - */ -static void -pack_limit (const struct TALER_EXCHANGE_AccountLimit *limit, - json_t *limits) -{ - json_t *jl; - - jl = GNUNET_JSON_PACK ( - TALER_JSON_pack_kycte ("operation_type", - limit->operation_type), - GNUNET_JSON_pack_time_rel ("timeframe", - limit->timeframe), - TALER_JSON_pack_amount ("threshold", - &limit->threshold), - GNUNET_JSON_pack_bool ("soft_limit", - limit->soft_limit) - ); - GNUNET_assert (0 == - json_array_append_new (limits, - jl)); -} - - -/** - * Return JSON array with AccountLimit objects giving - * the current limits for this exchange. - * - * @param[in,out] ekr overall request context - */ -static json_t * -get_exchange_limits ( - struct ExchangeKycRequest *ekr) -{ - const struct TALER_EXCHANGE_Keys *keys = ekr->keys; - json_t *limits; - - if (NULL != ekr->jlimits) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning custom KYC limits\n"); - return json_incref (ekr->jlimits); - } - if (NULL == keys) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No keys, thus no default KYC limits known\n"); - return NULL; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Returning default KYC limits (%u/%u)\n", - keys->hard_limits_length, - keys->zero_limits_length); - limits = json_array (); - GNUNET_assert (NULL != limits); - for (unsigned int i = 0; i<keys->hard_limits_length; i++) - { - const struct TALER_EXCHANGE_AccountLimit *limit - = &keys->hard_limits[i]; - - pack_limit (limit, - limits); - } - for (unsigned int i = 0; i<keys->zero_limits_length; i++) - { - const struct TALER_EXCHANGE_ZeroLimitedOperation *zlimit - = &keys->zero_limits[i]; - json_t *jl; - struct TALER_Amount zero; - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (keys->currency, - &zero)); - jl = GNUNET_JSON_PACK ( - TALER_JSON_pack_kycte ("operation_type", - zlimit->operation_type), - GNUNET_JSON_pack_time_rel ("timeframe", - GNUNET_TIME_UNIT_ZERO), - TALER_JSON_pack_amount ("threshold", - &zero), - GNUNET_JSON_pack_bool ("soft_limit", - true) - ); - GNUNET_assert (0 == - json_array_append_new (limits, - jl)); - } - return limits; -} - - -/** - * Maps @a ekr to a status code for clients to interpret the - * overall result. - * - * @param ekr request summary - * @return status of the KYC state as a string - */ -static const char * -map_to_status (const struct ExchangeKycRequest *ekr) -{ - if (ekr->no_keys) - { - return "no-exchange-keys"; - } - if (TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE == - ekr->last_ec) - return "unsupported-account"; - if (ekr->kyc_ok) - { - if (NULL != ekr->jlimits) - { - size_t off; - json_t *limit; - json_array_foreach (ekr->jlimits, off, limit) - { - struct TALER_Amount threshold; - enum TALER_KYCLOGIC_KycTriggerEvent operation_type; - bool soft = false; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_kycte ("operation_type", - &operation_type), - TALER_JSON_spec_amount_any ("threshold", - &threshold), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("soft_limit", - &soft), - NULL), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (limit, - spec, - NULL, NULL)) - { - GNUNET_break (0); - return "merchant-internal-error"; - } - if (! TALER_amount_is_zero (&threshold)) - continue; /* only care about zero-limits */ - if (! soft) - continue; /* only care about soft limits */ - if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || - (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) || - (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) ) - { - if (! ekr->auth_ok) - { - if (ekr->kyc_auth_conflict) - return "kyc-wire-impossible"; - return "kyc-wire-required"; - } - return "kyc-required"; - } - } - } - if (NULL == ekr->jlimits) - { - /* check default limits */ - const struct TALER_EXCHANGE_Keys *keys = ekr->keys; - - for (unsigned int i = 0; i < keys->zero_limits_length; i++) - { - enum TALER_KYCLOGIC_KycTriggerEvent operation_type - = keys->zero_limits[i].operation_type; - - if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) || - (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) || - (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) ) - { - if (! ekr->auth_ok) - { - if (ekr->kyc_auth_conflict) - return "kyc-wire-impossible"; - return "kyc-wire-required"; - } - return "kyc-required"; - } - } - } - return "ready"; - } - if (! ekr->auth_ok) - { - if (ekr->kyc_auth_conflict) - return "kyc-wire-impossible"; - return "kyc-wire-required"; - } - if (ekr->in_aml_review) - return "awaiting-aml-review"; - switch (ekr->last_http_status) - { - case 0: - return "exchange-unreachable"; - case MHD_HTTP_OK: - /* then we should have kyc_ok */ - GNUNET_break (0); - return NULL; - case MHD_HTTP_ACCEPTED: - /* Then KYC is really what is needed */ - return "kyc-required"; - case MHD_HTTP_NO_CONTENT: - /* then we should have had kyc_ok! */ - GNUNET_break (0); - return NULL; - case MHD_HTTP_FORBIDDEN: - /* then we should have had ! auth_ok */ - GNUNET_break (0); - return NULL; - case MHD_HTTP_NOT_FOUND: - /* then we should have had ! auth_ok */ - GNUNET_break (0); - return NULL; - case MHD_HTTP_CONFLICT: - /* then we should have had ! auth_ok */ - GNUNET_break (0); - return NULL; - case MHD_HTTP_INTERNAL_SERVER_ERROR: - return "exchange-internal-error"; - case MHD_HTTP_GATEWAY_TIMEOUT: - return "exchange-gateway-timeout"; - default: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Exchange responded with unexpected HTTP status %u to /kyc-check request!\n", - ekr->last_http_status); - break; - } - return "exchange-status-invalid"; -} - - -/** - * Take data from @a ekr to expand our response. - * - * @param ekr exchange we are done inspecting - */ -static void -ekr_expand_response (struct ExchangeKycRequest *ekr) -{ - struct TMH_Exchange *e = TMH_EXCHANGES_lookup_exchange (ekr->exchange_url); - const char *status; - - GNUNET_assert (NULL != e); - status = map_to_status (ekr); - if (NULL == status) - { - GNUNET_break (0); - status = "logic-bug"; - } - clear_status (ekr->kc, - status); - GNUNET_assert ( - 0 == - json_array_append_new ( - ekr->kc->kycs_data, - GNUNET_JSON_PACK ( - TALER_JSON_pack_full_payto ( - "payto_uri", - ekr->payto_uri), - GNUNET_JSON_pack_data_auto ( - "h_wire", - &ekr->h_wire), - GNUNET_JSON_pack_string ( - "status", - status), - GNUNET_JSON_pack_string ( - "exchange_url", - ekr->exchange_url), - GNUNET_JSON_pack_string ( - "exchange_currency", - TMH_EXCHANGES_get_currency (e)), - GNUNET_JSON_pack_bool ("no_keys", - ekr->no_keys), - GNUNET_JSON_pack_bool ("auth_conflict", - ekr->kyc_auth_conflict), - GNUNET_JSON_pack_uint64 ("exchange_http_status", - ekr->last_http_status), - (TALER_EC_NONE == ekr->last_ec) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "dummy", - NULL)) - : GNUNET_JSON_pack_uint64 ("exchange_code", - ekr->last_ec), - ekr->auth_ok - ? GNUNET_JSON_pack_data_auto ( - "access_token", - &ekr->access_token) - : GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "dummy", - NULL)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ( - "limits", - get_exchange_limits (ekr))), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ("payto_kycauths", - ekr->pkaa)) - ))); -} - - -/** - * We are done with asynchronous processing, generate the - * response for the @e kc. - * - * @param[in,out] kc KYC context to respond for - */ -static void -kc_respond (struct KycContext *kc) -{ - if ( (! kc->return_immediately) && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) - { - if (GNUNET_NO == kc->suspended) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending: long poll target %d not reached\n", - kc->lpt); - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Remaining suspended: long poll target %d not reached\n", - kc->lpt); - } - return; - } - /* All exchange requests done, create final - big response from cumulated replies */ - resume_kyc_with_response (kc); -} - - -/** - * We are done with the KYC request @a ekr. Remove it from the work list and - * check if we are done overall. - * - * @param[in] ekr key request that is done (and will be freed) - */ -static void -ekr_finished (struct ExchangeKycRequest *ekr) -{ - struct KycContext *kc = ekr->kc; - - ekr_expand_response (ekr); - GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head, - kc->exchange_pending_tail, - ekr); - json_decref (ekr->jlimits); - json_decref (ekr->pkaa); - if (NULL != ekr->keys) - TALER_EXCHANGE_keys_decref (ekr->keys); - GNUNET_free (ekr->exchange_url); - GNUNET_free (ekr->payto_uri.full_payto); - GNUNET_free (ekr); - - if (NULL != kc->exchange_pending_head) - return; /* wait for more */ - kc_respond (kc); -} - - -/** - * Figure out which exchange accounts from @a keys could - * be used for a KYC auth wire transfer from the account - * that @a ekr is checking. Will set the "pkaa" array - * in @a ekr. - * - * @param[in,out] ekr request we are processing - */ -static void -determine_eligible_accounts ( - struct ExchangeKycRequest *ekr) -{ - struct KycContext *kc = ekr->kc; - const struct TALER_EXCHANGE_Keys *keys = ekr->keys; - struct TALER_Amount kyc_amount; - char *merchant_pub_str; - struct TALER_NormalizedPayto np; - - ekr->pkaa = json_array (); - GNUNET_assert (NULL != ekr->pkaa); - { - const struct TALER_EXCHANGE_GlobalFee *gf; - - gf = TALER_EXCHANGE_get_global_fee (keys, - GNUNET_TIME_timestamp_get ()); - if (NULL == gf) - { - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (keys->currency, - &kyc_amount)); - } - else - { - /* FIXME-#9427: history fee should be globally renamed to KYC fee... */ - kyc_amount = gf->fees.history; - } - } - - merchant_pub_str - = GNUNET_STRINGS_data_to_string_alloc ( - &kc->mi->merchant_pub, - sizeof (kc->mi->merchant_pub)); - /* For all accounts of the exchange */ - np = TALER_payto_normalize (ekr->payto_uri); - for (unsigned int i = 0; i<keys->accounts_len; i++) - { - const struct TALER_EXCHANGE_WireAccount *account - = &keys->accounts[i]; - - /* KYC auth transfers are never supported with conversion */ - if (NULL != account->conversion_url) - continue; - /* filter by source account by credit_restrictions */ - if (GNUNET_YES != - TALER_EXCHANGE_test_account_allowed (account, - true, /* credit */ - np)) - continue; - /* exchange account is allowed, add it */ - { - const char *exchange_account_payto - = account->fpayto_uri.full_payto; - char *payto_kycauth; - - if (TALER_amount_is_zero (&kyc_amount)) - GNUNET_asprintf (&payto_kycauth, - "%s%cmessage=KYC:%s", - exchange_account_payto, - (NULL == strchr (exchange_account_payto, - '?')) - ? '?' - : '&', - merchant_pub_str); - else - GNUNET_asprintf (&payto_kycauth, - "%s%camount=%s&message=KYC:%s", - exchange_account_payto, - (NULL == strchr (exchange_account_payto, - '?')) - ? '?' - : '&', - TALER_amount2s (&kyc_amount), - merchant_pub_str); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found account %s where KYC auth is possible\n", - payto_kycauth); - GNUNET_assert (0 == - json_array_append_new (ekr->pkaa, - json_string (payto_kycauth))); - GNUNET_free (payto_kycauth); - } - } - GNUNET_free (np.normalized_payto); - GNUNET_free (merchant_pub_str); -} - - -/** - * Function called with the result of a #TMH_EXCHANGES_keys4exchange() - * operation. Runs the KYC check against the exchange. - * - * @param cls closure with our `struct ExchangeKycRequest *` - * @param keys keys of the exchange context - * @param exchange representation of the exchange - */ -static void -kyc_with_exchange (void *cls, - struct TALER_EXCHANGE_Keys *keys, - struct TMH_Exchange *exchange) -{ - struct ExchangeKycRequest *ekr = cls; - - (void) exchange; - ekr->fo = NULL; - if (NULL == keys) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to download `%skeys`\n", - ekr->exchange_url); - ekr->no_keys = true; - ekr_finished (ekr); - return; - } - ekr->keys = TALER_EXCHANGE_keys_incref (keys); - if (! ekr->auth_ok) - { - determine_eligible_accounts (ekr); - if (0 == json_array_size (ekr->pkaa)) - { - /* No KYC auth wire transfers are possible to this exchange from - our merchant bank account, so we cannot use this account with - this exchange if it has any KYC requirements! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC auth to `%s' impossible for merchant account `%s'\n", - ekr->exchange_url, - ekr->payto_uri.full_payto); - ekr->kyc_auth_conflict = true; - } - } - ekr_finished (ekr); -} - - -/** - * Closure for add_unreachable_status(). - */ -struct UnreachableContext -{ - /** - * Where we are building the response. - */ - struct KycContext *kc; - - /** - * Pointer to our account hash. - */ - const struct TALER_MerchantWireHashP *h_wire; - - /** - * Bank account for which we have no status from any exchange. - */ - struct TALER_FullPayto payto_uri; - -}; - -/** - * Add all trusted exchanges with "unknown" status for the - * bank account given in the context. - * - * @param cls a `struct UnreachableContext` - * @param url base URL of the exchange - * @param exchange internal handle for the exchange - */ -static void -add_unreachable_status (void *cls, - const char *url, - const struct TMH_Exchange *exchange) -{ - struct UnreachableContext *uc = cls; - struct KycContext *kc = uc->kc; - - clear_status (kc, - "exchange-unreachable"); - GNUNET_assert ( - 0 == - json_array_append_new ( - kc->kycs_data, - GNUNET_JSON_PACK ( - TALER_JSON_pack_full_payto ( - "payto_uri", - uc->payto_uri), - GNUNET_JSON_pack_data_auto ( - "h_wire", - uc->h_wire), - GNUNET_JSON_pack_string ( - "exchange_currency", - TMH_EXCHANGES_get_currency (exchange)), - GNUNET_JSON_pack_string ( - "status", - "exchange-unreachable"), - GNUNET_JSON_pack_string ( - "exchange_url", - url), - GNUNET_JSON_pack_bool ("no_keys", - true), - GNUNET_JSON_pack_bool ("auth_conflict", - false), - GNUNET_JSON_pack_uint64 ("exchange_http_status", - 0) - ))); - -} - - -/** - * Function called from account_kyc_get_status() with KYC status information - * for this merchant. - * - * @param cls our `struct KycContext *` - * @param h_wire hash of the wire account - * @param payto_uri payto:// URI of the merchant's bank account - * @param exchange_url base URL of the exchange for which this is a status - * @param last_check when did we last get an update on our KYC status from the exchange - * @param kyc_ok true if we satisfied the KYC requirements - * @param access_token access token for the KYC SPA, NULL if we cannot access it yet (need KYC auth wire transfer) - * @param last_http_status last HTTP status from /kyc-check - * @param last_ec last Taler error code from /kyc-check - * @param in_aml_review true if the account is pending review - * @param jlimits JSON array of applicable AccountLimits, or NULL if unknown (like defaults apply) - */ -static void -kyc_status_cb ( - void *cls, - const struct TALER_MerchantWireHashP *h_wire, - struct TALER_FullPayto payto_uri, - const char *exchange_url, - struct GNUNET_TIME_Timestamp last_check, - bool kyc_ok, - const struct TALER_AccountAccessTokenP *access_token, - unsigned int last_http_status, - enum TALER_ErrorCode last_ec, - bool in_aml_review, - const json_t *jlimits) -{ - struct KycContext *kc = cls; - struct ExchangeKycRequest *ekr; - - if (NULL == exchange_url) - { - struct UnreachableContext uc = { - .kc = kc, - .h_wire = h_wire, - .payto_uri = payto_uri - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Account has unknown KYC status for all exchanges.\n"); - TMH_exchange_get_trusted (&add_unreachable_status, - &uc); - kc_respond (kc); - return; - } - if (! TMH_EXCHANGES_check_trusted (exchange_url)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Skipping exchange `%s': not trusted\n", - exchange_url); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "KYC status for `%s' at `%s' is %u/%s/%s/%s\n", - payto_uri.full_payto, - exchange_url, - last_http_status, - kyc_ok ? "KYC OK" : "KYC NEEDED", - in_aml_review ? "IN AML REVIEW" : "NO AML REVIEW", - NULL == jlimits ? "DEFAULT LIMITS" : "CUSTOM LIMITS"); - switch (kc->lpt) - { - case TALER_EXCHANGE_KLPT_NONE: - break; - case TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER: - if (NULL != access_token) - kc->return_immediately = true; - break; - case TALER_EXCHANGE_KLPT_INVESTIGATION_DONE: - if (! in_aml_review) - kc->return_immediately = true; - break; - case TALER_EXCHANGE_KLPT_KYC_OK: - if (kyc_ok) - kc->return_immediately = true; - break; - } - ekr = GNUNET_new (struct ExchangeKycRequest); - GNUNET_CONTAINER_DLL_insert (kc->exchange_pending_head, - kc->exchange_pending_tail, - ekr); - ekr->last_http_status = last_http_status; - ekr->last_ec = last_ec; - if (NULL != jlimits) - ekr->jlimits = json_incref ((json_t *) jlimits); - ekr->h_wire = *h_wire; - ekr->exchange_url = GNUNET_strdup (exchange_url); - ekr->payto_uri.full_payto - = GNUNET_strdup (payto_uri.full_payto); - ekr->last_check = last_check; - ekr->kyc_ok = kyc_ok; - ekr->kc = kc; - ekr->in_aml_review = in_aml_review; - ekr->auth_ok = (NULL != access_token); - if ( (! ekr->auth_ok) || - (NULL == ekr->jlimits) ) - { - /* Figure out wire transfer instructions */ - if (GNUNET_NO == kc->suspended) - { - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - } - ekr->fo = TMH_EXCHANGES_keys4exchange ( - exchange_url, - false, - &kyc_with_exchange, - ekr); - if (NULL == ekr->fo) - { - GNUNET_break (0); - ekr_finished (ekr); - return; - } - return; - } - ekr->access_token = *access_token; - ekr_finished (ekr); -} - - -/** - * Check the KYC status of an instance. - * - * @param mi instance to check KYC status of - * @param connection the MHD connection to handle - * @param[in,out] hc context with further information about the request - * @return MHD result code - */ -static MHD_RESULT -get_instances_ID_kyc ( - struct TMH_MerchantInstance *mi, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct KycContext *kc = hc->ctx; - - if (NULL == kc) - { - kc = GNUNET_new (struct KycContext); - kc->mi = mi; - hc->ctx = kc; - hc->cc = &kyc_context_cleanup; - GNUNET_CONTAINER_DLL_insert (kc_head, - kc_tail, - kc); - kc->connection = connection; - kc->hc = hc; - kc->kycs_data = json_array (); - GNUNET_assert (NULL != kc->kycs_data); - TALER_MHD_parse_request_timeout (connection, - &kc->timeout); - { - uint64_t num = 0; - int val; - - TALER_MHD_parse_request_number (connection, - "lpt", - &num); - val = (int) num; - if ( (val < 0) || - (val > TALER_EXCHANGE_KLPT_MAX) ) - { - /* Protocol violation, but we can be graceful and - just ignore the long polling! */ - GNUNET_break_op (0); - val = TALER_EXCHANGE_KLPT_NONE; - } - kc->lpt = (enum TALER_EXCHANGE_KycLongPollTarget) val; - } - kc->return_immediately - = (TALER_EXCHANGE_KLPT_NONE == kc->lpt); - /* process 'exchange_url' argument */ - kc->exchange_url = MHD_lookup_connection_value ( - connection, - MHD_GET_ARGUMENT_KIND, - "exchange_url"); - if ( (NULL != kc->exchange_url) && - ( (! TALER_url_valid_charset (kc->exchange_url)) || - (! TALER_is_web_url (kc->exchange_url)) ) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "exchange_url must be a valid HTTP(s) URL"); - } - kc->lp_status = MHD_lookup_connection_value ( - connection, - MHD_GET_ARGUMENT_KIND, - "lp_status"); - kc->lp_not_status = MHD_lookup_connection_value ( - connection, - MHD_GET_ARGUMENT_KIND, - "lp_not_status"); - TALER_MHD_parse_request_arg_auto (connection, - "h_wire", - &kc->h_wire, - kc->have_h_wire); - TALER_MHD_parse_request_arg_auto (connection, - "lp_not_etag", - &kc->lp_not_etag, - kc->have_lp_not_etag); - - if (! GNUNET_TIME_absolute_is_past (kc->timeout)) - { - if (kc->have_h_wire) - { - struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = { - .header.size = htons (sizeof (ev)), - .header.type = htons ( - TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_STATUS_CHANGED - ), - .h_wire = kc->h_wire - }; - - kc->eh = TMH_db->event_listen ( - TMH_db->cls, - &ev.header, - GNUNET_TIME_absolute_get_remaining (kc->timeout), - &kyc_change_cb, - kc); - } - else - { - struct GNUNET_DB_EventHeaderP hdr = { - .size = htons (sizeof (hdr)), - .type = htons (TALER_DBEVENT_MERCHANT_KYC_STATUS_CHANGED) - }; - - kc->eh = TMH_db->event_listen ( - TMH_db->cls, - &hdr, - GNUNET_TIME_absolute_get_remaining (kc->timeout), - &kyc_change_cb, - kc); - } - } /* end register LISTEN hooks */ - } /* end 1st time initialization */ - - if (GNUNET_SYSERR == kc->suspended) - return MHD_NO; /* during shutdown, we don't generate any more replies */ - GNUNET_assert (GNUNET_NO == kc->suspended); - - if (NULL != kc->response) - return MHD_queue_response (connection, - kc->response_code, - kc->response); - - /* Check our database */ - { - enum GNUNET_DB_QueryStatus qs; - - GNUNET_break (0 == - json_array_clear (kc->kycs_data)); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checking KYC status for %s (%d/%s)\n", - mi->settings.id, - kc->have_h_wire, - kc->exchange_url); - /* We may run repeatedly due to long-polling; clear data - from previous runs first */ - GNUNET_break (0 == json_array_clear (kc->kycs_data)); - qs = TMH_db->account_kyc_get_status ( - TMH_db->cls, - mi->settings.id, - kc->have_h_wire - ? &kc->h_wire - : NULL, - kc->exchange_url, - &kyc_status_cb, - kc); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "account_kyc_get_status returned %d records\n", - (int) qs); - if (qs < 0) - { - /* Database error */ - GNUNET_break (0); - if (GNUNET_YES == kc->suspended) - { - /* must have suspended before DB error, resume! */ - MHD_resume_connection (connection); - kc->suspended = GNUNET_NO; - } - return TALER_MHD_reply_with_ec ( - connection, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "account_kyc_get_status"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* We use an Etag of all zeros for the 204 status code */ - static struct GNUNET_ShortHashCode zero_etag; - - /* no matching accounts, could not have suspended */ - GNUNET_assert (GNUNET_NO == kc->suspended); - if (kc->have_lp_not_etag && - (0 == GNUNET_memcmp (&zero_etag, - &kc->lp_not_etag)) && - (! GNUNET_TIME_absolute_is_past (kc->timeout)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No matching accounts, suspending to wait for this to change\n"); - MHD_suspend_connection (kc->connection); - kc->suspended = GNUNET_YES; - return MHD_YES; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No matching accounts, returning empty response\n"); - kc->response_code = MHD_HTTP_NO_CONTENT; - kc->response = MHD_create_response_from_buffer_static (0, - NULL); - TALER_MHD_add_global_headers (kc->response, - false); - { - char *etag; - - etag = GNUNET_STRINGS_data_to_string_alloc (&zero_etag, - sizeof (zero_etag)); - GNUNET_break (MHD_YES == - MHD_add_response_header (kc->response, - MHD_HTTP_HEADER_ETAG, - etag)); - GNUNET_free (etag); - } - return MHD_queue_response (connection, - kc->response_code, - kc->response); - } - } - if (GNUNET_YES == kc->suspended) - return MHD_YES; - /* Should have generated a response */ - GNUNET_break (NULL != kc->response); - return MHD_queue_response (connection, - kc->response_code, - kc->response); -} - - -MHD_RESULT -TMH_private_get_instances_ID_kyc ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - - (void) rh; - return get_instances_ID_kyc (mi, - connection, - hc); -} - - -MHD_RESULT -TMH_private_get_instances_default_ID_kyc ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi; - - (void) rh; - mi = TMH_lookup_instance (hc->infix); - if (NULL == mi) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); - } - return get_instances_ID_kyc (mi, - connection, - hc); -} - - -/* end of taler-merchant-httpd_private-get-instances-ID-kyc.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.h b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.h @@ -1,67 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-get-instances-ID-kyc.h - * @brief implements GET /instances/$ID/kyc request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_KYC_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_KYC_H -#include "taler-merchant-httpd.h" - - -/** - * Force all KYC contexts to be resumed as we are about - * to shut down MHD. - */ -void -TMH_force_kyc_resume (void); - - -/** - * Change the instance's kyc settings. - * This is the handler called using the instance's own kycentication. - * - * @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_instances_ID_kyc (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Change the instance's kyc settings. - * This is the handler called using the default instance's kycentication. - * - * @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_instances_default_ID_kyc (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c @@ -1,118 +0,0 @@ -/* - 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-httpd_private-get-instances-ID-tokens.c - * @brief implement GET /tokens - * @author Martin Schanzenbach - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_auth.h" -#include "taler-merchant-httpd_private-get-instances-ID-tokens.h" - - -/** - * Add token details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param creation_time when the token was created - * @param expiration_time when the token will expire - * @param scope internal scope identifier for the token (mapped to string) - * @param description human-readable purpose or context of the token - * @param serial serial (row) number of the product in the database - */ -static void -add_token (void *cls, - struct GNUNET_TIME_Timestamp creation_time, - struct GNUNET_TIME_Timestamp expiration_time, - uint32_t scope, - const char *description, - uint64_t serial) -{ - json_t *pa = cls; - bool refreshable; - const char *as; - - as = TMH_get_name_by_scope (scope, - &refreshable); - if (NULL == as) - { - GNUNET_break (0); - return; - } - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("creation_time", - creation_time), - GNUNET_JSON_pack_timestamp ("expiration", - expiration_time), - GNUNET_JSON_pack_string ("scope", - as), - GNUNET_JSON_pack_bool ("refreshable", - refreshable), - GNUNET_JSON_pack_string ("description", - description), - GNUNET_JSON_pack_uint64 ("serial", - serial)))); -} - - -MHD_RESULT -TMH_private_get_instances_ID_tokens (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *ta; - enum GNUNET_DB_QueryStatus qs; - int64_t limit = -20; /* default */ - uint64_t offset; - - 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); - ta = json_array (); - GNUNET_assert (NULL != ta); - qs = TMH_db->lookup_login_tokens (TMH_db->cls, - hc->instance->settings.id, - offset, - limit, - &add_token, - ta); - if (0 > qs) - { - GNUNET_break (0); - json_decref (ta); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("tokens", - ta)); -} - - -/* end of taler-merchant-httpd_private-get-instances-ID-tokens.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.h b/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.h @@ -1,41 +0,0 @@ -/* - 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-httpd_private-get-instances-ID-tokens.h - * @brief implement GET /tokens - * @author Martin Schanzenbach - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_TOKENS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_TOKENS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/tokens" 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_instances_ID_tokens (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-instances-ID-tokens.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID.c b/src/backend/taler-merchant-httpd_private-get-instances-ID.c @@ -1,156 +0,0 @@ -/* - This file is part of TALER - (C) 2019-2023 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-httpd_private-get-instances-ID.c - * @brief implement GET /instances/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-instances-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a GET "/instances/$ID" request. - * - * @param mi instance to return information about - * @param connection the MHD connection to handle - * @return MHD result code - */ -static MHD_RESULT -get_instances_ID (struct TMH_MerchantInstance *mi, - struct MHD_Connection *connection) -{ - json_t *ja; - json_t *auth; - - GNUNET_assert (NULL != mi); - ja = json_array (); - GNUNET_assert (NULL != ja); - for (struct TMH_WireMethod *wm = mi->wm_head; - NULL != wm; - wm = wm->next) - { - GNUNET_assert ( - 0 == - json_array_append_new ( - ja, - GNUNET_JSON_PACK ( - TALER_JSON_pack_full_payto ( - "payto_uri", - wm->payto_uri), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ( - "credit_facade_url", - wm->credit_facade_url)), - GNUNET_JSON_pack_data_auto ("h_wire", - &wm->h_wire), - GNUNET_JSON_pack_data_auto ( - "salt", - &wm->wire_salt), - GNUNET_JSON_pack_bool ("active", - wm->active)))); - } - if (GNUNET_YES == TMH_strict_v19) - { - // When pre v19 is deprecated this if guard can be removed - // and the code below should never return "external" - GNUNET_assert (! GNUNET_is_zero (&mi->auth.auth_hash)); - } - auth = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("method", - GNUNET_is_zero (&mi->auth.auth_hash) - ? "external" - : "token")); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("accounts", - ja), - GNUNET_JSON_pack_string ("name", - mi->settings.name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("website", - mi->settings.website)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("email", - mi->settings.email)), - GNUNET_JSON_pack_bool ("email_validated", - mi->settings.email_validated), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("phone_number", - mi->settings.phone)), - GNUNET_JSON_pack_bool ("phone_validated", - mi->settings.phone_validated), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("logo", - mi->settings.logo)), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &mi->merchant_pub), - GNUNET_JSON_pack_object_incref ("address", - mi->settings.address), - GNUNET_JSON_pack_object_incref ("jurisdiction", - mi->settings.jurisdiction), - GNUNET_JSON_pack_bool ("use_stefan", - mi->settings.use_stefan), - GNUNET_JSON_pack_time_rel ("default_wire_transfer_delay", - mi->settings.default_wire_transfer_delay), - GNUNET_JSON_pack_time_rel ("default_pay_delay", - mi->settings.default_pay_delay), - GNUNET_JSON_pack_time_rel ("default_refund_delay", - mi->settings.default_refund_delay), - GNUNET_JSON_pack_time_rounder_interval ( - "default_wire_transfer_rounding_interval", - mi->settings.default_wire_transfer_rounding_interval), - GNUNET_JSON_pack_object_steal ("auth", - auth)); -} - - -MHD_RESULT -TMH_private_get_instances_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - - return get_instances_ID (mi, - connection); -} - - -MHD_RESULT -TMH_private_get_instances_default_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi; - - mi = TMH_lookup_instance (hc->infix); - if ( (NULL == mi) || - (mi->deleted) ) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); - } - return get_instances_ID (mi, - connection); -} - - -/* end of taler-merchant-httpd_private-get-instances-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID.h b/src/backend/taler-merchant-httpd_private-get-instances-ID.h @@ -1,56 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-get-instances-ID.h - * @brief implement GET /instances/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/instances/$ID/private" 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_instances_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Handle a GET "/management/instances/$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_instances_default_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/* end of taler-merchant-httpd_private-get-instances-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-instances.c b/src/backend/taler-merchant-httpd_private-get-instances.c @@ -1,125 +0,0 @@ -/* - This file is part of TALER - (C) 2019-2023 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-httpd_private-get-instances.c - * @brief implement GET /instances - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-instances.h" - -/** - * Add merchant instance to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param key unused - * @param value a `struct TMH_MerchantInstance *` - * @return #GNUNET_OK (continue to iterate) - */ -static enum GNUNET_GenericReturnValue -add_instance (void *cls, - const struct GNUNET_HashCode *key, - void *value) -{ - json_t *ja = cls; - struct TMH_MerchantInstance *mi = value; - json_t *pta; - - (void) key; - /* Compile array of all unique wire methods supported by this - instance */ - pta = json_array (); - GNUNET_assert (NULL != pta); - for (struct TMH_WireMethod *wm = mi->wm_head; - NULL != wm; - wm = wm->next) - { - bool duplicate = false; - - if (! wm->active) - break; - /* Yes, O(n^2), but really how many bank accounts can an - instance realistically have for this to matter? */ - for (struct TMH_WireMethod *pm = mi->wm_head; - pm != wm; - pm = pm->next) - if (0 == strcasecmp (pm->wire_method, - wm->wire_method)) - { - duplicate = true; - break; - } - if (duplicate) - continue; - GNUNET_assert (0 == - json_array_append_new (pta, - json_string (wm->wire_method))); - } - GNUNET_assert (0 == - json_array_append_new ( - ja, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - mi->settings.name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("website", - mi->settings.website)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("logo", - mi->settings.logo)), - GNUNET_JSON_pack_string ("id", - mi->settings.id), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &mi->merchant_pub), - GNUNET_JSON_pack_array_steal ("payment_targets", - pta), - GNUNET_JSON_pack_bool ("deleted", - mi->deleted)))); - return GNUNET_OK; -} - - -/** - * Handle a GET "/instances" 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_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *ia; - - (void) rh; - (void) hc; - ia = json_array (); - GNUNET_assert (NULL != ia); - GNUNET_CONTAINER_multihashmap_iterate (TMH_by_id_map, - &add_instance, - ia); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("instances", - ia)); -} - - -/* end of taler-merchant-httpd_private-get-instances.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-instances.h b/src/backend/taler-merchant-httpd_private-get-instances.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-get-instances.h - * @brief implement GET /instances - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/instances" 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_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-instances.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -1,1795 +0,0 @@ -/* - This file is part of TALER - (C) 2017-2024, 2026 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/> -*/ -/** - * @file taler-merchant-httpd_private-get-orders-ID.c - * @brief implementation of GET /private/orders/ID handler - * @author Florian Dold - * @author Christian Grothoff - * @author Bohdan Potuzhnyi - * @author Iván Ávalos - */ -#include "taler/platform.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_error_codes.h> -#include <taler/taler_util.h> -#include <gnunet/gnunet_common.h> -#include <gnunet/gnunet_json_lib.h> -#include "taler/taler_merchant_util.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler-merchant-httpd_private-get-orders-ID.h" - -/** - * Data structure we keep for a check payment request. - */ -struct GetOrderRequestContext; - - -/** - * Request to an exchange for details about wire transfers - * in response to a coin's deposit operation. - */ -struct TransferQuery -{ - - /** - * Kept in a DLL. - */ - struct TransferQuery *next; - - /** - * Kept in a DLL. - */ - struct TransferQuery *prev; - - /** - * Base URL of the exchange. - */ - char *exchange_url; - - /** - * Overall request this TQ belongs with. - */ - struct GetOrderRequestContext *gorc; - - /** - * Hash of the merchant's bank account the transfer (presumably) went to. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * Value deposited (including deposit fee). - */ - struct TALER_Amount amount_with_fee; - - /** - * Deposit fee paid for this coin. - */ - struct TALER_Amount deposit_fee; - - /** - * Public key of the coin this is about. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Which deposit operation is this about? - */ - uint64_t deposit_serial; - -}; - - -/** - * Phases of order processing. - */ -enum GetOrderPhase -{ - /** - * Initialization. - */ - GOP_INIT = 0, - - /** - * Obtain contract terms from database. - */ - GOP_FETCH_CONTRACT = 1, - - /** - * Parse the contract terms. - */ - GOP_PARSE_CONTRACT = 2, - - /** - * Check if the contract was fully paid. - */ - GOP_CHECK_PAID = 3, - - /** - * Check if the wallet may have purchased an equivalent - * order before and we need to redirect the wallet to - * an existing paid order. - */ - GOP_CHECK_REPURCHASE = 4, - - /** - * Terminate processing of unpaid orders, either by - * suspending until payment or by returning the - * unpaid order status. - */ - GOP_UNPAID_FINISH = 5, - - /** - * Check if the (paid) order was refunded. - */ - GOP_CHECK_REFUNDS = 6, - - /** - * Load all deposits associated with the order. - */ - GOP_CHECK_DEPOSITS = 7, - - /** - * Check local records for transfers of funds to - * the merchant. - */ - GOP_CHECK_LOCAL_TRANSFERS = 8, - - /** - * Generate final comprehensive result. - */ - GOP_REPLY_RESULT = 9, - - /** - * End with the HTTP status and error code in - * wire_hc and wire_ec. - */ - GOP_ERROR = 10, - - /** - * We are suspended awaiting payment. - */ - GOP_SUSPENDED_ON_UNPAID = 11, - - /** - * Processing is done, return #MHD_YES. - */ - GOP_END_YES = 12, - - /** - * Processing is done, return #MHD_NO. - */ - GOP_END_NO = 13 - -}; - - -/** - * Data structure we keep for a check payment request. - */ -struct GetOrderRequestContext -{ - - /** - * Processing phase we are in. - */ - enum GetOrderPhase phase; - - /** - * Entry in the #resume_timeout_heap for this check payment, if we are - * suspended. - */ - struct TMH_SuspendedConnection sc; - - /** - * Which merchant instance is this for? - */ - struct TMH_HandlerContext *hc; - - /** - * session of the client - */ - const char *session_id; - - /** - * Kept in a DLL while suspended on exchange. - */ - struct GetOrderRequestContext *next; - - /** - * Kept in a DLL while suspended on exchange. - */ - struct GetOrderRequestContext *prev; - - /** - * Head of DLL of individual queries for transfer data. - */ - struct TransferQuery *tq_head; - - /** - * Tail of DLL of individual queries for transfer data. - */ - struct TransferQuery *tq_tail; - - /** - * Timeout task while waiting on exchange. - */ - struct GNUNET_SCHEDULER_Task *tt; - - /** - * Database event we are waiting on to be resuming - * for payment or refunds. - */ - struct GNUNET_DB_EventHandler *eh; - - /** - * Database event we are waiting on to be resuming - * for session capture. - */ - struct GNUNET_DB_EventHandler *session_eh; - - /** - * Contract terms of the payment we are checking. NULL when they - * are not (yet) known. - */ - json_t *contract_terms_json; - - /** - * Parsed contract terms, NULL when parsing failed - */ - struct TALER_MERCHANT_Contract *contract_terms; - - /** - * Claim token of the order. - */ - struct TALER_ClaimTokenP claim_token; - - /** - * Timestamp of the last payment. - */ - struct GNUNET_TIME_Timestamp last_payment; - - /** - * Wire details for the payment, to be returned in the reply. NULL - * if not available. - */ - json_t *wire_details; - - /** - * Details about refunds, NULL if there are no refunds. - */ - json_t *refund_details; - - /** - * Amount of the order, unset for unpaid v1 orders. - */ - struct TALER_Amount contract_amount; - - /** - * Hash over the @e contract_terms. - */ - struct TALER_PrivateContractHashP h_contract_terms; - - /** - * Set to the Etag of a response already known to the - * client. We should only return from long-polling - * on timeout (with "Not Modified") or when the Etag - * of the response differs from what is given here. - * Only set if @a have_lp_not_etag is true. - * Set from "lp_etag" query parameter. - */ - struct GNUNET_ShortHashCode lp_not_etag; - - /** - * Total amount the exchange deposited into our bank account - * (confirmed or unconfirmed), excluding fees. - */ - struct TALER_Amount deposits_total; - - /** - * Total amount in deposit fees we paid for all coins. - */ - struct TALER_Amount deposit_fees_total; - - /** - * Total amount in deposit fees cancelled due to refunds for all coins. - */ - struct TALER_Amount deposit_fees_refunded_total; - - /** - * Total value of the coins that the exchange deposited into our bank - * account (confirmed or unconfirmed), including deposit fees. - */ - struct TALER_Amount value_total; - - /** - * Serial ID of the order. - */ - uint64_t order_serial; - - /** - * Index of selected choice from ``choices`` array in the contract_terms. - * Is -1 for orders without choices. - */ - int16_t choice_index; - - /** - * Total refunds granted for this payment. Only initialized - * if @e refunded is set to true. - */ - struct TALER_Amount refund_amount; - - /** - * Exchange HTTP error code encountered while trying to determine wire transfer - * details. #TALER_EC_NONE for no error encountered. - */ - unsigned int exchange_hc; - - /** - * Exchange error code encountered while trying to determine wire transfer - * details. #TALER_EC_NONE for no error encountered. - */ - enum TALER_ErrorCode exchange_ec; - - /** - * Error code encountered while trying to determine wire transfer - * details. #TALER_EC_NONE for no error encountered. - */ - enum TALER_ErrorCode wire_ec; - - /** - * Set to YES if refunded orders should be included when - * doing repurchase detection. - */ - enum TALER_EXCHANGE_YesNoAll allow_refunded_for_repurchase; - - /** - * HTTP status to return with @e wire_ec, 0 if @e wire_ec is #TALER_EC_NONE. - */ - unsigned int wire_hc; - - /** - * Did we suspend @a connection and are thus in - * the #gorc_head DLL (#GNUNET_YES). Set to - * #GNUNET_NO if we are not suspended, and to - * #GNUNET_SYSERR if we should close the connection - * without a response due to shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * Set to true if this payment has been refunded and - * @e refund_amount is initialized. - */ - bool refunded; - - /** - * True if @e lp_not_etag was given. - */ - bool have_lp_not_etag; - - /** - * True if the order was paid. - */ - bool paid; - - /** - * True if the paid session in the database matches - * our @e session_id. - */ - bool paid_session_matches; - - /** - * True if the exchange wired the money to the merchant. - */ - bool wired; - - /** - * True if the order remains unclaimed. - */ - bool order_only; - - /** - * Set to true if this payment has been refunded and - * some refunds remain to be picked up by the wallet. - */ - bool refund_pending; - - /** - * Set to true if our database (incorrectly) has refunds - * in a different currency than the currency of the - * original payment for the order. - */ - bool refund_currency_mismatch; - - /** - * Set to true if our database (incorrectly) has deposits - * in a different currency than the currency of the - * original payment for the order. - */ - bool deposit_currency_mismatch; -}; - - -/** - * Head of list of suspended requests waiting on the exchange. - */ -static struct GetOrderRequestContext *gorc_head; - -/** - * Tail of list of suspended requests waiting on the exchange. - */ -static struct GetOrderRequestContext *gorc_tail; - - -void -TMH_force_gorc_resume (void) -{ - struct GetOrderRequestContext *gorc; - - while (NULL != (gorc = gorc_head)) - { - GNUNET_CONTAINER_DLL_remove (gorc_head, - gorc_tail, - gorc); - GNUNET_assert (GNUNET_YES == gorc->suspended); - gorc->suspended = GNUNET_SYSERR; - MHD_resume_connection (gorc->sc.con); - } -} - - -/** - * We have received a trigger from the database - * that we should (possibly) resume the request. - * - * @param cls a `struct GetOrderRequestContext` to resume - * @param extra string encoding refund amount (or NULL) - * @param extra_size number of bytes in @a extra - */ -static void -resume_by_event (void *cls, - const void *extra, - size_t extra_size) -{ - struct GetOrderRequestContext *gorc = cls; - - (void) extra; - (void) extra_size; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming request for order %s by trigger\n", - gorc->hc->infix); - if (GNUNET_NO == gorc->suspended) - return; /* duplicate event is possible */ - gorc->suspended = GNUNET_NO; - gorc->phase = GOP_FETCH_CONTRACT; - GNUNET_CONTAINER_DLL_remove (gorc_head, - gorc_tail, - gorc); - MHD_resume_connection (gorc->sc.con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -/** - * Clean up the session state for a GET /private/order/ID request. - * - * @param cls closure, must be a `struct GetOrderRequestContext *` - */ -static void -gorc_cleanup (void *cls) -{ - struct GetOrderRequestContext *gorc = cls; - struct TransferQuery *tq; - - while (NULL != (tq = gorc->tq_head)) - { - GNUNET_CONTAINER_DLL_remove (gorc->tq_head, - gorc->tq_tail, - tq); - GNUNET_free (tq->exchange_url); - GNUNET_free (tq); - } - - if (NULL != gorc->contract_terms_json) - json_decref (gorc->contract_terms_json); - if (NULL != gorc->contract_terms) - { - TALER_MERCHANT_contract_free (gorc->contract_terms); - gorc->contract_terms = NULL; - } - if (NULL != gorc->wire_details) - json_decref (gorc->wire_details); - if (NULL != gorc->refund_details) - json_decref (gorc->refund_details); - if (NULL != gorc->tt) - { - GNUNET_SCHEDULER_cancel (gorc->tt); - gorc->tt = NULL; - } - if (NULL != gorc->eh) - { - TMH_db->event_listen_cancel (gorc->eh); - gorc->eh = NULL; - } - if (NULL != gorc->session_eh) - { - TMH_db->event_listen_cancel (gorc->session_eh); - gorc->session_eh = NULL; - } - GNUNET_free (gorc); -} - - -/** - * Processing the request @a gorc is finished, set the - * final return value in phase based on @a mret. - * - * @param[in,out] gorc order context to initialize - * @param mret MHD HTTP response status to return - */ -static void -phase_end (struct GetOrderRequestContext *gorc, - MHD_RESULT mret) -{ - gorc->phase = (MHD_YES == mret) - ? GOP_END_YES - : GOP_END_NO; -} - - -/** - * Initialize event callbacks for the order processing. - * - * @param[in,out] gorc order context to initialize - */ -static void -phase_init (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), - .merchant_pub = hc->instance->merchant_pub - }; - - if (! GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout)) - { - gorc->phase++; - return; - } - - GNUNET_CRYPTO_hash (hc->infix, - strlen (hc->infix), - &pay_eh.h_order_id); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing to payment triggers for %p\n", - gorc); - gorc->eh = TMH_db->event_listen ( - TMH_db->cls, - &pay_eh.header, - GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_timeout), - &resume_by_event, - gorc); - if ( (NULL != gorc->session_id) && - (NULL != gorc->contract_terms->fulfillment_url) ) - { - struct TMH_SessionEventP session_eh = { - .header.size = htons (sizeof (session_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Subscribing to session triggers for %p\n", - gorc); - GNUNET_CRYPTO_hash (gorc->session_id, - strlen (gorc->session_id), - &session_eh.h_session_id); - GNUNET_CRYPTO_hash (gorc->contract_terms->fulfillment_url, - strlen (gorc->contract_terms->fulfillment_url), - &session_eh.h_fulfillment_url); - gorc->session_eh - = TMH_db->event_listen ( - TMH_db->cls, - &session_eh.header, - GNUNET_TIME_absolute_get_remaining (gorc->sc.long_poll_timeout), - &resume_by_event, - gorc); - } - gorc->phase++; -} - - -/** - * Obtain latest contract terms from the database. - * - * @param[in,out] gorc order context to update - */ -static void -phase_fetch_contract (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - enum GNUNET_DB_QueryStatus qs; - - if (NULL != gorc->contract_terms_json) - { - /* Free memory filled with old contract terms before fetching the latest - ones from the DB. Note that we cannot simply skip the database - interaction as the contract terms loaded previously might be from an - earlier *unclaimed* order state (which we loaded in a previous - invocation of this function and we are back here due to long polling) - and thus the contract terms could have changed during claiming. Thus, - we need to fetch the latest contract terms from the DB again. */ - json_decref (gorc->contract_terms_json); - gorc->contract_terms_json = NULL; - gorc->order_only = false; - } - TMH_db->preflight (TMH_db->cls); - qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - gorc->session_id, - &gorc->contract_terms_json, - &gorc->order_serial, - &gorc->paid, - &gorc->wired, - &gorc->paid_session_matches, - &gorc->claim_token, - &gorc->choice_index); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "lookup_contract_terms (%s) returned %d\n", - hc->infix, - (int) qs); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "contract terms")); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order %s is %s (%s) according to database, choice %d\n", - hc->infix, - gorc->paid ? "paid" : "unpaid", - gorc->wired ? "wired" : "unwired", - (int) gorc->choice_index); - gorc->phase++; - return; - } - GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs); - GNUNET_assert (! gorc->paid); - /* No contract, only order, fetch from orders table */ - gorc->order_only = true; - { - struct TALER_MerchantPostDataHashP unused; - - /* We need the order for two cases: Either when the contract doesn't exist yet, - * or when the order is claimed but unpaid, and we need the claim token. */ - qs = TMH_db->lookup_order (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &gorc->claim_token, - &unused, - &gorc->contract_terms_json); - } - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "order")); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - hc->infix)); - return; - } - gorc->phase++; -} - - -/** - * Obtain parse contract terms of the order. Extracts the fulfillment URL, - * total amount, summary and timestamp from the contract terms! - * - * @param[in,out] gorc order context to update - */ -static void -phase_parse_contract (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - - if (NULL == gorc->contract_terms) - { - gorc->contract_terms = TALER_MERCHANT_contract_parse ( - gorc->contract_terms_json, - true); - - if (NULL == gorc->contract_terms) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - hc->infix)); - return; - } - } - - switch (gorc->contract_terms->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - gorc->contract_amount = gorc->contract_terms->details.v0.brutto; - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - if (gorc->choice_index >= 0) - { - if (gorc->choice_index >= - gorc->contract_terms->details.v1.choices_len) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL)); - return; - } - - gorc->contract_amount = - gorc->contract_terms->details.v1.choices[gorc->choice_index].amount; - } - else - { - GNUNET_break (gorc->order_only); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Choice index %i for order %s is invalid or not yet available", - gorc->choice_index, - gorc->contract_terms->order_id); - } - break; - default: - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION, - NULL)); - return; - } - } - - if ( (! gorc->order_only) && - (GNUNET_OK != - TALER_JSON_contract_hash (gorc->contract_terms_json, - &gorc->h_contract_terms)) ) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - NULL)); - return; - } - GNUNET_assert (NULL != gorc->contract_terms_json); - GNUNET_assert (NULL != gorc->contract_terms); - gorc->phase++; -} - - -/** - * Check payment status of the order. - * - * @param[in,out] gorc order context to update - */ -static void -phase_check_paid (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - - if (gorc->order_only) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order %s unclaimed, no need to lookup payment status\n", - hc->infix); - GNUNET_assert (! gorc->paid); - GNUNET_assert (! gorc->wired); - gorc->phase++; - return; - } - if (NULL == gorc->session_id) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "No session ID, do not need to lookup session-ID specific payment status (%s/%s)\n", - gorc->paid ? "paid" : "unpaid", - gorc->wired ? "wired" : "unwired"); - gorc->phase++; - return; - } - if (! gorc->paid_session_matches) - { - gorc->paid = false; - gorc->wired = false; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order %s %s for session %s (%s)\n", - hc->infix, - gorc->paid ? "paid" : "unpaid", - gorc->session_id, - gorc->wired ? "wired" : "unwired"); - gorc->phase++; -} - - -/** - * Check if the @a reply satisfies the long-poll not_etag - * constraint. If so, return it as a response for @a gorc, - * otherwise suspend and wait for a change. - * - * @param[in,out] gorc request to handle - * @param reply body for JSON response (#MHD_HTTP_OK) - */ -static void -check_reply (struct GetOrderRequestContext *gorc, - const json_t *reply) -{ - struct GNUNET_ShortHashCode sh; - unsigned int http_response_code; - bool not_modified; - struct MHD_Response *response; - char *can; - - can = TALER_JSON_canonicalize (reply); - GNUNET_assert (GNUNET_YES == - GNUNET_CRYPTO_kdf (&sh, - sizeof (sh), - "GOR-SALT", - strlen ("GOR-SALT"), - can, - strlen (can), - NULL, - 0)); - not_modified = gorc->have_lp_not_etag && - (0 == GNUNET_memcmp (&sh, - &gorc->lp_not_etag)); - - if (not_modified && - (! GNUNET_TIME_absolute_is_past (gorc->sc.long_poll_timeout)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Status unchanged, not returning response yet\n"); - GNUNET_assert (GNUNET_NO == gorc->suspended); - /* note: not necessarily actually unpaid ... */ - GNUNET_CONTAINER_DLL_insert (gorc_head, - gorc_tail, - gorc); - gorc->phase = GOP_SUSPENDED_ON_UNPAID; - gorc->suspended = GNUNET_YES; - MHD_suspend_connection (gorc->sc.con); - GNUNET_free (can); - return; - } - { - const char *inm; - - inm = MHD_lookup_connection_value (gorc->sc.con, - MHD_GET_ARGUMENT_KIND, - MHD_HTTP_HEADER_IF_NONE_MATCH); - if ( (NULL == inm) || - ('"' != inm[0]) || - ('"' != inm[strlen (inm) - 1]) || - (0 != strncmp (inm + 1, - can, - strlen (can))) ) - not_modified = false; /* must return full response */ - } - GNUNET_free (can); - http_response_code = not_modified - ? MHD_HTTP_NOT_MODIFIED - : MHD_HTTP_OK; - response = TALER_MHD_make_json (reply); - { - char *etag; - char *qetag; - - etag = GNUNET_STRINGS_data_to_string_alloc (&sh, - sizeof (sh)); - GNUNET_asprintf (&qetag, - "\"%s\"", - etag); - GNUNET_break (MHD_YES == - MHD_add_response_header (response, - MHD_HTTP_HEADER_ETAG, - qetag)); - GNUNET_free (qetag); - GNUNET_free (etag); - } - - { - MHD_RESULT ret; - - ret = MHD_queue_response (gorc->sc.con, - http_response_code, - response); - MHD_destroy_response (response); - phase_end (gorc, - ret); - } -} - - -/** - * Check if re-purchase detection applies to the order. - * - * @param[in,out] gorc order context to update - */ -static void -phase_check_repurchase (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - char *already_paid_order_id = NULL; - enum GNUNET_DB_QueryStatus qs; - char *taler_pay_uri; - char *order_status_url; - json_t *reply; - - if ( (gorc->paid) || - (NULL == gorc->contract_terms->fulfillment_url) || - (NULL == gorc->session_id) ) - { - /* Repurchase cannot apply */ - gorc->phase++; - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Running re-purchase detection for %s/%s\n", - gorc->session_id, - gorc->contract_terms->fulfillment_url); - qs = TMH_db->lookup_order_by_fulfillment ( - TMH_db->cls, - hc->instance->settings.id, - gorc->contract_terms->fulfillment_url, - gorc->session_id, - TALER_EXCHANGE_YNA_NO != - gorc->allow_refunded_for_repurchase, - &already_paid_order_id); - if (0 > qs) - { - /* single, read-only SQL statements should never cause - serialization problems, and the entry should exist as per above */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "order by fulfillment")); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "No already paid order for %s/%s\n", - gorc->session_id, - gorc->contract_terms->fulfillment_url); - gorc->phase++; - return; - } - - /* User did pay for this order, but under a different session; ask wallet to - switch order ID */ - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Found already paid order %s\n", - already_paid_order_id); - taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, - hc->infix, - gorc->session_id, - hc->instance->settings.id, - &gorc->claim_token); - order_status_url = TMH_make_order_status_url (gorc->sc.con, - hc->infix, - gorc->session_id, - hc->instance->settings.id, - &gorc->claim_token, - NULL); - if ( (NULL == taler_pay_uri) || - (NULL == order_status_url) ) - { - GNUNET_break_op (0); - GNUNET_free (taler_pay_uri); - GNUNET_free (order_status_url); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, - "host")); - return; - } - reply = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("taler_pay_uri", - taler_pay_uri), - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - GNUNET_JSON_pack_string ("order_status", - "unpaid"), - GNUNET_JSON_pack_string ("already_paid_order_id", - already_paid_order_id), - GNUNET_JSON_pack_string ("already_paid_fulfillment_url", - gorc->contract_terms->fulfillment_url), - /* undefined for unpaid v1 contracts */ - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("total_amount", - TALER_amount_is_valid (&gorc->contract_amount) - ? &gorc->contract_amount - : NULL)), - GNUNET_JSON_pack_object_incref ("proto_contract_terms", - gorc->contract_terms_json), - GNUNET_JSON_pack_string ("summary", - gorc->contract_terms->summary), - GNUNET_JSON_pack_timestamp ("pay_deadline", - gorc->contract_terms->pay_deadline), - GNUNET_JSON_pack_timestamp ("creation_time", - gorc->contract_terms->timestamp)); - - GNUNET_free (order_status_url); - GNUNET_free (taler_pay_uri); - GNUNET_free (already_paid_order_id); - check_reply (gorc, - reply); - json_decref (reply); -} - - -/** - * Check if we should suspend until the order is paid. - * - * @param[in,out] gorc order context to update - */ -static void -phase_unpaid_finish (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - char *order_status_url; - - if (gorc->paid) - { - gorc->phase++; - return; - } - /* User never paid for this order, suspend waiting - on payment or return details. */ - if (GNUNET_TIME_absolute_is_future (gorc->sc.long_poll_timeout) && - (! gorc->have_lp_not_etag) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending GET /private/orders/%s\n", - hc->infix); - GNUNET_CONTAINER_DLL_insert (gorc_head, - gorc_tail, - gorc); - gorc->phase = GOP_SUSPENDED_ON_UNPAID; - gorc->suspended = GNUNET_YES; - MHD_suspend_connection (gorc->sc.con); - return; - } - order_status_url = TMH_make_order_status_url (gorc->sc.con, - hc->infix, - gorc->session_id, - hc->instance->settings.id, - &gorc->claim_token, - NULL); - if (! gorc->order_only) - { - json_t *reply; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order %s claimed but not paid yet\n", - hc->infix); - reply = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - GNUNET_JSON_pack_object_incref ("contract_terms", - gorc->contract_terms_json), - GNUNET_JSON_pack_string ("order_status", - "claimed")); - GNUNET_free (order_status_url); - check_reply (gorc, - reply); - json_decref (reply); - return; - } - { - char *taler_pay_uri; - json_t *reply; - - taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con, - hc->infix, - gorc->session_id, - hc->instance->settings.id, - &gorc->claim_token); - reply = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("taler_pay_uri", - taler_pay_uri), - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - GNUNET_JSON_pack_string ("order_status", - "unpaid"), - GNUNET_JSON_pack_object_incref ("proto_contract_terms", - gorc->contract_terms_json), - /* undefined for unpaid v1 contracts */ - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("total_amount", - &gorc->contract_amount)), - GNUNET_JSON_pack_string ("summary", - gorc->contract_terms->summary), - GNUNET_JSON_pack_timestamp ("creation_time", - gorc->contract_terms->timestamp)); - check_reply (gorc, - reply); - GNUNET_free (taler_pay_uri); - } - GNUNET_free (order_status_url); -} - - -/** - * Function called with information about a refund. - * It is responsible for summing up the refund amount. - * - * @param cls closure - * @param refund_serial unique serial number of the refund - * @param timestamp time of the refund (for grouping of refunds in the wallet UI) - * @param coin_pub public coin from which the refund comes from - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param rtransaction_id identificator of the refund - * @param reason human-readable explanation of the refund - * @param refund_amount refund amount which is being taken from @a coin_pub - * @param pending true if the this refund was not yet processed by the wallet/exchange - */ -static void -process_refunds_cb ( - void *cls, - uint64_t refund_serial, - struct GNUNET_TIME_Timestamp timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - uint64_t rtransaction_id, - const char *reason, - const struct TALER_Amount *refund_amount, - bool pending) -{ - struct GetOrderRequestContext *gorc = cls; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found refund %llu over %s for reason %s\n", - (unsigned long long) rtransaction_id, - TALER_amount2s (refund_amount), - reason); - GNUNET_assert ( - 0 == - json_array_append_new ( - gorc->refund_details, - GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - refund_amount), - GNUNET_JSON_pack_bool ("pending", - pending), - GNUNET_JSON_pack_timestamp ("timestamp", - timestamp), - GNUNET_JSON_pack_string ("reason", - reason)))); - /* For refunded coins, we are not charged deposit fees, so subtract those - again */ - for (struct TransferQuery *tq = gorc->tq_head; - NULL != tq; - tq = tq->next) - { - if (0 != - strcmp (exchange_url, - tq->exchange_url)) - continue; - if (0 != - GNUNET_memcmp (&tq->coin_pub, - coin_pub)) - continue; - if (GNUNET_OK != - TALER_amount_cmp_currency ( - &gorc->deposit_fees_total, - &tq->deposit_fee)) - { - gorc->refund_currency_mismatch = true; - return; - } - GNUNET_assert ( - 0 <= - TALER_amount_add (&gorc->deposit_fees_refunded_total, - &gorc->deposit_fees_refunded_total, - &tq->deposit_fee)); - } - if (GNUNET_OK != - TALER_amount_cmp_currency ( - &gorc->refund_amount, - refund_amount)) - { - gorc->refund_currency_mismatch = true; - return; - } - GNUNET_assert (0 <= - TALER_amount_add (&gorc->refund_amount, - &gorc->refund_amount, - refund_amount)); - gorc->refunded = true; - gorc->refund_pending |= pending; -} - - -/** - * Check refund status for the order. - * - * @param[in,out] gorc order context to update - */ -static void -phase_check_refunds (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (! gorc->order_only); - GNUNET_assert (gorc->paid); - - /* Accumulate refunds, if any. */ - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (gorc->contract_amount.currency, - &gorc->refund_amount)); - json_array_clear (gorc->refund_details); - qs = TMH_db->lookup_refunds_detailed ( - TMH_db->cls, - hc->instance->settings.id, - &gorc->h_contract_terms, - &process_refunds_cb, - gorc); - if (0 > qs) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "detailed refunds")); - return; - } - if (gorc->refund_currency_mismatch) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "refunds in different currency than original order price")); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Total refunds are %s\n", - TALER_amount2s (&gorc->refund_amount)); - gorc->phase++; -} - - -/** - * Function called with each @a coin_pub that was deposited into the - * @a h_wire account of the merchant for the @a deposit_serial as part - * of the payment for the order identified by @a cls. - * - * Queries the exchange for the payment status associated with the - * given coin. - * - * @param cls a `struct GetOrderRequestContext` - * @param deposit_serial identifies the deposit operation - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param h_wire hash of the merchant's wire account into which the deposit was made - * @param deposit_timestamp when was the deposit made - * @param amount_with_fee amount the exchange will deposit for this coin - * @param deposit_fee fee the exchange will charge for this coin - * @param coin_pub public key of the deposited coin - */ -static void -deposit_cb ( - void *cls, - uint64_t deposit_serial, - const char *exchange_url, - const struct TALER_MerchantWireHashP *h_wire, - struct GNUNET_TIME_Timestamp deposit_timestamp, - const struct TALER_Amount *amount_with_fee, - const struct TALER_Amount *deposit_fee, - const struct TALER_CoinSpendPublicKeyP *coin_pub) -{ - struct GetOrderRequestContext *gorc = cls; - struct TransferQuery *tq; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Checking deposit status for coin %s (over %s)\n", - TALER_B2S (coin_pub), - TALER_amount2s (amount_with_fee)); - gorc->last_payment - = GNUNET_TIME_timestamp_max (gorc->last_payment, - deposit_timestamp); - tq = GNUNET_new (struct TransferQuery); - tq->gorc = gorc; - tq->exchange_url = GNUNET_strdup (exchange_url); - tq->deposit_serial = deposit_serial; - GNUNET_CONTAINER_DLL_insert (gorc->tq_head, - gorc->tq_tail, - tq); - tq->coin_pub = *coin_pub; - tq->h_wire = *h_wire; - tq->amount_with_fee = *amount_with_fee; - tq->deposit_fee = *deposit_fee; -} - - -/** - * Check wire transfer status for the order at the exchange. - * - * @param[in,out] gorc order context to update - */ -static void -phase_check_deposits (struct GetOrderRequestContext *gorc) -{ - GNUNET_assert (! gorc->order_only); - GNUNET_assert (gorc->paid); - - /* amount must be always valid for paid orders */ - GNUNET_assert (GNUNET_OK == - TALER_amount_is_valid (&gorc->contract_amount)); - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (gorc->contract_amount.currency, - &gorc->deposits_total)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (gorc->contract_amount.currency, - &gorc->deposit_fees_total)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (gorc->contract_amount.currency, - &gorc->deposit_fees_refunded_total)); - TMH_db->lookup_deposits_by_order (TMH_db->cls, - gorc->order_serial, - &deposit_cb, - gorc); - gorc->phase++; -} - - -/** - * Function called with available wire details, to be added to - * the response. - * - * @param cls a `struct GetOrderRequestContext` - * @param wtid wire transfer subject of the wire transfer for the coin - * @param exchange_url base URL of the exchange that made the payment - * @param execution_time when was the payment made - * @param deposit_value contribution of the coin to the total wire transfer value - * @param deposit_fee deposit fee charged by the exchange for the coin - * @param transfer_confirmed did the merchant confirm that a wire transfer with - * @a wtid over the total amount happened? - * @param expected_credit_serial row for the expected wire transfer this - * entry references - */ -static void -process_transfer_details ( - void *cls, - const struct TALER_WireTransferIdentifierRawP *wtid, - const char *exchange_url, - struct GNUNET_TIME_Timestamp execution_time, - const struct TALER_Amount *deposit_value, - const struct TALER_Amount *deposit_fee, - bool transfer_confirmed, - uint64_t expected_credit_serial) -{ - struct GetOrderRequestContext *gorc = cls; - json_t *wire_details = gorc->wire_details; - struct TALER_Amount wired; - - if ( (GNUNET_OK != - TALER_amount_cmp_currency (&gorc->deposits_total, - deposit_value)) || - (GNUNET_OK != - TALER_amount_cmp_currency (&gorc->deposit_fees_total, - deposit_fee)) ) - { - GNUNET_break (0); - gorc->deposit_currency_mismatch = true; - return; - } - - /* Compute total amount *wired* */ - GNUNET_assert (0 <= - TALER_amount_add (&gorc->deposits_total, - &gorc->deposits_total, - deposit_value)); - GNUNET_assert (0 <= - TALER_amount_add (&gorc->deposit_fees_total, - &gorc->deposit_fees_total, - deposit_fee)); - GNUNET_assert (0 <= TALER_amount_subtract (&wired, - deposit_value, - deposit_fee)); - GNUNET_assert (0 == - json_array_append_new ( - wire_details, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_data_auto ("wtid", - wtid), - GNUNET_JSON_pack_string ("exchange_url", - exchange_url), - TALER_JSON_pack_amount ("amount", - &wired), - TALER_JSON_pack_amount ("deposit_fee", - deposit_fee), - GNUNET_JSON_pack_timestamp ("execution_time", - execution_time), - GNUNET_JSON_pack_bool ("confirmed", - transfer_confirmed), - GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", - expected_credit_serial)))); -} - - -/** - * Check transfer status in local database. - * - * @param[in,out] gorc order context to update - */ -static void -phase_check_local_transfers (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (gorc->paid); - GNUNET_assert (! gorc->order_only); - - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (gorc->contract_amount.currency, - &gorc->deposits_total)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (gorc->contract_amount.currency, - &gorc->deposit_fees_total)); - GNUNET_assert (NULL != gorc->wire_details); - /* We may be running again due to long-polling, clear state first */ - json_array_clear (gorc->wire_details); - qs = TMH_db->lookup_transfer_details_by_order (TMH_db->cls, - gorc->order_serial, - &process_transfer_details, - gorc); - if (0 > qs) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "transfer details")); - return; - } - if (gorc->deposit_currency_mismatch) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "deposits in different currency than original order price")); - return; - } - - if (! gorc->wired) - { - /* we believe(d) the wire transfer did not happen yet, check if maybe - in light of new evidence it did */ - struct TALER_Amount expect_total; - - if (0 > - TALER_amount_subtract (&expect_total, - &gorc->contract_amount, - &gorc->refund_amount)) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - "refund exceeds contract value")); - return; - } - GNUNET_assert ( - 0 <= - TALER_amount_add (&expect_total, - &expect_total, - &gorc->deposit_fees_refunded_total)); - - if (0 > - TALER_amount_subtract (&expect_total, - &expect_total, - &gorc->deposit_fees_total)) - { - GNUNET_break (0); - phase_end (gorc, - TALER_MHD_reply_with_error ( - gorc->sc.con, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - "deposit fees exceed total minus refunds")); - return; - } - if (0 >= - TALER_amount_cmp (&expect_total, - &gorc->deposits_total)) - { - /* expect_total <= gorc->deposits_total: good: we got the wire transfer */ - gorc->wired = true; - qs = TMH_db->mark_order_wired (TMH_db->cls, - gorc->order_serial); - GNUNET_break (qs >= 0); /* just warn if transaction failed */ - TMH_notify_order_change (hc->instance, - TMH_OSF_PAID - | TMH_OSF_WIRED, - gorc->contract_terms->timestamp, - gorc->order_serial); - } - } - gorc->phase++; -} - - -/** - * Generate final result for the status request. - * - * @param[in,out] gorc order context to update - */ -static void -phase_reply_result (struct GetOrderRequestContext *gorc) -{ - struct TMH_HandlerContext *hc = gorc->hc; - char *order_status_url; - - GNUNET_assert (gorc->paid); - GNUNET_assert (! gorc->order_only); - - { - struct TALER_PrivateContractHashP *h_contract = NULL; - - /* In a session-bound payment, allow the browser to check the order - * status page (e.g. to get a refund). - * - * Note that we don't allow this outside of session-based payment, as - * otherwise this becomes an oracle to convert order_id to h_contract. - */ - if (NULL != gorc->session_id) - h_contract = &gorc->h_contract_terms; - - order_status_url = - TMH_make_order_status_url (gorc->sc.con, - hc->infix, - gorc->session_id, - hc->instance->settings.id, - &gorc->claim_token, - h_contract); - } - if (GNUNET_TIME_absolute_is_zero (gorc->last_payment.abs_time)) - { - GNUNET_break (GNUNET_YES == - TALER_amount_is_zero (&gorc->contract_amount)); - gorc->last_payment = gorc->contract_terms->timestamp; - } - { - json_t *reply; - - reply = GNUNET_JSON_PACK ( - // Deprecated in protocol v6! - GNUNET_JSON_pack_array_steal ("wire_reports", - json_array ()), - GNUNET_JSON_pack_uint64 ("exchange_code", - gorc->exchange_ec), - GNUNET_JSON_pack_uint64 ("exchange_http_status", - gorc->exchange_hc), - /* legacy: */ - GNUNET_JSON_pack_uint64 ("exchange_ec", - gorc->exchange_ec), - /* legacy: */ - GNUNET_JSON_pack_uint64 ("exchange_hc", - gorc->exchange_hc), - TALER_JSON_pack_amount ("deposit_total", - &gorc->deposits_total), - GNUNET_JSON_pack_object_incref ("contract_terms", - gorc->contract_terms_json), - GNUNET_JSON_pack_string ("order_status", - "paid"), - GNUNET_JSON_pack_timestamp ("last_payment", - gorc->last_payment), - GNUNET_JSON_pack_bool ("refunded", - gorc->refunded), - GNUNET_JSON_pack_bool ("wired", - gorc->wired), - GNUNET_JSON_pack_bool ("refund_pending", - gorc->refund_pending), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("refund_amount", - &gorc->refund_amount)), - GNUNET_JSON_pack_array_incref ("wire_details", - gorc->wire_details), - GNUNET_JSON_pack_array_incref ("refund_details", - gorc->refund_details), - GNUNET_JSON_pack_string ("order_status_url", - order_status_url), - (gorc->choice_index >= 0) - ? GNUNET_JSON_pack_int64 ("choice_index", - gorc->choice_index) - : GNUNET_JSON_pack_end_ ()); - check_reply (gorc, - reply); - json_decref (reply); - } - GNUNET_free (order_status_url); -} - - -/** - * End with error status in wire_hc and wire_ec. - * - * @param[in,out] gorc order context to update - */ -static void -phase_error (struct GetOrderRequestContext *gorc) -{ - GNUNET_assert (TALER_EC_NONE != gorc->wire_ec); - phase_end (gorc, - TALER_MHD_reply_with_error (gorc->sc.con, - gorc->wire_hc, - gorc->wire_ec, - NULL)); -} - - -MHD_RESULT -TMH_private_get_orders_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct GetOrderRequestContext *gorc = hc->ctx; - - if (NULL == gorc) - { - /* First time here, parse request and check order is known */ - GNUNET_assert (NULL != hc->infix); - gorc = GNUNET_new (struct GetOrderRequestContext); - hc->cc = &gorc_cleanup; - hc->ctx = gorc; - gorc->sc.con = connection; - gorc->hc = hc; - gorc->wire_details = json_array (); - GNUNET_assert (NULL != gorc->wire_details); - gorc->refund_details = json_array (); - GNUNET_assert (NULL != gorc->refund_details); - gorc->session_id = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "session_id"); - if (! (TALER_MHD_arg_to_yna (connection, - "allow_refunded_for_repurchase", - TALER_EXCHANGE_YNA_NO, - &gorc->allow_refunded_for_repurchase)) ) - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "allow_refunded_for_repurchase"); - TALER_MHD_parse_request_timeout (connection, - &gorc->sc.long_poll_timeout); - TALER_MHD_parse_request_arg_auto (connection, - "lp_not_etag", - &gorc->lp_not_etag, - gorc->have_lp_not_etag); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Starting GET /private/orders/%s processing with timeout %s\n", - hc->infix, - GNUNET_STRINGS_absolute_time_to_string ( - gorc->sc.long_poll_timeout)); - } - if (GNUNET_SYSERR == gorc->suspended) - return MHD_NO; /* we are in shutdown */ - while (1) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing order %s in phase %d\n", - hc->infix, - (int) gorc->phase); - switch (gorc->phase) - { - case GOP_INIT: - phase_init (gorc); - break; - case GOP_FETCH_CONTRACT: - phase_fetch_contract (gorc); - break; - case GOP_PARSE_CONTRACT: - phase_parse_contract (gorc); - break; - case GOP_CHECK_PAID: - phase_check_paid (gorc); - break; - case GOP_CHECK_REPURCHASE: - phase_check_repurchase (gorc); - break; - case GOP_UNPAID_FINISH: - phase_unpaid_finish (gorc); - break; - case GOP_CHECK_REFUNDS: - phase_check_refunds (gorc); - break; - case GOP_CHECK_DEPOSITS: - phase_check_deposits (gorc); - break; - case GOP_CHECK_LOCAL_TRANSFERS: - phase_check_local_transfers (gorc); - break; - case GOP_REPLY_RESULT: - phase_reply_result (gorc); - break; - case GOP_ERROR: - phase_error (gorc); - break; - case GOP_SUSPENDED_ON_UNPAID: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Suspending order request awaiting payment\n"); - return MHD_YES; - case GOP_END_YES: - return MHD_YES; - case GOP_END_NO: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Closing connection, no response generated\n"); - return MHD_NO; - } - } /* end first-time per-request initialization */ -} diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.h b/src/backend/taler-merchant-httpd_private-get-orders-ID.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - (C) 2017, 2020 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/> -*/ -/** - * @file taler-merchant-httpd_private-get-orders-ID.h - * @brief headers for GET /private/orders/ID handler - * @author Christian Grothoff - * @author Florian Dold - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_ID_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - -/** - * Manages a GET /private/orders/ID call, checking the status of a payment and - * refunds and, if necessary, constructing the URL for a payment redirect URL. - * - * @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_orders_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Force resuming all long polling GET orders ID requests, we are shutting - * down. - */ -void -TMH_force_gorc_resume (void); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c b/src/backend/taler-merchant-httpd_private-get-orders.c @@ -1,1532 +0,0 @@ -/* - This file is part of TALER - (C) 2019--2026 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-httpd_private-get-orders.c - * @brief implement GET /orders - * @author Christian Grothoff - * - * FIXME-cleanup: consider introducing phases / state machine - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-orders.h" -#include <taler/taler_merchant_util.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> - - -/** - * Sensible bound on TALER_MERCHANTDB_OrderFilter.delta - */ -#define MAX_DELTA 1024 - -#define CSV_HEADER \ - "Order ID,Row,Timestamp,Amount,Refund amount,Pending refund amount,Summary,Refundable,Paid\r\n" -#define CSV_FOOTER "\r\n" - -#define XML_HEADER "<?xml version=\"1.0\"?>" \ - "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \ - " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \ - " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \ - " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \ - " xmlns:o=\"urn:schemas-microsoft-com:office:office\"" \ - " xmlns:x=\"urn:schemas-microsoft-com:office:excel\"" \ - " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \ - "<Styles>" \ - "<Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd hh:mm:ss\"/></Style>" \ - "<Style ss:ID=\"Total\"><Font ss:Bold=\"1\"/></Style>" \ - "</Styles>\n" \ - "<Worksheet ss:Name=\"Orders\">\n" \ - "<Table>\n" \ - "<Row>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Order ID</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Timestamp</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Price</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Refunded</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Summary</Data></Cell>\n" \ - "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Paid</Data></Cell>\n" \ - "</Row>\n" -#define XML_FOOTER "</Table></Worksheet></Workbook>" - - -/** - * A pending GET /orders request. - */ -struct TMH_PendingOrder -{ - - /** - * Kept in a DLL. - */ - struct TMH_PendingOrder *prev; - - /** - * Kept in a DLL. - */ - struct TMH_PendingOrder *next; - - /** - * Which connection was suspended. - */ - struct MHD_Connection *con; - - /** - * Which instance is this client polling? This also defines - * which DLL this struct is part of. - */ - struct TMH_MerchantInstance *mi; - - /** - * At what time does this request expire? If set in the future, we - * may wait this long for a payment to arrive before responding. - */ - struct GNUNET_TIME_Absolute long_poll_timeout; - - /** - * Filter to apply. - */ - struct TALER_MERCHANTDB_OrderFilter of; - - /** - * The array of orders (used for JSON and PDF/Typst). - */ - json_t *pa; - - /** - * Running total of order amounts, for totals row in CSV/XML/PDF. - * Initialised to zero on first order seen. - */ - struct TALER_Amount total_amount; - - /** - * Running total of granted refund amounts. - * Initialised to zero on first paid order seen. - */ - struct TALER_Amount total_refund_amount; - - /** - * Running total of pending refund amounts. - * Initialised to zero on first paid order seen. - */ - struct TALER_Amount total_pending_refund_amount; - - /** - * True once @e total_amount has been initialised with a currency. - */ - bool total_amount_initialized; - - /** - * True once @e total_refund_amount / @e total_pending_refund_amount - * have been initialised with a currency. - */ - bool total_refund_initialized; - - /** - * The name of the instance we are querying for. - */ - const char *instance_id; - - /** - * Alias of @a of.summary_filter, but with memory to be released (owner). - */ - char *summary_filter; - - /** - * The result after adding the orders (#TALER_EC_NONE for okay, anything else for an error). - */ - enum TALER_ErrorCode result; - - /** - * Is the structure in the DLL - */ - bool in_dll; - - /** - * Output format requested by the client. - */ - enum - { - POF_JSON, - POF_CSV, - POF_XML, - POF_PDF - } format; - - /** - * Buffer used when format is #POF_CSV. - */ - struct GNUNET_Buffer csv; - - /** - * Buffer used when format is #POF_XML. - */ - struct GNUNET_Buffer xml; - - /** - * Async context used to run Typst (for #POF_PDF). - */ - struct TALER_MHD_TypstContext *tc; - - /** - * Pre-built MHD response (used when #POF_PDF Typst is done). - */ - struct MHD_Response *response; - - /** - * Task to timeout pending order. - */ - struct GNUNET_SCHEDULER_Task *order_timeout_task; - - /** - * HTTP status to return with @e response. - */ - unsigned int http_status; -}; - - -/** - * DLL head for requests suspended waiting for Typst. - */ -static struct TMH_PendingOrder *pdf_head; - -/** - * DLL tail for requests suspended waiting for Typst. - */ -static struct TMH_PendingOrder *pdf_tail; - - -void -TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi) -{ - struct TMH_PendingOrder *po; - - while (NULL != (po = mi->po_head)) - { - GNUNET_assert (po->in_dll); - GNUNET_CONTAINER_DLL_remove (mi->po_head, - mi->po_tail, - po); - MHD_resume_connection (po->con); - po->in_dll = false; - } - if (NULL != mi->po_eh) - { - TMH_db->event_listen_cancel (mi->po_eh); - mi->po_eh = NULL; - } -} - - -void -TMH_force_get_orders_resume_typst () -{ - struct TMH_PendingOrder *po; - - while (NULL != (po = pdf_head)) - { - GNUNET_CONTAINER_DLL_remove (pdf_head, - pdf_tail, - po); - MHD_resume_connection (po->con); - } -} - - -/** - * Task run to trigger timeouts on GET /orders requests with long polling. - * - * @param cls a `struct TMH_PendingOrder *` - */ -static void -order_timeout (void *cls) -{ - struct TMH_PendingOrder *po = cls; - struct TMH_MerchantInstance *mi = po->mi; - - po->order_timeout_task = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming long polled job due to timeout\n"); - GNUNET_assert (po->in_dll); - GNUNET_CONTAINER_DLL_remove (mi->po_head, - mi->po_tail, - po); - po->in_dll = false; - MHD_resume_connection (po->con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -/** - * Cleanup our "context", where we stored the data - * we are building for the response. - * - * @param ctx context to clean up, must be a `struct TMH_PendingOrder *` - */ -static void -cleanup (void *ctx) -{ - struct TMH_PendingOrder *po = ctx; - - if (po->in_dll) - { - struct TMH_MerchantInstance *mi = po->mi; - - GNUNET_CONTAINER_DLL_remove (mi->po_head, - mi->po_tail, - po); - MHD_resume_connection (po->con); - } - if (NULL != po->order_timeout_task) - { - GNUNET_SCHEDULER_cancel (po->order_timeout_task); - po->order_timeout_task = NULL; - } - json_decref (po->pa); - GNUNET_free (po->summary_filter); - switch (po->format) - { - case POF_JSON: - break; - case POF_CSV: - GNUNET_buffer_clear (&po->csv); - break; - case POF_XML: - GNUNET_buffer_clear (&po->xml); - break; - case POF_PDF: - if (NULL != po->tc) - { - TALER_MHD_typst_cancel (po->tc); - po->tc = NULL; - } - break; - } - if (NULL != po->response) - { - MHD_destroy_response (po->response); - po->response = NULL; - } - GNUNET_free (po); -} - - -/** - * Closure for #process_refunds_cb(). - */ -struct ProcessRefundsClosure -{ - /** - * Place where we accumulate the granted refunds. - */ - struct TALER_Amount total_refund_amount; - - /** - * Place where we accumulate the pending refunds. - */ - struct TALER_Amount pending_refund_amount; - - /** - * Set to an error code if something goes wrong. - */ - enum TALER_ErrorCode ec; -}; - - -/** - * Function called with information about a refund. - * It is responsible for summing up the refund amount. - * - * @param cls closure - * @param refund_serial unique serial number of the refund - * @param timestamp time of the refund (for grouping of refunds in the wallet UI) - * @param coin_pub public coin from which the refund comes from - * @param exchange_url URL of the exchange that issued @a coin_pub - * @param rtransaction_id identificator of the refund - * @param reason human-readable explanation of the refund - * @param refund_amount refund amount which is being taken from @a coin_pub - * @param pending true if the this refund was not yet processed by the wallet/exchange - */ -static void -process_refunds_cb (void *cls, - uint64_t refund_serial, - struct GNUNET_TIME_Timestamp timestamp, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const char *exchange_url, - uint64_t rtransaction_id, - const char *reason, - const struct TALER_Amount *refund_amount, - bool pending) -{ - struct ProcessRefundsClosure *prc = cls; - - if (GNUNET_OK != - TALER_amount_cmp_currency (&prc->total_refund_amount, - refund_amount)) - { - /* Database error, refunds in mixed currency in DB. Not OK! */ - prc->ec = TALER_EC_GENERIC_DB_INVARIANT_FAILURE; - GNUNET_break (0); - return; - } - GNUNET_assert (0 <= - TALER_amount_add (&prc->total_refund_amount, - &prc->total_refund_amount, - refund_amount)); - if (pending) - GNUNET_assert (0 <= - TALER_amount_add (&prc->pending_refund_amount, - &prc->pending_refund_amount, - refund_amount)); -} - - -/** - * Add one order entry to the running order-amount total in @a po. - * Sets po->result to #TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow. - * - * @param[in,out] po pending order accumulator - * @param amount the order amount to add - */ -static void -accumulate_total (struct TMH_PendingOrder *po, - const struct TALER_Amount *amount) -{ - if (! po->total_amount_initialized) - { - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (amount->currency, - &po->total_amount)); - po->total_amount_initialized = true; - } - if (0 > TALER_amount_add (&po->total_amount, - &po->total_amount, - amount)) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT; - } -} - - -/** - * Add refund amounts to the running refund totals in @a po. - * Sets po->result to #TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT on overflow. - * Only called for paid orders (where refund tracking is meaningful). - * - * @param[in,out] po pending order accumulator - * @param refund granted refund amount for this order - * @param pending pending (not-yet-processed) refund amount for this order - */ -static void -accumulate_refund_totals (struct TMH_PendingOrder *po, - const struct TALER_Amount *refund, - const struct TALER_Amount *pending) -{ - if (TALER_EC_NONE != po->result) - return; - if (! po->total_refund_initialized) - { - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (refund->currency, - &po->total_refund_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pending->currency, - &po->total_pending_refund_amount)); - po->total_refund_initialized = true; - } - if (0 > TALER_amount_add (&po->total_refund_amount, - &po->total_refund_amount, - refund)) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT; - return; - } - if (0 > TALER_amount_add (&po->total_pending_refund_amount, - &po->total_pending_refund_amount, - pending)) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT; - } -} - - -/** - * Add order details to our response accumulator. - * - * @param cls some closure - * @param orig_order_id the order this is about - * @param order_serial serial ID of the order - * @param creation_time when was the order created - */ -static void -add_order (void *cls, - const char *orig_order_id, - uint64_t order_serial, - struct GNUNET_TIME_Timestamp creation_time) -{ - struct TMH_PendingOrder *po = cls; - json_t *contract_terms = NULL; - struct TALER_PrivateContractHashP h_contract_terms; - enum GNUNET_DB_QueryStatus qs; - char *order_id = NULL; - bool refundable = false; - bool paid; - bool wired; - struct TALER_MERCHANT_Contract *contract = NULL; - int16_t choice_index = -1; - struct ProcessRefundsClosure prc = { - .ec = TALER_EC_NONE - }; - const struct TALER_Amount *amount; - char amount_buf[128]; - char refund_buf[128]; - char pending_buf[128]; - - /* Bail early if we already have an error */ - if (TALER_EC_NONE != po->result) - return; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Adding order `%s' (%llu) to result set at instance `%s'\n", - orig_order_id, - (unsigned long long) order_serial, - po->instance_id); - qs = TMH_db->lookup_order_status_by_serial (TMH_db->cls, - po->instance_id, - order_serial, - &order_id, - &h_contract_terms, - &paid); - if (qs < 0) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* Contract terms don't exist, so the order cannot be paid. */ - paid = false; - if (NULL == orig_order_id) - { - /* Got a DB trigger about a new proposal, but it - was already deleted again. Just ignore the event. */ - return; - } - order_id = GNUNET_strdup (orig_order_id); - } - - { - /* First try to find the order in the contracts */ - uint64_t os; - bool session_matches; - - qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, - po->instance_id, - order_id, - NULL, - &contract_terms, - &os, - &paid, - &wired, - &session_matches, - NULL, - &choice_index); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - GNUNET_break (os == order_serial); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* Might still be unclaimed, so try order table */ - struct TALER_MerchantPostDataHashP unused; - - paid = false; - wired = false; - qs = TMH_db->lookup_order (TMH_db->cls, - po->instance_id, - order_id, - NULL, - &unused, - &contract_terms); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Order %llu disappeared during iteration. Skipping.\n", - (unsigned long long) order_serial); - goto cleanup; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; - goto cleanup; - } - - contract = TALER_MERCHANT_contract_parse (contract_terms, - true); - if (NULL == contract) - { - GNUNET_break (0); - po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; - goto cleanup; - } - - if (paid) - { - const struct TALER_Amount *brutto; - - switch (contract->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - brutto = &contract->details.v0.brutto; - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - { - struct TALER_MERCHANT_ContractChoice *choice - = &contract->details.v1.choices[choice_index]; - - GNUNET_assert (choice_index < contract->details.v1.choices_len); - brutto = &choice->amount; - } - break; - default: - GNUNET_break (0); - goto cleanup; - } - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (brutto->currency, - &prc.total_refund_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (brutto->currency, - &prc.pending_refund_amount)); - - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - po->instance_id, - &h_contract_terms, - &process_refunds_cb, - &prc); - if (0 > qs) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; - goto cleanup; - } - if (TALER_EC_NONE != prc.ec) - { - GNUNET_break (0); - po->result = prc.ec; - goto cleanup; - } - if (0 > TALER_amount_cmp (&prc.total_refund_amount, - brutto) && - GNUNET_TIME_absolute_is_future (contract->refund_deadline.abs_time)) - refundable = true; - } - - /* compute amount totals */ - amount = NULL; - switch (contract->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - { - amount = &contract->details.v0.brutto; - - if (TALER_amount_is_zero (amount) && - (po->of.wired != TALER_EXCHANGE_YNA_ALL) ) - { - /* If we are actually filtering by wire status, - and the order was over an amount of zero, - do not return it as wire status is not - exactly meaningful for orders over zero. */ - goto cleanup; - } - - /* Accumulate order total */ - if (paid) - accumulate_total (po, - amount); - if (TALER_EC_NONE != po->result) - goto cleanup; - /* Accumulate refund totals (only meaningful for paid orders) */ - if (paid) - { - accumulate_refund_totals (po, - &prc.total_refund_amount, - &prc.pending_refund_amount); - if (TALER_EC_NONE != po->result) - goto cleanup; - } - } - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - if (-1 == choice_index) - choice_index = 0; /* default choice */ - GNUNET_assert (choice_index < contract->details.v1.choices_len); - { - struct TALER_MERCHANT_ContractChoice *choice - = &contract->details.v1.choices[choice_index]; - - amount = &choice->amount; - /* Accumulate order total */ - accumulate_total (po, - amount); - if (TALER_EC_NONE != po->result) - goto cleanup; - /* Accumulate refund totals (only meaningful for paid orders) */ - if (paid) - { - accumulate_refund_totals (po, - &prc.total_refund_amount, - &prc.pending_refund_amount); - if (TALER_EC_NONE != po->result) - goto cleanup; - } - } - default: - GNUNET_break (0); - po->result = TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION; - goto cleanup; - } - - /* convert amounts to strings (needed for some formats) */ - /* FIXME: use currency formatting rules in the future - instead of TALER_amount2s for human readability... */ - strcpy (amount_buf, - TALER_amount2s (amount)); - if (paid) - strcpy (refund_buf, - TALER_amount2s (&prc.total_refund_amount)); - if (paid) - strcpy (pending_buf, - TALER_amount2s (&prc.pending_refund_amount)); - - switch (po->format) - { - case POF_JSON: - case POF_PDF: - GNUNET_assert ( - 0 == - json_array_append_new ( - po->pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_id", - contract->order_id), - GNUNET_JSON_pack_uint64 ("row_id", - order_serial), - GNUNET_JSON_pack_timestamp ("timestamp", - creation_time), - TALER_JSON_pack_amount ("amount", - amount), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ( - "refund_amount", - paid - ? &prc.total_refund_amount - : NULL)), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ( - "pending_refund_amount", - paid - ? &prc.pending_refund_amount - : NULL)), - GNUNET_JSON_pack_string ("summary", - contract->summary), - GNUNET_JSON_pack_bool ("refundable", - refundable), - GNUNET_JSON_pack_bool ("paid", - paid)))); - break; - case POF_CSV: - { - size_t len = strlen (contract->summary); - size_t wpos = 0; - char *esummary; - - /* Escape 'summary' to double '"' as per RFC 4180, 2.7. */ - esummary = GNUNET_malloc (2 * len + 1); - for (size_t off = 0; off<len; off++) - { - if ('"' == contract->summary[off]) - esummary[wpos++] = '"'; - esummary[wpos++] = contract->summary[off]; - } - - GNUNET_buffer_write_fstr ( - &po->csv, - "%s,%llu,%llu,%s,%s,%s,\"%s\",%s,%s\r\n", - contract->order_id, - (unsigned long long) order_serial, - (unsigned long long) GNUNET_TIME_timestamp_to_s (creation_time), - amount_buf, - paid ? refund_buf : "", - paid ? pending_buf : "", - esummary, - refundable ? "yes" : "no", - paid ? "yes" : "no"); - GNUNET_free (esummary); - break; - } - case POF_XML: - { - char *esummary = TALER_escape_xml (contract->summary); - char creation_time_s[128]; - const struct tm *tm; - time_t tt; - - tt = (time_t) GNUNET_TIME_timestamp_to_s (creation_time); - tm = gmtime (&tt); - strftime (creation_time_s, - sizeof (creation_time_s), - "%Y-%m-%dT%H:%M:%S", - tm); - GNUNET_buffer_write_fstr ( - &po->xml, - "<Row>" - "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"DateTime\">%s</Data></Cell>" - "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>" - "</Row>\n", - contract->order_id, - creation_time_s, - amount_buf, - paid ? refund_buf : "", - NULL != esummary ? esummary : "", - paid ? "TRUE" : "FALSE", - paid ? "1" : "0"); - GNUNET_free (esummary); - } - break; - } /* end switch po->format */ - -cleanup: - json_decref (contract_terms); - GNUNET_free (order_id); - if (NULL != contract) - { - TALER_MERCHANT_contract_free (contract); - contract = NULL; - } -} - - -/** - * We have received a trigger from the database - * that we should (possibly) resume some requests. - * - * @param cls a `struct TMH_MerchantInstance` - * @param extra a `struct TMH_OrderChangeEventP` - * @param extra_size number of bytes in @a extra - */ -static void -resume_by_event (void *cls, - const void *extra, - size_t extra_size) -{ - struct TMH_MerchantInstance *mi = cls; - const struct TMH_OrderChangeEventDetailsP *oce = extra; - struct TMH_PendingOrder *pn; - enum TMH_OrderStateFlags osf; - uint64_t order_serial_id; - struct GNUNET_TIME_Timestamp date; - - if (sizeof (*oce) != extra_size) - { - GNUNET_break (0); - return; - } - osf = (enum TMH_OrderStateFlags) (int) ntohl (oce->order_state); - order_serial_id = GNUNET_ntohll (oce->order_serial_id); - date = GNUNET_TIME_timestamp_ntoh (oce->execution_date); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Received notification about order %llu\n", - (unsigned long long) order_serial_id); - for (struct TMH_PendingOrder *po = mi->po_head; - NULL != po; - po = pn) - { - pn = po->next; - if (! ( ( ((TALER_EXCHANGE_YNA_YES == po->of.paid) == - (0 != (osf & TMH_OSF_PAID))) || - (TALER_EXCHANGE_YNA_ALL == po->of.paid) ) && - ( ((TALER_EXCHANGE_YNA_YES == po->of.refunded) == - (0 != (osf & TMH_OSF_REFUNDED))) || - (TALER_EXCHANGE_YNA_ALL == po->of.refunded) ) && - ( ((TALER_EXCHANGE_YNA_YES == po->of.wired) == - (0 != (osf & TMH_OSF_WIRED))) || - (TALER_EXCHANGE_YNA_ALL == po->of.wired) ) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client %p waits on different order type\n", - po); - continue; - } - if (po->of.delta > 0) - { - if (order_serial_id < po->of.start_row) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client %p waits on different order row\n", - po); - continue; - } - if (GNUNET_TIME_timestamp_cmp (date, - <, - po->of.date)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client %p waits on different order date\n", - po); - continue; - } - po->of.delta--; - } - else - { - if (order_serial_id > po->of.start_row) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client %p waits on different order row\n", - po); - continue; - } - if (GNUNET_TIME_timestamp_cmp (date, - >, - po->of.date)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Client %p waits on different order date\n", - po); - continue; - } - po->of.delta++; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Waking up client %p!\n", - po); - add_order (po, - NULL, - order_serial_id, - date); - GNUNET_assert (po->in_dll); - GNUNET_CONTAINER_DLL_remove (mi->po_head, - mi->po_tail, - po); - po->in_dll = false; - MHD_resume_connection (po->con); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ - } - if (NULL == mi->po_head) - { - TMH_db->event_listen_cancel (mi->po_eh); - mi->po_eh = NULL; - } -} - - -/** - * There has been a change or addition of a new @a order_id. Wake up - * long-polling clients that may have been waiting for this event. - * - * @param mi the instance where the order changed - * @param osf order state flags - * @param date execution date of the order - * @param order_serial_id serial ID of the order in the database - */ -void -TMH_notify_order_change (struct TMH_MerchantInstance *mi, - enum TMH_OrderStateFlags osf, - struct GNUNET_TIME_Timestamp date, - uint64_t order_serial_id) -{ - struct TMH_OrderChangeEventDetailsP oce = { - .order_serial_id = GNUNET_htonll (order_serial_id), - .execution_date = GNUNET_TIME_timestamp_hton (date), - .order_state = htonl (osf) - }; - struct TMH_OrderChangeEventP eh = { - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE), - .header.size = htons (sizeof (eh)), - .merchant_pub = mi->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients of new order %llu at %s\n", - (unsigned long long) order_serial_id, - TALER_B2S (&mi->merchant_pub)); - TMH_db->event_notify (TMH_db->cls, - &eh.header, - &oce, - sizeof (oce)); -} - - -/** - * Transforms an (untrusted) input filter into a Postgresql LIKE filter. - * Escapes "%" and "_" in the @a input and adds "%" at the beginning - * and the end to turn the @a input into a suitable Postgresql argument. - * - * @param input text to turn into a substring match expression, or NULL - * @return NULL if @a input was NULL, otherwise transformed @a input - */ -static char * -tr (const char *input) -{ - char *out; - size_t slen; - size_t wpos; - - if (NULL == input) - return NULL; - slen = strlen (input); - out = GNUNET_malloc (slen * 2 + 3); - wpos = 0; - out[wpos++] = '%'; - for (size_t i = 0; i<slen; i++) - { - char c = input[i]; - - if ( (c == '%') || - (c == '_') ) - out[wpos++] = '\\'; - out[wpos++] = c; - } - out[wpos++] = '%'; - GNUNET_assert (wpos < slen * 2 + 3); - return out; -} - - -/** - * Function called with the result of a #TALER_MHD_typst() operation. - * - * @param cls closure, a `struct TMH_PendingOrder *` - * @param tr result of the operation - */ -static void -pdf_cb (void *cls, - const struct TALER_MHD_TypstResponse *tr) -{ - struct TMH_PendingOrder *po = cls; - - po->tc = NULL; - GNUNET_CONTAINER_DLL_remove (pdf_head, - pdf_tail, - po); - if (TALER_EC_NONE != tr->ec) - { - po->http_status - = TALER_ErrorCode_get_http_status (tr->ec); - po->response - = TALER_MHD_make_error (tr->ec, - tr->details.hint); - } - else - { - po->http_status = MHD_HTTP_OK; - po->response = TALER_MHD_response_from_pdf_file (tr->details.filename); - } - MHD_resume_connection (po->con); - TALER_MHD_daemon_trigger (); -} - - -/** - * Build the final response for a completed (non-long-poll) request and - * queue it on @a connection. - * - * Handles all formats (JSON, CSV, XML, PDF). For PDF this may suspend - * the connection while Typst runs asynchronously; in that case the caller - * must return #MHD_YES immediately. - * - * @param po the pending order state (already fully populated) - * @param connection the MHD connection - * @param mi the merchant instance - * @return MHD result code - */ -static MHD_RESULT -reply_orders (struct TMH_PendingOrder *po, - struct MHD_Connection *connection, - struct TMH_MerchantInstance *mi) -{ - char total_buf[128]; - char refund_buf[128]; - char pending_buf[128]; - - if (po->total_amount_initialized) - strcpy (total_buf, - TALER_amount2s (&po->total_amount)); - if (po->total_refund_initialized) - strcpy (refund_buf, - TALER_amount2s (&po->total_refund_amount)); - if (po->total_refund_initialized) - strcpy (pending_buf, - TALER_amount2s (&po->total_pending_refund_amount)); - - switch (po->format) - { - case POF_JSON: - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_incref ("orders", - po->pa)); - case POF_CSV: - { - struct MHD_Response *resp; - MHD_RESULT mret; - - GNUNET_buffer_write_fstr ( - &po->csv, - "Total (paid only),,,%s,%s,%s,,,\r\n", - po->total_amount_initialized - ? total_buf - : "-", - po->total_refund_initialized - ? refund_buf - : "-", - po->total_refund_initialized - ? pending_buf - : "-"); - GNUNET_buffer_write_str (&po->csv, - CSV_FOOTER); - resp = MHD_create_response_from_buffer (po->csv.position, - po->csv.mem, - MHD_RESPMEM_MUST_COPY); - TALER_MHD_add_global_headers (resp, - false); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_CONTENT_TYPE, - "text/csv")); - mret = MHD_queue_response (connection, - MHD_HTTP_OK, - resp); - MHD_destroy_response (resp); - return mret; - } - case POF_XML: - { - struct MHD_Response *resp; - MHD_RESULT mret; - - /* Append totals row with paid and refunded amount columns */ - GNUNET_buffer_write_fstr ( - &po->xml, - "<Row>" - "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">Total (paid only)</Data></Cell>" - "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>" - "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\">%s</Data></Cell>" - "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>" - "<Cell ss:StyleID=\"Total\"><Data ss:Type=\"String\"></Data></Cell>" - "</Row>\n", - po->total_amount_initialized - ? total_buf - : "-", - po->total_refund_initialized - ? refund_buf - : "-"); - GNUNET_buffer_write_str (&po->xml, - XML_FOOTER); - resp = MHD_create_response_from_buffer (po->xml.position, - po->xml.mem, - MHD_RESPMEM_MUST_COPY); - TALER_MHD_add_global_headers (resp, - false); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_CONTENT_TYPE, - "application/vnd.ms-excel")); - mret = MHD_queue_response (connection, - MHD_HTTP_OK, - resp); - MHD_destroy_response (resp); - return mret; - } - case POF_PDF: - { - /* Build the JSON document for Typst, passing all totals */ - json_t *root; - struct TALER_MHD_TypstDocument doc; - - root = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("business_name", - mi->settings.name), - GNUNET_JSON_pack_array_incref ("orders", - po->pa), - po->total_amount_initialized - ? TALER_JSON_pack_amount ("total_amount", - &po->total_amount) - : GNUNET_JSON_pack_string ("total_amount", - "-"), - po->total_refund_initialized - ? TALER_JSON_pack_amount ("total_refund_amount", - &po->total_refund_amount) - : GNUNET_JSON_pack_string ("total_refund_amount", - "-"), - po->total_refund_initialized - ? TALER_JSON_pack_amount ("total_pending_refund_amount", - &po->total_pending_refund_amount) - : GNUNET_JSON_pack_string ("total_pending_refund_amount", - "-")); - doc.form_name = "orders"; - doc.data = root; - - po->tc = TALER_MHD_typst (TMH_cfg, - false, /* remove on exit */ - "merchant", - 1, /* one document */ - &doc, - &pdf_cb, - po); - json_decref (root); - if (NULL == po->tc) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client requested PDF, but Typst is unavailable\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, - NULL); - } - GNUNET_CONTAINER_DLL_insert (pdf_head, - pdf_tail, - po); - MHD_suspend_connection (connection); - return MHD_YES; - } - } /* end switch */ - GNUNET_assert (0); - return MHD_NO; -} - - -/** - * Handle a GET "/orders" 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_orders (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_PendingOrder *po = hc->ctx; - struct TMH_MerchantInstance *mi = hc->instance; - enum GNUNET_DB_QueryStatus qs; - - if (NULL != po) - { - if (TALER_EC_NONE != po->result) - { - /* Resumed from long-polling with error */ - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - po->result, - NULL); - } - if (POF_PDF == po->format) - { - /* resumed from long-polling or from Typst PDF generation */ - /* We really must have a response in this case */ - if (NULL == po->response) - { - GNUNET_break (0); - return MHD_NO; - } - return MHD_queue_response (connection, - po->http_status, - po->response); - } - return reply_orders (po, - connection, - mi); - } - po = GNUNET_new (struct TMH_PendingOrder); - hc->ctx = po; - hc->cc = &cleanup; - po->con = connection; - po->pa = json_array (); - GNUNET_assert (NULL != po->pa); - po->instance_id = mi->settings.id; - po->mi = mi; - - /* Determine desired output format from Accept header */ - { - const char *mime; - - mime = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT); - if (NULL == mime) - mime = "application/json"; - if (0 == strcmp (mime, - "*/*")) - mime = "application/json"; - if (0 == strcmp (mime, - "application/json")) - { - po->format = POF_JSON; - } - else if (0 == strcmp (mime, - "text/csv")) - { - po->format = POF_CSV; - GNUNET_buffer_write_str (&po->csv, - CSV_HEADER); - } - else if (0 == strcmp (mime, - "application/vnd.ms-excel")) - { - po->format = POF_XML; - GNUNET_buffer_write_str (&po->xml, - XML_HEADER); - } - else if (0 == strcmp (mime, - "application/pdf")) - { - po->format = POF_PDF; - } - else - { - GNUNET_break_op (0); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_NOT_ACCEPTABLE, - GNUNET_JSON_pack_string ("hint", - mime)); - } - } - - if (! (TALER_MHD_arg_to_yna (connection, - "paid", - TALER_EXCHANGE_YNA_ALL, - &po->of.paid)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "paid"); - } - if (! (TALER_MHD_arg_to_yna (connection, - "refunded", - TALER_EXCHANGE_YNA_ALL, - &po->of.refunded)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "refunded"); - } - if (! (TALER_MHD_arg_to_yna (connection, - "wired", - TALER_EXCHANGE_YNA_ALL, - &po->of.wired)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "wired"); - } - po->of.delta = -20; - /* deprecated in protocol v12 */ - TALER_MHD_parse_request_snumber (connection, - "delta", - &po->of.delta); - /* since protocol v12 */ - TALER_MHD_parse_request_snumber (connection, - "limit", - &po->of.delta); - if ( (-MAX_DELTA > po->of.delta) || - (po->of.delta > MAX_DELTA) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "limit"); - } - { - const char *date_s_str; - - date_s_str = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "date_s"); - if (NULL == date_s_str) - { - if (po->of.delta > 0) - po->of.date = GNUNET_TIME_UNIT_ZERO_TS; - else - po->of.date = GNUNET_TIME_UNIT_FOREVER_TS; - } - else - { - char dummy; - unsigned long long ll; - - if (1 != - sscanf (date_s_str, - "%llu%c", - &ll, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "date_s"); - } - - po->of.date = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_from_s (ll)); - if (GNUNET_TIME_absolute_is_never (po->of.date.abs_time)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "date_s"); - } - } - } - if (po->of.delta > 0) - { - struct GNUNET_TIME_Relative duration - = GNUNET_TIME_UNIT_FOREVER_REL; - struct GNUNET_TIME_Absolute cut_off; - - TALER_MHD_parse_request_rel_time (connection, - "max_age", - &duration); - cut_off = GNUNET_TIME_absolute_subtract (GNUNET_TIME_absolute_get (), - duration); - po->of.date = GNUNET_TIME_timestamp_max ( - po->of.date, - GNUNET_TIME_absolute_to_timestamp (cut_off)); - } - if (po->of.delta > 0) - po->of.start_row = 0; - else - po->of.start_row = INT64_MAX; - /* deprecated in protocol v12 */ - TALER_MHD_parse_request_number (connection, - "start", - &po->of.start_row); - /* since protocol v12 */ - TALER_MHD_parse_request_number (connection, - "offset", - &po->of.start_row); - if (INT64_MAX < po->of.start_row) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "offset"); - } - po->summary_filter = tr (MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "summary_filter")); - po->of.summary_filter = po->summary_filter; /* just an (read-only) alias! */ - po->of.session_id - = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "session_id"); - po->of.fulfillment_url - = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "fulfillment_url"); - TALER_MHD_parse_request_timeout (connection, - &po->long_poll_timeout); - if (GNUNET_TIME_absolute_is_never (po->long_poll_timeout)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "timeout_ms"); - } - if ( (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) && - (NULL == mi->po_eh) ) - { - struct TMH_OrderChangeEventP change_eh = { - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDERS_CHANGE), - .header.size = htons (sizeof (change_eh)), - .merchant_pub = mi->merchant_pub - }; - - mi->po_eh = TMH_db->event_listen (TMH_db->cls, - &change_eh.header, - GNUNET_TIME_UNIT_FOREVER_REL, - &resume_by_event, - mi); - } - - po->of.timeout = GNUNET_TIME_absolute_get_remaining (po->long_poll_timeout); - - qs = TMH_db->lookup_orders (TMH_db->cls, - po->instance_id, - &po->of, - &add_order, - po); - if (0 > qs) - { - GNUNET_break (0); - po->result = TALER_EC_GENERIC_DB_FETCH_FAILED; - } - if (TALER_EC_NONE != po->result) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - po->result, - NULL); - } - if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) && - (GNUNET_TIME_absolute_is_future (po->long_poll_timeout)) ) - { - GNUNET_assert (NULL == po->order_timeout_task); - po->order_timeout_task - = GNUNET_SCHEDULER_add_at (po->long_poll_timeout, - &order_timeout, - po); - GNUNET_CONTAINER_DLL_insert (mi->po_head, - mi->po_tail, - po); - po->in_dll = true; - MHD_suspend_connection (connection); - return MHD_YES; - } - return reply_orders (po, - connection, - mi); -} - - -/* end of taler-merchant-httpd_private-get-orders.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-orders.h b/src/backend/taler-merchant-httpd_private-get-orders.h @@ -1,76 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-get-orders.h - * @brief implement GET /orders - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_ORDERS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/orders" 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_orders (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * There has been a change or addition of a new @a order_id. Wake up - * long-polling clients that may have been waiting for this event. - * - * @param mi the instance where the order changed - * @param osf order state flags - * @param date execution date of the order - * @param order_serial_id serial ID of the order in the database - */ -void -TMH_notify_order_change (struct TMH_MerchantInstance *mi, - enum TMH_OrderStateFlags osf, - struct GNUNET_TIME_Timestamp date, - uint64_t order_serial_id); - - -/** - * We are shutting down (or an instance is being deleted), force resume of all - * GET /orders requests. - * - * @param mi instance to force resuming for - */ -void -TMH_force_get_orders_resume (struct TMH_MerchantInstance *mi); - - -/** - * We are shutting down (or an instance is being deleted), force resume of all - * GET /orders requests. - */ -void -TMH_force_get_orders_resume_typst (void); - - -/* end of taler-merchant-httpd_private-get-orders.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-otp-devices-ID.c b/src/backend/taler-merchant-httpd_private-get-otp-devices-ID.c @@ -1,110 +0,0 @@ -/* - This file is part of TALER - (C) 2022-2024 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-httpd_private-get-otp-devices-ID.c - * @brief implement GET /otp-devices/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-otp-devices-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a GET "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_OtpDeviceDetails tp = { 0 }; - enum GNUNET_DB_QueryStatus qs; - uint64_t faketime_s - = GNUNET_TIME_timestamp_to_s (GNUNET_TIME_timestamp_get ()); - struct GNUNET_TIME_Timestamp my_time; - struct TALER_Amount price; - - TALER_MHD_parse_request_number (connection, - "faketime", - &faketime_s); - memset (&price, - 0, - sizeof (price)); - TALER_MHD_parse_request_amount (connection, - "price", - &price); - my_time = GNUNET_TIME_timestamp_from_s (faketime_s); - GNUNET_assert (NULL != mi); - qs = TMH_db->select_otp (TMH_db->cls, - mi->settings.id, - hc->infix, - &tp); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_otp"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, - hc->infix); - } - { - MHD_RESULT ret; - char *pos_confirmation; - - pos_confirmation = (NULL == tp.otp_key) - ? NULL - : TALER_build_pos_confirmation (tp.otp_key, - tp.otp_algorithm, - &price, - my_time); - /* Note: we deliberately (by design) do not return the otp_key */ - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("device_description", - tp.otp_description), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_code", - pos_confirmation)), - GNUNET_JSON_pack_uint64 ("otp_timestamp", - faketime_s), - GNUNET_JSON_pack_uint64 ("otp_algorithm", - tp.otp_algorithm), - GNUNET_JSON_pack_uint64 ("otp_ctr", - tp.otp_ctr)); - GNUNET_free (pos_confirmation); - GNUNET_free (tp.otp_description); - GNUNET_free (tp.otp_key); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-get-otp-devices-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-otp-devices-ID.h b/src/backend/taler-merchant-httpd_private-get-otp-devices-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-otp-devices-ID.h - * @brief implement GET /otp-devices/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/otp-devices/$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_otp_devices_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-otp-devices-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-otp-devices.c b/src/backend/taler-merchant-httpd_private-get-otp-devices.c @@ -1,80 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-otp-devices.c - * @brief implement GET /otp-devices - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-otp-devices.h" - - -/** - * Add OTP device details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param otp_id ID of the OTP device - * @param otp_description human-readable description for the OTP device - */ -static void -add_otp (void *cls, - const char *otp_id, - const char *otp_description) -{ - json_t *pa = cls; - - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("otp_device_id", - otp_id), - GNUNET_JSON_pack_string ("device_description", - otp_description)))); -} - - -MHD_RESULT -TMH_private_get_otp_devices (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *pa; - enum GNUNET_DB_QueryStatus qs; - - pa = json_array (); - GNUNET_assert (NULL != pa); - qs = TMH_db->lookup_otp_devices (TMH_db->cls, - hc->instance->settings.id, - &add_otp, - pa); - if (0 > qs) - { - GNUNET_break (0); - json_decref (pa); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("otp_devices", - pa)); -} - - -/* end of taler-merchant-httpd_private-get-otp-devices.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-otp-devices.h b/src/backend/taler-merchant-httpd_private-get-otp-devices.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-otp-devices.h - * @brief implement GET /otp-devices - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_OTP_DEVICES_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/otp-devices" 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_otp_devices (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-otp-devices.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c @@ -1,234 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020, 2021, 2024 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-httpd_private-get-pos.c - * @brief implement GET /private/pos - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-pos.h" -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_helper.h" - -/** - * Closure for add_product(). - */ -struct Context -{ - /** - * JSON array of products we are building. - */ - json_t *pa; - - /** - * JSON array of categories we are building. - */ - json_t *ca; - -}; - - -/** - * Add category to the @e ca array. - * - * @param cls a `struct Context` with JSON arrays to build - * @param category_id ID of the category - * @param category_name name of the category - * @param category_name_i18n translations of the @a category_name - * @param product_count number of products in the category - */ -static void -add_category ( - void *cls, - uint64_t category_id, - const char *category_name, - const json_t *category_name_i18n, - uint64_t product_count) -{ - struct Context *ctx = cls; - - (void) product_count; - GNUNET_assert ( - 0 == - json_array_append_new ( - ctx->ca, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("id", - category_id), - GNUNET_JSON_pack_object_incref ("name_i18n", - (json_t *) category_name_i18n), - GNUNET_JSON_pack_string ("name", - category_name)))); -} - - -/** - * Add product details to our JSON array. - * - * @param cls a `struct Context` with JSON arrays to build - * @param product_serial row ID of the product - * @param product_id ID of the product - * @param pd full product details - * @param num_categories length of @a categories array - * @param categories array of categories the - * product is in - */ -static void -add_product (void *cls, - uint64_t product_serial, - const char *product_id, - const struct TALER_MERCHANTDB_ProductDetails *pd, - size_t num_categories, - const uint64_t *categories) -{ - struct Context *ctx = cls; - json_t *pa = ctx->pa; - json_t *cata; - int64_t total_stock_api; - char unit_total_stock_buf[64]; - - cata = json_array (); - GNUNET_assert (NULL != cata); - for (size_t i = 0; i<num_categories; i++) - GNUNET_assert ( - 0 == json_array_append_new ( - cata, - json_integer (categories[i]))); - if (0 == num_categories) - { - // If there is no category, we return the default category - GNUNET_assert ( - 0 == json_array_append_new ( - cata, - json_integer (0))); - } - if (INT64_MAX == pd->total_stock) - total_stock_api = -1; - else - total_stock_api = (int64_t) pd->total_stock; - TALER_MERCHANT_vk_format_fractional_string ( - TALER_MERCHANT_VK_STOCK, - pd->total_stock, - pd->total_stock_frac, - sizeof (unit_total_stock_buf), - unit_total_stock_buf); - - GNUNET_assert ( - 0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("product_name", - pd->product_name), - GNUNET_JSON_pack_string ("description", - pd->description), - GNUNET_JSON_pack_object_incref ("description_i18n", - (json_t *) pd->description_i18n), - GNUNET_JSON_pack_string ("unit", - pd->unit), - // Note: deprecated field - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("price", - (0 == pd->price_array_length) - ? NULL - : &pd->price_array[0])), - TALER_JSON_pack_amount_array ("unit_price", - pd->price_array_length, - pd->price_array), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("image", - pd->image)), - GNUNET_JSON_pack_array_steal ("categories", - cata), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ("taxes", - (json_t *) pd->taxes)), - GNUNET_JSON_pack_int64 ("total_stock", - total_stock_api), - GNUNET_JSON_pack_string ("unit_total_stock", - unit_total_stock_buf), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - pd->allow_fractional_quantity), - GNUNET_JSON_pack_uint64 ("unit_precision_level", - pd->fractional_precision_level), - GNUNET_JSON_pack_uint64 ("minimum_age", - pd->minimum_age), - GNUNET_JSON_pack_uint64 ("product_serial", - product_serial), - GNUNET_JSON_pack_string ("product_id", - product_id)))); -} - - -MHD_RESULT -TMH_private_get_pos (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct Context ctx; - enum GNUNET_DB_QueryStatus qs; - - ctx.pa = json_array (); - GNUNET_assert (NULL != ctx.pa); - ctx.ca = json_array (); - GNUNET_assert (NULL != ctx.ca); - GNUNET_assert ( - 0 == json_array_append_new ( - ctx.ca, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("id", - 0), - GNUNET_JSON_pack_string ("name", - "default")))); - qs = TMH_db->lookup_categories (TMH_db->cls, - hc->instance->settings.id, - &add_category, - &ctx); - if (0 > qs) - { - GNUNET_break (0); - json_decref (ctx.pa); - json_decref (ctx.ca); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - qs = TMH_db->lookup_all_products (TMH_db->cls, - hc->instance->settings.id, - &add_product, - &ctx); - if (0 > qs) - { - GNUNET_break (0); - json_decref (ctx.pa); - json_decref (ctx.ca); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("categories", - ctx.ca), - GNUNET_JSON_pack_array_steal ("products", - ctx.pa)); -} - - -/* end of taler-merchant-httpd_private-get-pos.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-pos.h b/src/backend/taler-merchant-httpd_private-get-pos.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-get-pos.h - * @brief implement GET /pos - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_POS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_POS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/pos" 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_pos (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-pos.h */ -#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 @@ -1,100 +0,0 @@ -/* - 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 "taler/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; - size_t pot_total_len; - struct TALER_Amount *pot_totals; - 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_len, - &pot_totals); - - 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), - (0 == pot_total_len) - ? GNUNET_JSON_pack_array_steal ("pot_totals", - json_array ()) - : TALER_JSON_pack_amount_array ("pot_totals", - pot_total_len, - pot_totals)); - GNUNET_free (pot_totals); - 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 @@ -1,41 +0,0 @@ -/* - 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 @@ -1,128 +0,0 @@ -/* - 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 "taler/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_len length of the @a pot_totals array - * @param pot_totals current total amounts in the pot - */ -static void -add_pot (void *cls, - uint64_t money_pot_id, - const char *name, - size_t pot_total_len, - const struct TALER_Amount *pot_totals) -{ - 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), - (0 == pot_total_len) - ? GNUNET_JSON_pack_array_steal ("pot_totals", - json_array ()) - : TALER_JSON_pack_amount_array ("pot_totals", - pot_total_len, - pot_totals)); - 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 @@ -1,41 +0,0 @@ -/* - 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-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c @@ -1,160 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020, 2021 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-httpd_private-get-products-ID.c - * @brief implement GET /products/$ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-products-ID.h" -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_util.h> - -/** - * Handle a GET "/products/$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_products_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; - enum GNUNET_DB_QueryStatus qs; - size_t num_categories = 0; - uint64_t *categories = NULL; - json_t *jcategories; - - GNUNET_assert (NULL != mi); - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - hc->infix, - &pd, - &num_categories, - &categories); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product"); - } - if (0 == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - hc->infix); - } - jcategories = json_array (); - GNUNET_assert (NULL != jcategories); - for (size_t i = 0; i<num_categories; i++) - { - GNUNET_assert (0 == - json_array_append_new (jcategories, - json_integer (categories[i]))); - } - GNUNET_free (categories); - - { - MHD_RESULT ret; - int64_t total_stock_api; - char unit_total_stock_buf[64]; - - if (INT64_MAX == pd.total_stock) - total_stock_api = -1; - else - total_stock_api = (int64_t) pd.total_stock; - - TALER_MERCHANT_vk_format_fractional_string ( - TALER_MERCHANT_VK_STOCK, - pd.total_stock, - pd.total_stock_frac, - sizeof (unit_total_stock_buf), - unit_total_stock_buf); - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("product_name", - pd.product_name), - GNUNET_JSON_pack_string ("description", - pd.description), - GNUNET_JSON_pack_object_incref ("description_i18n", - pd.description_i18n), - GNUNET_JSON_pack_string ("unit", - pd.unit), - GNUNET_JSON_pack_array_steal ("categories", - jcategories), - // Note: deprecated field - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("price", - (0 == pd.price_array_length) - ? NULL - : &pd.price_array[0])), - TALER_JSON_pack_amount_array ("unit_price", - pd.price_array_length, - pd.price_array), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("image", - pd.image)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ("taxes", - pd.taxes)), - GNUNET_JSON_pack_int64 ("total_stock", - total_stock_api), - GNUNET_JSON_pack_string ("unit_total_stock", - unit_total_stock_buf), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - pd.allow_fractional_quantity), - GNUNET_JSON_pack_uint64 ("unit_precision_level", - pd.fractional_precision_level), - TALER_JSON_pack_amount_array ("unit_price", - pd.price_array_length, - pd.price_array), - GNUNET_JSON_pack_uint64 ("total_sold", - pd.total_sold), - GNUNET_JSON_pack_uint64 ("total_lost", - pd.total_lost), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("address", - pd.address)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("next_restock", - (pd.next_restock))), - GNUNET_JSON_pack_uint64 ("product_group_id", - pd.product_group_id), - GNUNET_JSON_pack_uint64 ("money_pot_id", - pd.money_pot_id), - GNUNET_JSON_pack_bool ("price_is_net", - pd.price_is_net), - GNUNET_JSON_pack_uint64 ("minimum_age", - pd.minimum_age)); - TALER_MERCHANTDB_product_details_free (&pd); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-get-products-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.h b/src/backend/taler-merchant-httpd_private-get-products-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-get-products-ID.h - * @brief implement GET /products/$ID/ - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/products/$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_products_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-products-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-products.c b/src/backend/taler-merchant-httpd_private-get-products.c @@ -1,149 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020, 2021, 2024, 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-httpd_private-get-products.c - * @brief implement GET /products - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-products.h" - - -/** - * Add product details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param product_serial serial (row) number of the product in the database - * @param product_id ID of the product - */ -static void -add_product (void *cls, - uint64_t product_serial, - const char *product_id) -{ - json_t *pa = cls; - - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("product_serial", - product_serial), - GNUNET_JSON_pack_string ("product_id", - product_id)))); -} - - -/** - * Transforms an (untrusted) input filter into a Postgresql LIKE filter. - * Escapes "%" and "_" in the @a input and adds "%" at the beginning - * and the end to turn the @a input into a suitable Postgresql argument. - * - * @param input text to turn into a substring match expression, or NULL - * @return NULL if @a input was NULL, otherwise transformed @a input - */ -static char * -tr (const char *input) -{ - char *out; - size_t slen; - size_t wpos; - - if (NULL == input) - return NULL; - slen = strlen (input); - out = GNUNET_malloc (slen * 2 + 3); - wpos = 0; - out[wpos++] = '%'; - for (size_t i = 0; i<slen; i++) - { - char c = input[i]; - - if ( (c == '%') || - (c == '_') ) - out[wpos++] = '\\'; - out[wpos++] = c; - } - out[wpos++] = '%'; - GNUNET_assert (wpos < slen * 2 + 3); - return out; -} - - -MHD_RESULT -TMH_private_get_products (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *pa; - enum GNUNET_DB_QueryStatus qs; - char *category_filter; - char *name_filter; - char *description_filter; - int64_t limit; - uint64_t offset; - - limit = 20; /* default */ - TALER_MHD_parse_request_snumber (connection, - "limit", - &limit); - if (limit < 0) - offset = INT64_MAX; - else - offset = 0; - TALER_MHD_parse_request_number (connection, - "offset", - &offset); - category_filter = tr (MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "category_filter")); - name_filter = tr (MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "name_filter")); - description_filter = tr (MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "description_filter")); - pa = json_array (); - GNUNET_assert (NULL != pa); - qs = TMH_db->lookup_products (TMH_db->cls, - hc->instance->settings.id, - offset, - limit, - category_filter, - name_filter, - description_filter, - &add_product, - pa); - GNUNET_free (category_filter); - GNUNET_free (name_filter); - GNUNET_free (description_filter); - if (0 > qs) - { - GNUNET_break (0); - json_decref (pa); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("products", - pa)); -} - - -/* end of taler-merchant-httpd_private-get-products.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-products.h b/src/backend/taler-merchant-httpd_private-get-products.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2019, 2020 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-httpd_private-get-products.h - * @brief implement GET /products - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_PRODUCTS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/products" 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_products (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-products.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-report-ID.c b/src/backend/taler-merchant-httpd_private-get-report-ID.c @@ -1,135 +0,0 @@ -/* - 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-report-ID.c - * @brief implementation of GET /private/reports/$REPORT_ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-report-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_get_report (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *report_id_str = hc->infix; - unsigned long long report_id; - char *report_program_section; - char *report_description; - char *mime_type; - char *data_source; - char *target_address; - struct GNUNET_TIME_Relative frequency; - struct GNUNET_TIME_Relative frequency_shift; - struct GNUNET_TIME_Absolute next_transmission; - enum TALER_ErrorCode last_error_code; - char *last_error_detail; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - - { - char dummy; - - if (1 != sscanf (report_id_str, - "%llu%c", - &report_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "report_id"); - } - } - - qs = TMH_db->select_report (TMH_db->cls, - hc->instance->settings.id, - (uint64_t) report_id, - &report_program_section, - &report_description, - &mime_type, - &data_source, - &target_address, - &frequency, - &frequency_shift, - &next_transmission, - &last_error_code, - &last_error_detail); - - 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_report"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, - report_id_str); - } - - { - json_t *response; - - response = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("report_serial", - report_id), - GNUNET_JSON_pack_string ("description", - report_description), - GNUNET_JSON_pack_string ("program_section", - report_program_section), - GNUNET_JSON_pack_string ("mime_type", - mime_type), - GNUNET_JSON_pack_string ("data_source", - data_source), - GNUNET_JSON_pack_string ("target_address", - target_address), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("last_error_detail", - last_error_detail)), - GNUNET_JSON_pack_time_rel ("report_frequency", - frequency), - GNUNET_JSON_pack_time_rel ("report_frequency_shift", - frequency_shift)); - GNUNET_free (report_program_section); - GNUNET_free (report_description); - GNUNET_free (mime_type); - GNUNET_free (data_source); - GNUNET_free (target_address); - GNUNET_free (last_error_detail); - if (TALER_EC_NONE != last_error_code) - { - GNUNET_assert (0 == - json_object_set_new (response, - "last_error_code", - json_integer (last_error_code))); - } - return TALER_MHD_reply_json_steal (connection, - response, - MHD_HTTP_OK); - } -} diff --git a/src/backend/taler-merchant-httpd_private-get-report-ID.h b/src/backend/taler-merchant-httpd_private-get-report-ID.h @@ -1,40 +0,0 @@ -/* - 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-report-ID.h - * @brief HTTP serving layer for getting report details - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORT_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORT_ID_H -#include "taler-merchant-httpd.h" - -/** - * Handle GET /private/reports/$REPORT_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_report (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-reports.c b/src/backend/taler-merchant-httpd_private-get-reports.c @@ -1,119 +0,0 @@ -/* - 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-reports.c - * @brief implementation of GET /private/reports - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-reports.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> - - -/** - * Sensible bound on the limit. - */ -#define MAX_DELTA 1024 - - -/** - * Callback for listing reports. - * - * @param cls closure with a `json_t *` - * @param report_id unique identifier of the report - * @param report_description human-readable description - * @param frequency how often the report is generated - */ -static void -add_report (void *cls, - uint64_t report_id, - const char *report_description, - struct GNUNET_TIME_Relative frequency) -{ - json_t *reports = cls; - json_t *entry; - - entry = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("report_serial", - report_id), - GNUNET_JSON_pack_string ("description", - report_description), - GNUNET_JSON_pack_time_rel ("report_frequency", - frequency)); - GNUNET_assert (NULL != entry); - GNUNET_assert (0 == - json_array_append_new (reports, - entry)); -} - - -MHD_RESULT -TMH_private_get_reports (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - int64_t limit = -20; - uint64_t offset; - enum GNUNET_DB_QueryStatus qs; - json_t *reports; - - (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"); - } - - reports = json_array (); - GNUNET_assert (NULL != reports); - qs = TMH_db->select_reports (TMH_db->cls, - hc->instance->settings.id, - limit, - offset, - &add_report, - reports); - if (qs < 0) - { - json_decref (reports); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_reports"); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("reports", - reports)); -} diff --git a/src/backend/taler-merchant-httpd_private-get-reports.h b/src/backend/taler-merchant-httpd_private-get-reports.h @@ -1,41 +0,0 @@ -/* - 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-reports.h - * @brief HTTP serving layer for listing reports - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORTS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_REPORTS_H - -#include "taler-merchant-httpd.h" - -/** - * Handle GET /private/reports 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_reports (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.c b/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.c @@ -1,254 +0,0 @@ -/* - 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-httpd_private-get-statistics-amount-SLUG.c - * @brief implement GET /statistics-amount/$SLUG/ - * @author Martin Schanzenbach - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-statistics-amount-SLUG.h" -#include <gnunet/gnunet_json_lib.h> -#include <taler/taler_json_lib.h> - - -/** - * Typically called by `lookup_statistics_amount_by_bucket`. - * - * @param cls a `json_t *` JSON array to build - * @param description description of the statistic - * @param bucket_start start time of the bucket - * @param bucket_end end time of the bucket - * @param bucket_range range of the bucket - * @param amounts_len the length of @a cumulative_amounts - * @param amounts the cumulative amounts array - */ -static void -amount_by_bucket (void *cls, - const char *description, - struct GNUNET_TIME_Timestamp bucket_start, - struct GNUNET_TIME_Timestamp bucket_end, - const char *bucket_range, - unsigned int amounts_len, - const struct TALER_Amount amounts[static amounts_len]) -{ - json_t *root = cls; - json_t *amount_array; - json_t *buckets_array; - - GNUNET_assert (json_is_object (root)); - buckets_array = json_object_get (root, - "buckets"); - GNUNET_assert (NULL != buckets_array); - GNUNET_assert (json_is_array (buckets_array)); - - amount_array = json_array (); - GNUNET_assert (NULL != amount_array); - for (unsigned int i = 0; i < amounts_len; i++) - { - GNUNET_assert ( - 0 == - json_array_append_new (amount_array, - TALER_JSON_from_amount (&amounts[i]))); - } - - GNUNET_assert ( - 0 == - json_array_append_new ( - buckets_array, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ( - "start_time", - bucket_start), - GNUNET_JSON_pack_timestamp ( - "end_time", - bucket_end), - GNUNET_JSON_pack_string ( - "range", - bucket_range), - GNUNET_JSON_pack_array_steal ( - "cumulative_amounts", - amount_array)))); - if (NULL == json_object_get (root, - "buckets_description")) - { - GNUNET_assert (0 == - json_object_set_new (root, - "buckets_description", - json_string (description))); - } -} - - -/** - * Typically called by `lookup_statistics_amount_by_interval`. - * - * @param cls a `json_t *` JSON array to build - * @param description description of the statistic - * @param bucket_start start time of the bucket - * @param amounts_len the length of @a cumulative_amounts - * @param amounts the cumulative amounts array - */ -static void -amount_by_interval (void *cls, - const char *description, - struct GNUNET_TIME_Timestamp bucket_start, - unsigned int amounts_len, - const struct TALER_Amount amounts[static amounts_len]) -{ - json_t *root; - json_t *amount_array; - json_t *intervals_array; - - root = cls; - GNUNET_assert (json_is_object (root)); - intervals_array = json_object_get (root, - "intervals"); - GNUNET_assert (NULL != intervals_array); - GNUNET_assert (json_is_array (intervals_array)); - - amount_array = json_array (); - GNUNET_assert (NULL != amount_array); - for (unsigned int i = 0; i < amounts_len; i++) - { - GNUNET_assert ( - 0 == - json_array_append_new (amount_array, - TALER_JSON_from_amount (&amounts[i]))); - } - - - GNUNET_assert ( - 0 == - json_array_append_new ( - intervals_array, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ( - "start_time", - bucket_start), - GNUNET_JSON_pack_array_steal ( - "cumulative_amounts", - amount_array)))); - if (NULL == json_object_get (root, - "intervals_description")) - { - GNUNET_assert ( - 0 == - json_object_set_new (root, - "intervals_description", - json_string (description))); - } -} - - -/** - * Handle a GET "/statistics-amount/$SLUG" 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_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - json_t *root; - bool get_buckets = true; - bool get_intervals = true; - - GNUNET_assert (NULL != mi); - { - const char *filter; - - filter = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "by"); - if (NULL != filter) - { - if (0 == strcasecmp (filter, - "bucket")) - get_intervals = false; - else if (0 == strcasecmp (filter, - "interval")) - get_buckets = false; - else if (0 != strcasecmp (filter, - "any")) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "by"); - } - } - } - root = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("intervals", - json_array ()), - GNUNET_JSON_pack_array_steal ("buckets", - json_array ())); - if (get_buckets) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_statistics_amount_by_bucket ( - TMH_db->cls, - mi->settings.id, - hc->infix, - &amount_by_bucket, - root); - if (0 > qs) - { - GNUNET_break (0); - json_decref (root); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_amount_by_bucket"); - } - } - if (get_intervals) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_statistics_amount_by_interval ( - TMH_db->cls, - mi->settings.id, - hc->infix, - &amount_by_interval, - root); - if (0 > qs) - { - GNUNET_break (0); - json_decref (root); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_amount_by_interval"); - } - } - return TALER_MHD_reply_json (connection, - root, - MHD_HTTP_OK); -} - - -/* end of taler-merchant-httpd_private-get-statistics-amount-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.h b/src/backend/taler-merchant-httpd_private-get-statistics-amount-SLUG.h @@ -1,41 +0,0 @@ -/* - 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-httpd_private-get-statistics-counter-SLUG.h - * @brief implement GET /statistics-amount/$SLUG/ - * @author Martin Schanzenbach - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_AMOUNT_SLUG_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_AMOUNT_SLUG_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/statistics-amount/$SLUG" 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_statistics_amount_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-statistics-amount-SLUG.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.c b/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.c @@ -1,227 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 2024, 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-httpd_private-get-statistics-counter-SLUG.c - * @brief implement GET /statistics-counter/$SLUG/ - * @author Martin Schanzenbach - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-statistics-counter-SLUG.h" -#include <gnunet/gnunet_json_lib.h> -#include <taler/taler_json_lib.h> - - -/** - * Function returning integer-valued statistics. - * Typically called by `lookup_statistics_counter_by_bucket`. - * - * @param cls a `json_t *` JSON array to build - * @param description description of the statistic - * @param bucket_start start time of the bucket - * @param bucket_end end time of the bucket - * @param bucket_range range of the bucket - * @param cumulative_number counter value - */ -static void -counter_by_bucket (void *cls, - const char *description, - struct GNUNET_TIME_Timestamp bucket_start, - struct GNUNET_TIME_Timestamp bucket_end, - const char *bucket_range, - uint64_t cumulative_number) -{ - json_t *root = cls; - json_t *buckets_array; - - GNUNET_assert (json_is_object (root)); - buckets_array = json_object_get (root, - "buckets"); - GNUNET_assert (NULL != buckets_array); - GNUNET_assert (json_is_array (buckets_array)); - GNUNET_assert ( - 0 == - json_array_append_new ( - buckets_array, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ( - "start_time", - bucket_start), - GNUNET_JSON_pack_timestamp ( - "end_time", - bucket_end), - GNUNET_JSON_pack_string ( - "range", - bucket_range), - GNUNET_JSON_pack_uint64 ( - "cumulative_counter", - cumulative_number)))); - if (NULL == json_object_get (root, - "buckets_description")) - { - GNUNET_assert ( - 0 == - json_object_set_new (root, - "buckets_description", - json_string (description))); - } -} - - -/** - * Function returning integer-valued statistics for a time interval. - * Called by `lookup_statistics_counter_by_interval`. - * - * @param cls a `json_t *` JSON array to build - * @param description description of the statistic - * @param bucket_start start time of the interval - * @param cumulative_number counter value - */ -static void -counter_by_interval (void *cls, - const char *description, - struct GNUNET_TIME_Timestamp bucket_start, - uint64_t cumulative_number) -{ - json_t *root = cls; - json_t *intervals_array; - - GNUNET_assert (json_is_object (root)); - intervals_array = json_object_get (root, - "intervals"); - GNUNET_assert (NULL != intervals_array); - GNUNET_assert (json_is_array (intervals_array)); - GNUNET_assert ( - 0 == - json_array_append_new ( - intervals_array, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ( - "start_time", - bucket_start), - GNUNET_JSON_pack_uint64 ( - "cumulative_counter", - cumulative_number)))); - if (NULL == json_object_get (root, - "intervals_description")) - { - GNUNET_assert ( - 0 == - json_object_set_new (root, - "intervals_description", - json_string (description))); - } -} - - -/** - * Handle a GET "/statistics-counter/$SLUG" 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_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - json_t *root; - bool get_buckets = true; - bool get_intervals = true; - - GNUNET_assert (NULL != mi); - { - const char *filter; - - filter = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "by"); - if (NULL != filter) - { - if (0 == strcasecmp (filter, - "bucket")) - get_intervals = false; - else if (0 == strcasecmp (filter, - "interval")) - get_buckets = false; - else if (0 != strcasecmp (filter, - "any")) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "by"); - } - } - } - root = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("intervals", - json_array ()), - GNUNET_JSON_pack_array_steal ("buckets", - json_array ())); - if (get_buckets) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_statistics_counter_by_bucket ( - TMH_db->cls, - mi->settings.id, - hc->infix, - &counter_by_bucket, - root); - if (0 > qs) - { - GNUNET_break (0); - json_decref (root); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_counter_by_bucket"); - } - } - if (get_intervals) - { - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_statistics_counter_by_interval ( - TMH_db->cls, - mi->settings.id, - hc->infix, - &counter_by_interval, - root); - if (0 > qs) - { - GNUNET_break (0); - json_decref (root); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_counter_by_interval"); - } - } - return TALER_MHD_reply_json (connection, - root, - MHD_HTTP_OK); -} - - -/* end of taler-merchant-httpd_private-get-statistics-counter-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.h b/src/backend/taler-merchant-httpd_private-get-statistics-counter-SLUG.h @@ -1,41 +0,0 @@ -/* - 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-httpd_private-get-statistics-counter-SLUG.h - * @brief implement GET /statistics-counter/$SLUG/ - * @author Martin Schanzenbach - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_COUNTER_SLUG_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_COUNTER_SLUG_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/statistics-counter/$SLUG" 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_statistics_counter_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-statistics-counter-SLUG.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c b/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.c @@ -1,762 +0,0 @@ -/* - 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-httpd_private-get-statistics-report-transactions.c - * @brief implement GET /statistics-report/transactions - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-statistics-report-transactions.h" -#include <gnunet/gnunet_json_lib.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_mhd_lib.h> - - -/** - * Closure for the detail_cb(). - */ -struct ResponseContext -{ - /** - * Format of the response we are to generate. - */ - enum - { - RCF_JSON, - RCF_PDF - } format; - - /** - * Stored in a DLL while suspended. - */ - struct ResponseContext *next; - - /** - * Stored in a DLL while suspended. - */ - struct ResponseContext *prev; - - /** - * Context for this request. - */ - struct TMH_HandlerContext *hc; - - /** - * Async context used to run Typst. - */ - struct TALER_MHD_TypstContext *tc; - - /** - * Response to return. - */ - struct MHD_Response *response; - - /** - * Time when we started processing the request. - */ - struct GNUNET_TIME_Timestamp now; - - /** - * Period of each bucket. - */ - struct GNUNET_TIME_Relative period; - - /** - * Granularity of the buckets. Matches @e period. - */ - const char *granularity; - - /** - * Number of buckets to return. - */ - uint64_t count; - - /** - * HTTP status to use with @e response. - */ - unsigned int http_status; - - /** - * Length of the @e labels array. - */ - unsigned int labels_cnt; - - /** - * Array of labels for the chart. - */ - char **labels; - - /** - * Data groups for the chart. - */ - json_t *data_groups; - - /** - * #GNUNET_YES if connection was suspended, - * #GNUNET_SYSERR if we were resumed on shutdown. - */ - enum GNUNET_GenericReturnValue suspended; - -}; - - -/** - * DLL of requests awaiting Typst. - */ -static struct ResponseContext *rctx_head; - -/** - * DLL of requests awaiting Typst. - */ -static struct ResponseContext *rctx_tail; - - -void -TMH_handler_statistic_report_transactions_cleanup () -{ - struct ResponseContext *rctx; - - while (NULL != (rctx = rctx_head)) - { - GNUNET_CONTAINER_DLL_remove (rctx_head, - rctx_tail, - rctx); - rctx->suspended = GNUNET_SYSERR; - MHD_resume_connection (rctx->hc->connection); - } -} - - -/** - * Free resources from @a ctx - * - * @param[in] ctx the `struct ResponseContext` to clean up - */ -static void -free_rc (void *ctx) -{ - struct ResponseContext *rctx = ctx; - - if (NULL != rctx->tc) - { - TALER_MHD_typst_cancel (rctx->tc); - rctx->tc = NULL; - } - if (NULL != rctx->response) - { - MHD_destroy_response (rctx->response); - rctx->response = NULL; - } - for (unsigned int i = 0; i<rctx->labels_cnt; i++) - GNUNET_free (rctx->labels[i]); - GNUNET_array_grow (rctx->labels, - rctx->labels_cnt, - 0); - json_decref (rctx->data_groups); - GNUNET_free (rctx); -} - - -/** - * Function called with the result of a #TALER_MHD_typst() operation. - * - * @param cls closure - * @param tr result of the operation - */ -static void -pdf_cb (void *cls, - const struct TALER_MHD_TypstResponse *tr) -{ - struct ResponseContext *rctx = cls; - - rctx->tc = NULL; - GNUNET_CONTAINER_DLL_remove (rctx_head, - rctx_tail, - rctx); - rctx->suspended = GNUNET_NO; - MHD_resume_connection (rctx->hc->connection); - TALER_MHD_daemon_trigger (); - if (TALER_EC_NONE != tr->ec) - { - rctx->http_status - = TALER_ErrorCode_get_http_status (tr->ec); - rctx->response - = TALER_MHD_make_error (tr->ec, - tr->details.hint); - return; - } - rctx->http_status - = MHD_HTTP_OK; - rctx->response - = TALER_MHD_response_from_pdf_file (tr->details.filename); -} - - -/** - * Typically called by `lookup_statistics_amount_by_bucket2`. - * - * @param[in,out] cls our `struct ResponseContext` to update - * @param bucket_start start time of the bucket - * @param amounts_len the length of @a amounts array - * @param amounts the cumulative amounts in the bucket - */ -static void -amount_by_bucket (void *cls, - struct GNUNET_TIME_Timestamp bucket_start, - unsigned int amounts_len, - const struct TALER_Amount amounts[static amounts_len]) -{ - struct ResponseContext *rctx = cls; - json_t *values; - - for (unsigned int i = 0; i<amounts_len; i++) - { - bool found = false; - - for (unsigned int j = 0; j<rctx->labels_cnt; j++) - { - if (0 == strcmp (amounts[i].currency, - rctx->labels[j])) - { - found = true; - break; - } - } - if (! found) - { - GNUNET_array_append (rctx->labels, - rctx->labels_cnt, - GNUNET_strdup (amounts[i].currency)); - } - } - - values = json_array (); - GNUNET_assert (NULL != values); - for (unsigned int i = 0; i<rctx->labels_cnt; i++) - { - const char *label = rctx->labels[i]; - double d = 0.0; - - for (unsigned int j = 0; j<amounts_len; j++) - { - const struct TALER_Amount *a = &amounts[j]; - - if (0 != strcmp (amounts[j].currency, - label)) - continue; - d = a->value * 1.0 - + (a->fraction * 1.0 / TALER_AMOUNT_FRAC_BASE); - break; - } /* for all amounts */ - GNUNET_assert (0 == - json_array_append_new (values, - json_real (d))); - } /* for all labels */ - - { - json_t *dg; - - dg = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("start_date", - bucket_start), - GNUNET_JSON_pack_array_steal ("values", - values)); - GNUNET_assert (0 == - json_array_append_new (rctx->data_groups, - dg)); - - } -} - - -/** - * Create the transaction volume report. - * - * @param[in,out] rctx request context to use - * @param[in,out] charts JSON chart array to expand - * @return #GNUNET_OK on success, - * #GNUNET_NO to end with #MHD_YES, - * #GNUNET_NO to end with #MHD_NO. - */ -static enum GNUNET_GenericReturnValue -make_transaction_volume_report (struct ResponseContext *rctx, - json_t *charts) -{ - const char *bucket_name = "deposits-received"; - enum GNUNET_DB_QueryStatus qs; - json_t *chart; - json_t *labels; - - rctx->data_groups = json_array (); - GNUNET_assert (NULL != rctx->data_groups); - qs = TMH_db->lookup_statistics_amount_by_bucket2 ( - TMH_db->cls, - rctx->hc->instance->settings.id, - bucket_name, - rctx->granularity, - rctx->count, - &amount_by_bucket, - rctx); - if (0 > qs) - { - GNUNET_break (0); - return (MHD_YES == - TALER_MHD_reply_with_error ( - rctx->hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_amount_by_bucket2")) - ? GNUNET_NO : GNUNET_SYSERR; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - json_decref (rctx->data_groups); - rctx->data_groups = NULL; - return GNUNET_OK; - } - - labels = json_array (); - GNUNET_assert (NULL != labels); - for (unsigned int i=0; i<rctx->labels_cnt; i++) - { - GNUNET_assert (0 == - json_array_append_new (labels, - json_string (rctx->labels[i]))); - GNUNET_free (rctx->labels[i]); - } - GNUNET_array_grow (rctx->labels, - rctx->labels_cnt, - 0); - chart = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("chart_name", - "Sales volume"), - GNUNET_JSON_pack_string ("y_label", - "Sales"), - GNUNET_JSON_pack_array_steal ("data_groups", - rctx->data_groups), - GNUNET_JSON_pack_array_steal ("labels", - labels), - GNUNET_JSON_pack_bool ("cumulative", - false)); - rctx->data_groups = NULL; - GNUNET_assert (0 == - json_array_append_new (charts, - chart)); - return GNUNET_OK; -} - - -/** - * Typically called by `lookup_statistics_counter_by_bucket2`. - * - * @param[in,out] cls our `struct ResponseContext` to update - * @param bucket_start start time of the bucket - * @param counters_len the length of @a cumulative_amounts - * @param descriptions description for the counter in the bucket - * @param counters the counters in the bucket - */ -static void -count_by_bucket (void *cls, - struct GNUNET_TIME_Timestamp bucket_start, - unsigned int counters_len, - const char *descriptions[static counters_len], - uint64_t counters[static counters_len]) -{ - struct ResponseContext *rctx = cls; - json_t *values; - - for (unsigned int i = 0; i<counters_len; i++) - { - bool found = false; - - for (unsigned int j = 0; j<rctx->labels_cnt; j++) - { - if (0 == strcmp (descriptions[i], - rctx->labels[j])) - { - found = true; - break; - } - } - if (! found) - { - GNUNET_array_append (rctx->labels, - rctx->labels_cnt, - GNUNET_strdup (descriptions[i])); - } - } - - values = json_array (); - GNUNET_assert (NULL != values); - for (unsigned int i = 0; i<rctx->labels_cnt; i++) - { - const char *label = rctx->labels[i]; - uint64_t v = 0; - - for (unsigned int j = 0; j<counters_len; j++) - { - if (0 != strcmp (descriptions[j], - label)) - continue; - v = counters[j]; - break; - } /* for all amounts */ - GNUNET_assert (0 == - json_array_append_new (values, - json_integer (v))); - } /* for all labels */ - - { - json_t *dg; - - dg = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("start_date", - bucket_start), - GNUNET_JSON_pack_array_steal ("values", - values)); - GNUNET_assert (0 == - json_array_append_new (rctx->data_groups, - dg)); - - } -} - - -/** - * Create the transaction count report. - * - * @param[in,out] rctx request context to use - * @param[in,out] charts JSON chart array to expand - * @return #GNUNET_OK on success, - * #GNUNET_NO to end with #MHD_YES, - * #GNUNET_NO to end with #MHD_NO. - */ -static enum GNUNET_GenericReturnValue -make_transaction_count_report (struct ResponseContext *rctx, - json_t *charts) -{ - const char *prefix = "orders-paid"; - enum GNUNET_DB_QueryStatus qs; - json_t *chart; - json_t *labels; - - rctx->data_groups = json_array (); - GNUNET_assert (NULL != rctx->data_groups); - qs = TMH_db->lookup_statistics_counter_by_bucket2 ( - TMH_db->cls, - rctx->hc->instance->settings.id, - prefix, /* prefix to match against bucket name */ - rctx->granularity, - rctx->count, - &count_by_bucket, - rctx); - if (0 > qs) - { - GNUNET_break (0); - return (MHD_YES == - TALER_MHD_reply_with_error ( - rctx->hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_statistics_counter_by_bucket2")) - ? GNUNET_NO : GNUNET_SYSERR; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - json_decref (rctx->data_groups); - rctx->data_groups = NULL; - return GNUNET_OK; - } - labels = json_array (); - GNUNET_assert (NULL != labels); - for (unsigned int i=0; i<rctx->labels_cnt; i++) - { - const char *label = rctx->labels[i]; - - /* This condition should always hold. */ - if (0 == - strncmp (prefix, - label, - strlen (prefix))) - label += strlen (prefix); - GNUNET_assert (0 == - json_array_append_new (labels, - json_string (label))); - GNUNET_free (rctx->labels[i]); - } - GNUNET_array_grow (rctx->labels, - rctx->labels_cnt, - 0); - chart = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("chart_name", - "Transaction counts"), - GNUNET_JSON_pack_string ("y_label", - "Number of transactions"), - GNUNET_JSON_pack_array_steal ("data_groups", - rctx->data_groups), - GNUNET_JSON_pack_array_steal ("labels", - labels), - GNUNET_JSON_pack_bool ("cumulative", - false)); - rctx->data_groups = NULL; - GNUNET_assert (0 == - json_array_append_new (charts, - chart)); - return GNUNET_OK; -} - - -/** - * Handle a GET "/private/statistics-report/transactions" 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_statistics_report_transactions ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct ResponseContext *rctx = hc->ctx; - struct TMH_MerchantInstance *mi = hc->instance; - json_t *charts; - - if (NULL != rctx) - { - GNUNET_assert (GNUNET_YES != rctx->suspended); - if (GNUNET_SYSERR == rctx->suspended) - return MHD_NO; - if (NULL == rctx->response) - { - GNUNET_break (0); - return MHD_NO; - } - return MHD_queue_response (connection, - rctx->http_status, - rctx->response); - } - rctx = GNUNET_new (struct ResponseContext); - rctx->hc = hc; - rctx->now = GNUNET_TIME_timestamp_get (); - hc->ctx = rctx; - hc->cc = &free_rc; - GNUNET_assert (NULL != mi); - - rctx->granularity = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "granularity"); - if (NULL == rctx->granularity) - { - rctx->granularity = "day"; - rctx->period = GNUNET_TIME_UNIT_DAYS; - rctx->count = 95; - } - else - { - const struct - { - const char *name; - struct GNUNET_TIME_Relative period; - uint64_t default_counter; - } map[] = { - { - .name = "second", - .period = GNUNET_TIME_UNIT_SECONDS, - .default_counter = 120, - }, - { - .name = "minute", - .period = GNUNET_TIME_UNIT_MINUTES, - .default_counter = 120, - }, - { - .name = "hour", - .period = GNUNET_TIME_UNIT_HOURS, - .default_counter = 48, - }, - { - .name = "day", - .period = GNUNET_TIME_UNIT_DAYS, - .default_counter = 95, - }, - { - .name = "month", - .period = GNUNET_TIME_UNIT_MONTHS, - .default_counter = 36, - }, - { - .name = "quarter", - .period = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MONTHS, - 3), - .default_counter = 40, - }, - { - .name = "year", - .period = GNUNET_TIME_UNIT_YEARS, - .default_counter = 10 - }, - { - .name = NULL - } - }; - - rctx->count = 0; - for (unsigned int i = 0; map[i].name != NULL; i++) - { - if (0 == strcasecmp (map[i].name, - rctx->granularity)) - { - rctx->count = map[i].default_counter; - rctx->period = map[i].period; - break; - } - } - if (0 == rctx->count) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "granularity"); - } - } /* end handling granularity */ - - /* Figure out desired output format */ - { - const char *mime; - - mime = MHD_lookup_connection_value (connection, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT); - if (NULL == mime) - mime = "application/json"; - if (0 == strcmp (mime, - "application/json")) - { - rctx->format = RCF_JSON; - } - else if (0 == strcmp (mime, - "application/pdf")) - { - - rctx->format = RCF_PDF; - } - else - { - GNUNET_break_op (0); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_NOT_ACCEPTABLE, - GNUNET_JSON_pack_string ("hint", - mime)); - } - } /* end of determine output format */ - - TALER_MHD_parse_request_number (connection, - "count", - &rctx->count); - - /* create charts */ - charts = json_array (); - GNUNET_assert (NULL != charts); - { - enum GNUNET_GenericReturnValue ret; - - ret = make_transaction_volume_report (rctx, - charts); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; - ret = make_transaction_count_report (rctx, - charts); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; - } - - /* generate response */ - { - struct GNUNET_TIME_Timestamp start_date; - struct GNUNET_TIME_Timestamp end_date; - json_t *root; - - end_date = rctx->now; - start_date - = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_subtract ( - end_date.abs_time, - GNUNET_TIME_relative_multiply (rctx->period, - rctx->count))); - root = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("business_name", - mi->settings.name), - GNUNET_JSON_pack_timestamp ("start_date", - start_date), - GNUNET_JSON_pack_timestamp ("end_date", - end_date), - GNUNET_JSON_pack_time_rel ("bucket_period", - rctx->period), - GNUNET_JSON_pack_array_steal ("charts", - charts)); - - switch (rctx->format) - { - case RCF_JSON: - return TALER_MHD_reply_json (connection, - root, - MHD_HTTP_OK); - case RCF_PDF: - { - struct TALER_MHD_TypstDocument doc = { - .form_name = "transactions", - .data = root - }; - - rctx->tc = TALER_MHD_typst (TMH_cfg, - false, /* remove on exit */ - "merchant", - 1, /* one document, length of "array"! */ - &doc, - &pdf_cb, - rctx); - json_decref (root); - if (NULL == rctx->tc) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Client requested PDF, but Typst is unavailable\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK, - NULL); - } - GNUNET_CONTAINER_DLL_insert (rctx_head, - rctx_tail, - rctx); - rctx->suspended = GNUNET_YES; - MHD_suspend_connection (connection); - return MHD_YES; - } - } /* end switch */ - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-get-statistics-report-transactions.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.h b/src/backend/taler-merchant-httpd_private-get-statistics-report-transactions.h @@ -1,49 +0,0 @@ -/* - 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-httpd_private-get-statistics-report-transactions.h - * @brief implement GET /statistics-report/transactions - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_REPORT_TRANSACTIONS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_STATISTICS_REPORT_TRANSACTIONS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/statistics-report/transactions" 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_statistics_report_transactions ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Cleanup ongoing report requests. - */ -void -TMH_handler_statistic_report_transactions_cleanup (void); - -/* end of taler-merchant-httpd_private-get-statistics-report-transactions.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-templates-ID.c b/src/backend/taler-merchant-httpd_private-get-templates-ID.c @@ -1,80 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-templates-ID.c - * @brief implement GET /templates/$ID - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-templates-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_get_templates_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - qs = TMH_db->lookup_template (TMH_db->cls, - mi->settings.id, - hc->infix, - &tp); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_template"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - hc->infix); - } - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("editable_defaults", - tp.editable_defaults)), - GNUNET_JSON_pack_string ("template_description", - tp.template_description), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("otp_id", - tp.otp_id)), - GNUNET_JSON_pack_object_incref ("template_contract", - tp.template_contract)); - TALER_MERCHANTDB_template_details_free (&tp); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-get-templates-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-templates-ID.h b/src/backend/taler-merchant-httpd_private-get-templates-ID.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-templates-ID.h - * @brief implement GET /templates/$ID/ - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/templates/$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_templates_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-templates-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-templates.c b/src/backend/taler-merchant-httpd_private-get-templates.c @@ -1,79 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-templates.c - * @brief implement GET /templates - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-templates.h" - - -/** - * Add template details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param template_id ID of the template - * @param template_description human-readable description for the template - */ -static void -add_template (void *cls, - const char *template_id, - const char *template_description) -{ - json_t *pa = cls; - - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("template_id", template_id), - GNUNET_JSON_pack_string ("template_description", - template_description)))); -} - - -MHD_RESULT -TMH_private_get_templates (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *pa; - enum GNUNET_DB_QueryStatus qs; - - pa = json_array (); - GNUNET_assert (NULL != pa); - qs = TMH_db->lookup_templates (TMH_db->cls, - hc->instance->settings.id, - &add_template, - pa); - if (0 > qs) - { - GNUNET_break (0); - json_decref (pa); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_templates"); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("templates", - pa)); -} - - -/* end of taler-merchant-httpd_private-get-templates.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-templates.h b/src/backend/taler-merchant-httpd_private-get-templates.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-templates.h - * @brief implement GET /templates - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TEMPLATES_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/templates" 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_templates (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-templates.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.c b/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.c @@ -1,126 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 2024 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-httpd_private-get-token-families-SLUG.c - * @brief implement GET /tokenfamilies/$SLUG/ - * @author Christian Blättler - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-token-families-SLUG.h" -#include <gnunet/gnunet_json_lib.h> -#include <taler/taler_json_lib.h> - - -/** - * Handle a GET "/tokenfamilies/$SLUG" 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_tokenfamilies_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 }; - enum GNUNET_DB_QueryStatus status; - - GNUNET_assert (NULL != mi); - status = TMH_db->lookup_token_family (TMH_db->cls, - mi->settings.id, - hc->infix, - &details); - if (0 > status) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_token_family"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == status) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TOKEN_FAMILY_UNKNOWN, - hc->infix); - } - { - char *kind = NULL; - MHD_RESULT result; - - if (TALER_MERCHANTDB_TFK_Subscription == details.kind) - { - kind = GNUNET_strdup ("subscription"); - } - else if (TALER_MERCHANTDB_TFK_Discount == details.kind) - { - kind = GNUNET_strdup ("discount"); - } - else - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "invalid_token_family_kind"); - } - - result = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("slug", - details.slug), - GNUNET_JSON_pack_string ("name", - details.name), - GNUNET_JSON_pack_string ("description", - details.description), - GNUNET_JSON_pack_object_steal ("description_i18n", - details.description_i18n), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("extra_data", - details.extra_data)), - GNUNET_JSON_pack_timestamp ("valid_after", - details.valid_after), - GNUNET_JSON_pack_timestamp ("valid_before", - details.valid_before), - GNUNET_JSON_pack_time_rel ("duration", - details.duration), - GNUNET_JSON_pack_time_rel ("validity_granularity", - details.validity_granularity), - GNUNET_JSON_pack_time_rel ("start_offset", - details.start_offset), - GNUNET_JSON_pack_string ("kind", - kind), - GNUNET_JSON_pack_int64 ("issued", - details.issued), - GNUNET_JSON_pack_int64 ("used", - details.used) - ); - - GNUNET_free (details.name); - GNUNET_free (details.description); - GNUNET_free (details.cipher_spec); - GNUNET_free (kind); - return result; - } -} - - -/* end of taler-merchant-httpd_private-get-token-families-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.h b/src/backend/taler-merchant-httpd_private-get-token-families-SLUG.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-get-token-families-SLUG.h - * @brief implement GET /tokenfamilies/$SLUG/ - * @author Christian Blättler - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_SLUG_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_SLUG_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/tokenfamilies/$SLUG" 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_tokenfamilies_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-token-families-SLUG.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-token-families.c b/src/backend/taler-merchant-httpd_private-get-token-families.c @@ -1,101 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 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-httpd_private-get-token-families.c - * @brief implement GET /tokenfamilies - * @author Christian Blättler - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-token-families.h" - - -/** - * Add token family details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param slug slug of the token family - * @param name name of the token family - * @param valid_after start time of the token family's validity period - * @param valid_before end time of the token family's validity period - * @param kind kind of the token family - */ -static void -add_token_family (void *cls, - const char *slug, - const char *name, - const char *description, - const json_t *description_i18n, - struct GNUNET_TIME_Timestamp valid_after, - struct GNUNET_TIME_Timestamp valid_before, - const char *kind) -{ - json_t *pa = cls; - - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("slug", - slug), - GNUNET_JSON_pack_string ("name", - name), - GNUNET_JSON_pack_string ("description", - description), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("description_i18n", - (json_t *) - description_i18n)), - GNUNET_JSON_pack_timestamp ("valid_after", - valid_after), - GNUNET_JSON_pack_timestamp ("valid_before", - valid_before), - GNUNET_JSON_pack_string ("kind", - kind)))); -} - - -MHD_RESULT -TMH_private_get_tokenfamilies (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *families; - enum GNUNET_DB_QueryStatus qs; - - families = json_array (); - GNUNET_assert (NULL != families); - qs = TMH_db->lookup_token_families (TMH_db->cls, - hc->instance->settings.id, - &add_token_family, - families); - if (0 > qs) - { - GNUNET_break (0); - json_decref (families); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ( - "token_families", - families)); -} - - -/* end of taler-merchant-httpd_private-get-token-families.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-token-families.h b/src/backend/taler-merchant-httpd_private-get-token-families.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-get-token-families.h - * @brief implement GET /tokenfamilies - * @author Christian Blättler - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TOKENFAMILIES_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/tokenfamilies" 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_tokenfamilies (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-token-families.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-transfers.c b/src/backend/taler-merchant-httpd_private-get-transfers.c @@ -1,189 +0,0 @@ -/* - This file is part of TALER - (C) 2014-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-httpd_private-get-transfers.c - * @brief implement API for obtaining a list of wire transfers - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_private-get-transfers.h" - - -/** - * Function called with information about a wire transfer. - * Generate a response (array entry) based on the given arguments. - * - * @param cls closure with a `json_t *` array to build up the response - * @param credit_amount amount expected to be wired to the merchant (minus fees), NULL if unknown - * @param wtid wire transfer identifier - * @param payto_uri target account that received the wire transfer - * @param exchange_url base URL of the exchange that made the wire transfer - * @param transfer_serial_id serial number identifying the transfer in the backend - * @param expected_transfer_serial_id serial number identifying the expected transfer in the backend, 0 if not @a expected - * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS - * if it did not yet happen - * @param expected true if the merchant acknowledged the wire transfer reception - */ -static void -transfer_cb (void *cls, - const struct TALER_Amount *credit_amount, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct TALER_FullPayto payto_uri, - const char *exchange_url, - uint64_t transfer_serial_id, - uint64_t expected_transfer_serial_id, - struct GNUNET_TIME_Absolute execution_time, - bool expected) -{ - json_t *ja = cls; - json_t *r; - - r = GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("credit_amount", - credit_amount), - GNUNET_JSON_pack_data_auto ("wtid", - wtid), - TALER_JSON_pack_full_payto ("payto_uri", - payto_uri), - GNUNET_JSON_pack_string ("exchange_url", - exchange_url), - GNUNET_JSON_pack_uint64 ("transfer_serial_id", - transfer_serial_id), - (0 == expected_transfer_serial_id) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("dummy", - NULL)) - : GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", - expected_transfer_serial_id), - // FIXME: protocol breaking to remove... - GNUNET_JSON_pack_bool ("verified", - false), - // FIXME: protocol breaking to remove... - GNUNET_JSON_pack_bool ("confirmed", - true), - GNUNET_JSON_pack_bool ("expected", - expected), - GNUNET_TIME_absolute_is_zero (execution_time) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("dummy", - NULL)) - : GNUNET_JSON_pack_timestamp ( - "execution_time", - GNUNET_TIME_absolute_to_timestamp (execution_time))); - GNUNET_assert (0 == - json_array_append_new (ja, - r)); -} - - -/** - * Manages a GET /private/transfers call. - * - * @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_transfers (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TALER_FullPayto payto_uri = { - .full_payto = NULL - }; - struct GNUNET_TIME_Timestamp before = GNUNET_TIME_UNIT_FOREVER_TS; - struct GNUNET_TIME_Timestamp after = GNUNET_TIME_UNIT_ZERO_TS; - int64_t limit = -20; - uint64_t offset; - enum TALER_EXCHANGE_YesNoAll expected; - - (void) rh; - TALER_MHD_parse_request_snumber (connection, - "limit", - &limit); - if (limit < 0) - offset = INT64_MAX; - else - offset = 0; - TALER_MHD_parse_request_number (connection, - "offset", - &offset); - TALER_MHD_parse_request_yna (connection, - "expected", - TALER_EXCHANGE_YNA_ALL, - &expected); - TALER_MHD_parse_request_timestamp (connection, - "before", - &before); - TALER_MHD_parse_request_timestamp (connection, - "after", - &after); - { - const char *esc_payto; - - esc_payto = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "payto_uri"); - if (NULL != esc_payto) - { - payto_uri.full_payto - = GNUNET_strdup (esc_payto); - (void) MHD_http_unescape (payto_uri.full_payto); - } - } - TMH_db->preflight (TMH_db->cls); - { - json_t *ja; - enum GNUNET_DB_QueryStatus qs; - - ja = json_array (); - GNUNET_assert (NULL != ja); - qs = TMH_db->lookup_transfers (TMH_db->cls, - hc->instance->settings.id, - payto_uri, - before, - after, - limit, - offset, - expected, - &transfer_cb, - ja); - GNUNET_free (payto_uri.full_payto); - if (0 > qs) - { - /* Simple select queries should not cause serialization issues */ - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); - /* Always report on hard error as well to enable diagnostics */ - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "transfers"); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("transfers", - ja)); - } -} - - -/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-transfers.h b/src/backend/taler-merchant-httpd_private-get-transfers.h @@ -1,42 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2020 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/> -*/ -/** - * @file taler-merchant-httpd_private-get-transfers.h - * @brief headers for GET /transfers handler - * @author Christian Grothoff - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_TRANSFERS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_TRANSFERS_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Manages a GET /private/transfers call. - * - * @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_transfers (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-units-ID.c b/src/backend/taler-merchant-httpd_private-get-units-ID.c @@ -1,89 +0,0 @@ -/* - 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-httpd_private-get-units-ID.c - * @brief implement GET /private/units/$UNIT - * @author Bohdan Potuzhnyi - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-units-ID.h" - - -MHD_RESULT -TMH_private_get_units_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TALER_MERCHANTDB_UnitDetails ud = { 0 }; - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT ret; - - (void) rh; - GNUNET_assert (NULL != hc->infix); - qs = TMH_db->select_unit (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &ud); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, - hc->infix); - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "unit"); - } - - ret = TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("unit_serial", - ud.unit_serial), - GNUNET_JSON_pack_string ("unit", - ud.unit), - GNUNET_JSON_pack_string ("unit_name_long", - ud.unit_name_long), - GNUNET_JSON_pack_object_incref ( - "unit_name_long_i18n", - ud.unit_name_long_i18n), - GNUNET_JSON_pack_string ("unit_name_short", - ud.unit_name_short), - GNUNET_JSON_pack_object_incref ( - "unit_name_short_i18n", - ud.unit_name_short_i18n), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - ud.unit_allow_fraction - ), - GNUNET_JSON_pack_uint64 ( - "unit_precision_level", - ud.unit_precision_level), - GNUNET_JSON_pack_bool ("unit_active", - ud.unit_active), - GNUNET_JSON_pack_bool ("unit_builtin", - ud.unit_builtin)); - TALER_MERCHANTDB_unit_details_free (&ud); - return ret; -} - - -/* end of taler-merchant-httpd_private-get-units-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-units-ID.h b/src/backend/taler-merchant-httpd_private-get-units-ID.h @@ -1,33 +0,0 @@ -/* - 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-httpd_private-get-units-ID.h - * @brief implement GET /private/units/$UNIT - * @author Bohdan Potuzhnyi - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_ID_H - -#include "taler-merchant-httpd.h" - - -MHD_RESULT -TMH_private_get_units_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-units-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-units.c b/src/backend/taler-merchant-httpd_private-get-units.c @@ -1,91 +0,0 @@ -/* - 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-httpd_private-get-units.c - * @brief implement GET /private/units - * @author Bohdan Potuzhnyi - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-units.h" - - -static void -add_unit (void *cls, - uint64_t unit_serial, - const struct TALER_MERCHANTDB_UnitDetails *ud) -{ - json_t *ua = cls; - - GNUNET_assert ( - 0 == - json_array_append_new ( - ua, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("unit_serial", - unit_serial), - GNUNET_JSON_pack_string ("unit", - ud->unit), - GNUNET_JSON_pack_string ("unit_name_long", - ud->unit_name_long), - GNUNET_JSON_pack_object_incref ("unit_name_long_i18n", - (json_t *) ud->unit_name_long_i18n), - GNUNET_JSON_pack_string ("unit_name_short", - ud->unit_name_short), - GNUNET_JSON_pack_object_incref ("unit_name_short_i18n", - (json_t *) ud->unit_name_short_i18n), - GNUNET_JSON_pack_bool ("unit_allow_fraction", - ud->unit_allow_fraction), - GNUNET_JSON_pack_uint64 ("unit_precision_level", - ud->unit_precision_level), - GNUNET_JSON_pack_bool ("unit_active", - ud->unit_active), - GNUNET_JSON_pack_bool ("unit_builtin", - ud->unit_builtin)))); -} - - -MHD_RESULT -TMH_private_get_units (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *ua; - enum GNUNET_DB_QueryStatus qs; - - (void) rh; - ua = json_array (); - GNUNET_assert (NULL != ua); - qs = TMH_db->lookup_units (TMH_db->cls, - hc->instance->settings.id, - &add_unit, - ua); - if (0 > qs) - { - GNUNET_break (0); - json_decref (ua); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("units", - ua)); -} - - -/* end of taler-merchant-httpd_private-get-units.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-units.h b/src/backend/taler-merchant-httpd_private-get-units.h @@ -1,33 +0,0 @@ -/* - 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-httpd_private-get-units.h - * @brief implement GET /private/units - * @author Bohdan Potuzhnyi - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_UNITS_H - -#include "taler-merchant-httpd.h" - - -MHD_RESULT -TMH_private_get_units (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-units.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-webhooks-ID.c b/src/backend/taler-merchant-httpd_private-get-webhooks-ID.c @@ -1,92 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-webhooks-ID.c - * @brief implement GET /webhooks/$ID - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-webhooks-ID.h" -#include <taler/taler_json_lib.h> - - -/** - * Handle a GET "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_WebhookDetails wb = { 0 }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - qs = TMH_db->lookup_webhook (TMH_db->cls, - mi->settings.id, - hc->infix, - &wb); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_webhook"); - } - if (0 == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_WEBHOOK_UNKNOWN, - hc->infix); - } - { - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("event_type", - wb.event_type), - GNUNET_JSON_pack_string ("url", - wb.url), - GNUNET_JSON_pack_string ("http_method", - wb.http_method), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("header_template", - wb.header_template)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("body_template", - wb.body_template))); - GNUNET_free (wb.event_type); - GNUNET_free (wb.url); - GNUNET_free (wb.http_method); - GNUNET_free (wb.header_template); - GNUNET_free (wb.body_template); - - return ret; - } -} - - -/* end of taler-merchant-httpd_private-get-webhooks-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-webhooks-ID.h b/src/backend/taler-merchant-httpd_private-get-webhooks-ID.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-webhooks-ID.h - * @brief implement GET /webhooks/$ID/ - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/webhooks/$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_webhooks_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-webhooks-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-get-webhooks.c b/src/backend/taler-merchant-httpd_private-get-webhooks.c @@ -1,80 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-webhooks.c - * @brief implement GET /webhooks - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-get-webhooks.h" - - -/** - * Add webhook details to our JSON array. - * - * @param cls a `json_t *` JSON array to build - * @param webhook_id ID of the webhook - * @param event_type what type of event is the hook for - */ -static void -add_webhook (void *cls, - const char *webhook_id, - const char *event_type) -{ - json_t *pa = cls; - - GNUNET_assert (0 == - json_array_append_new ( - pa, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("webhook_id", - webhook_id), - GNUNET_JSON_pack_string ("event_type", - event_type)))); -} - - -MHD_RESULT -TMH_private_get_webhooks (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - json_t *pa; - enum GNUNET_DB_QueryStatus qs; - - pa = json_array (); - GNUNET_assert (NULL != pa); - qs = TMH_db->lookup_webhooks (TMH_db->cls, - hc->instance->settings.id, - &add_webhook, - pa); - if (0 > qs) - { - GNUNET_break (0); - json_decref (pa); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_array_steal ("webhooks", - pa)); -} - - -/* end of taler-merchant-httpd_private-get-webhooks.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-webhooks.h b/src/backend/taler-merchant-httpd_private-get-webhooks.h @@ -1,41 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-get-webhooks.h - * @brief implement GET /webhooks - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_GET_WEBHOOKS_H - -#include "taler-merchant-httpd.h" - - -/** - * Handle a GET "/webhooks" 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_webhooks (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-get-webhooks.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-accounts-ID.c b/src/backend/taler-merchant-httpd_private-patch-accounts-ID.c @@ -1,149 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 2025, 2026 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-httpd_private-patch-accounts-ID.c - * @brief implementing PATCH /accounts/$ID request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-accounts-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_mfa.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_accounts_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *h_wire_s = hc->infix; - enum GNUNET_DB_QueryStatus qs; - const json_t *cfc; - const char *extra_wire_subject_metadata = NULL; - const char *cfu; - struct TALER_MerchantWireHashP h_wire; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("credit_facade_url", - &cfu), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("extra_wire_subject_metadata", - &extra_wire_subject_metadata), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("credit_facade_credentials", - &cfc), - NULL), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != h_wire_s); - if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (h_wire_s, - strlen (h_wire_s), - &h_wire, - sizeof (h_wire))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_GENERIC_H_WIRE_MALFORMED, - h_wire_s); - } - if (! TALER_is_valid_subject_metadata_string ( - extra_wire_subject_metadata)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "extra_wire_subject_metadata"); - } - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - qs = TMH_db->update_account (TMH_db->cls, - mi->settings.id, - &h_wire, - extra_wire_subject_metadata, - cfu, - cfc); - { - MHD_RESULT ret = MHD_NO; - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "update_account"); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ACCOUNT_UNKNOWN, - h_wire_s); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-patch-accounts-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-accounts-ID.h b/src/backend/taler-merchant-httpd_private-patch-accounts-ID.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-patch-accounts-ID.h - * @brief implementing PATCH /accounts request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ACCOUNTS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ACCOUNTS_ID_H -#include "taler-merchant-httpd.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_accounts_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-categories-ID.c b/src/backend/taler-merchant-httpd_private-patch-categories-ID.c @@ -1,120 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-patch-categories-ID.c - * @brief implementing PATCH /categories/$ID request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-categories-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_patch_categories_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - unsigned long long cnum; - char dummy; - const char *category_name; - const json_t *category_name_i18n; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("name", - &category_name), - GNUNET_JSON_spec_object_const ("name_i18n", - &category_name_i18n), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != hc->infix); - if (1 != sscanf (hc->infix, - "%llu%c", - &cnum, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "category_id must be a number"); - } - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - qs = TMH_db->update_category (TMH_db->cls, - mi->settings.id, - cnum, - category_name, - category_name_i18n); - { - MHD_RESULT ret = MHD_NO; - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "update_category"); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, - category_name); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-patch-categories-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-categories-ID.h b/src/backend/taler-merchant-httpd_private-patch-categories-ID.h @@ -1,45 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-patch-categories-ID.h - * @brief implementing PATCH /private/categories/$ID request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_CATEGORIES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_CATEGORIES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * PATCH descriptions of an existing product category. - * - * @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_categories_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-group-ID.c b/src/backend/taler-merchant-httpd_private-patch-group-ID.c @@ -1,110 +0,0 @@ -/* - 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-group-ID.c - * @brief implementation of PATCH /private/groups/$GROUP_ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-group-ID.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_patch_group (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *group_id_str = hc->infix; - unsigned long long group_id; - const char *group_name; - const char *description; - enum GNUNET_DB_QueryStatus qs; - bool conflict; - char dummy; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("group_name", - &group_name), - GNUNET_JSON_spec_string ("description", - &description), - GNUNET_JSON_spec_end () - }; - - (void) rh; - if (1 != sscanf (group_id_str, - "%llu%c", - &group_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "group_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_product_group (TMH_db->cls, - hc->instance->settings.id, - (uint64_t) group_id, - group_name, - description, - &conflict); - - 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_product_group"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, - group_id_str); - } - if (conflict) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_PRODUCT_GROUP_CONFLICTING_NAME, - group_name); - } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} diff --git a/src/backend/taler-merchant-httpd_private-patch-group-ID.h b/src/backend/taler-merchant-httpd_private-patch-group-ID.h @@ -1,41 +0,0 @@ -/* - 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-group-ID.h - * @brief HTTP serving layer for updating product groups - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_GROUP_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_GROUP_ID_H - -#include "taler-merchant-httpd.h" - -/** - * Handle PATCH /private/groups/$GROUP_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_group (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-instances-ID.c b/src/backend/taler-merchant-httpd_private-patch-instances-ID.c @@ -1,514 +0,0 @@ -/* - This file is part of TALER - (C) 2020-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-httpd_private-patch-instances-ID.c - * @brief implementing PATCH /instances/$ID request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-instances-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include "taler-merchant-httpd_mfa.h" - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Free memory used by @a wm - * - * @param wm wire method to free - */ -static void -free_wm (struct TMH_WireMethod *wm) -{ - GNUNET_free (wm->payto_uri.full_payto); - GNUNET_free (wm->wire_method); - GNUNET_free (wm); -} - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @param mi instance to patch - * @param mfa_check true if a MFA check is required - * @param connection the MHD connection to handle - * @param[in,out] hc context with further information about the request - * @return MHD result code - */ -static MHD_RESULT -patch_instances_ID (struct TMH_MerchantInstance *mi, - bool mfa_check, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TALER_MERCHANTDB_InstanceSettings is; - const char *name; - struct TMH_WireMethod *wm_head = NULL; - struct TMH_WireMethod *wm_tail = NULL; - const char *iphone = NULL; - bool no_transfer_delay; - bool no_pay_delay; - bool no_refund_delay; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("name", - &name), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("website", - (const char **) &is.website), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("email", - (const char **) &is.email), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("phone_number", - &iphone), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("logo", - (const char **) &is.logo), - NULL), - GNUNET_JSON_spec_json ("address", - &is.address), - GNUNET_JSON_spec_json ("jurisdiction", - &is.jurisdiction), - GNUNET_JSON_spec_bool ("use_stefan", - &is.use_stefan), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("default_pay_delay", - &is.default_pay_delay), - &no_pay_delay), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("default_refund_delay", - &is.default_refund_delay), - &no_refund_delay), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay", - &is.default_wire_transfer_delay), - &no_transfer_delay), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_time_rounder_interval ( - "default_wire_transfer_rounding_interval", - &is.default_wire_transfer_rounding_interval), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - memset (&is, - 0, - sizeof (is)); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - if (! TMH_location_object_valid (is.address)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); - } - if ( (NULL != is.logo) && - (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "logo"); - } - - if (! TMH_location_object_valid (is.jurisdiction)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "jurisdiction"); - } - - if (no_transfer_delay) - is.default_wire_transfer_delay = mi->settings.default_wire_transfer_delay; - if (no_pay_delay) - is.default_pay_delay = mi->settings.default_pay_delay; - if (no_refund_delay) - is.default_refund_delay = mi->settings.default_refund_delay; - if (GNUNET_TIME_relative_is_forever (is.default_pay_delay)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "default_pay_delay"); - } - if (GNUNET_TIME_relative_is_forever (is.default_refund_delay)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "default_refund_delay"); - } - if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "default_wire_transfer_delay"); - } - if (NULL != iphone) - { - is.phone = TALER_MERCHANT_phone_validate_normalize (iphone, - false); - if (NULL == is.phone) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "phone_number"); - } - if ( (NULL != TMH_phone_regex) && - (0 != - regexec (&TMH_phone_rx, - is.phone, - 0, - NULL, - 0)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "phone_number"); - } - } - if ( (NULL != is.email) && - (! TALER_MERCHANT_email_valid (is.email)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "email"); - } - if ( (NULL != is.phone) && - (NULL != mi->settings.phone) && - (0 == strcmp (mi->settings.phone, - is.phone)) ) - is.phone_validated = mi->settings.phone_validated; - if ( (NULL != is.email) && - (NULL != mi->settings.email) && - (0 == strcmp (mi->settings.email, - is.email)) ) - is.email_validated = mi->settings.email_validated; - if (mfa_check) - { - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels; - - if ( (0 != (mtc & TEH_TCS_SMS)) && - (NULL != mi->settings.phone) && - (NULL == is.phone) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "phone_number"); - } - if ( (0 != (mtc & TEH_TCS_EMAIL)) && - (NULL != mi->settings.email) && - (NULL == is.email) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "email"); - } - if ( (is.phone_validated || - (NULL == is.phone) ) && - (0 != (mtc & TEH_TCS_SMS)) ) - mtc -= TEH_TCS_SMS; - if ( (is.email_validated || - (NULL == is.email) ) && - (0 != (mtc & TEH_TCS_EMAIL)) ) - mtc -= TEH_TCS_EMAIL; - switch (mtc) - { - case TEH_TCS_NONE: - ret = GNUNET_OK; - break; - case TEH_TCS_SMS: - GNUNET_assert (NULL != is.phone); - is.phone_validated = true; - /* validate new phone number, if possible require old e-mail - address for authorization */ - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, - true, - TALER_MERCHANT_MFA_CHANNEL_SMS, - is.phone, - 0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL - & TEH_mandatory_tan_channels) - ? TALER_MERCHANT_MFA_CHANNEL_NONE - : TALER_MERCHANT_MFA_CHANNEL_EMAIL, - mi->settings.email, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - case TEH_TCS_EMAIL: - GNUNET_assert (NULL != is.email); - is.email_validated = true; - /* validate new e-mail address, if possible require old phone - address for authorization */ - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, - true, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - is.email, - 0 == (TALER_MERCHANT_MFA_CHANNEL_SMS - & TEH_mandatory_tan_channels) - ? TALER_MERCHANT_MFA_CHANNEL_NONE - : TALER_MERCHANT_MFA_CHANNEL_SMS, - mi->settings.phone, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - case TEH_TCS_EMAIL_AND_SMS: - GNUNET_assert (NULL != mi->settings.phone); - GNUNET_assert (NULL != mi->settings.email); - is.phone_validated = true; - is.email_validated = true; - /* To change both, we require both old and both new - addresses to consent */ - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, - true, - TALER_MERCHANT_MFA_CHANNEL_SMS, - mi->settings.phone, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - mi->settings.email, - TALER_MERCHANT_MFA_CHANNEL_SMS, - is.phone, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - is.email, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - } - if (GNUNET_OK != ret) - { - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - } - - for (unsigned int retry = 0; retry<MAX_RETRIES; retry++) - { - /* Cleanup after earlier loops */ - { - struct TMH_WireMethod *wm; - - while (NULL != (wm = wm_head)) - { - GNUNET_CONTAINER_DLL_remove (wm_head, - wm_tail, - wm); - free_wm (wm); - } - } - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "PATCH /instances")) - { - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - /* Check for equality of settings */ - if (! ( (0 == strcmp (mi->settings.name, - name)) && - ((mi->settings.email == is.email) || - (NULL != is.email && NULL != mi->settings.email && - 0 == strcmp (mi->settings.email, - is.email))) && - ((mi->settings.phone == is.phone) || - (NULL != is.phone && NULL != mi->settings.phone && - 0 == strcmp (mi->settings.phone, - is.phone))) && - ((mi->settings.website == is.website) || - (NULL != is.website && NULL != mi->settings.website && - 0 == strcmp (mi->settings.website, - is.website))) && - ((mi->settings.logo == is.logo) || - (NULL != is.logo && NULL != mi->settings.logo && - 0 == strcmp (mi->settings.logo, - is.logo))) && - (1 == json_equal (mi->settings.address, - is.address)) && - (1 == json_equal (mi->settings.jurisdiction, - is.jurisdiction)) && - (mi->settings.use_stefan == is.use_stefan) && - (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay, - ==, - is.default_wire_transfer_delay)) && - (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay, - ==, - is.default_refund_delay)) && - (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay, - ==, - is.default_pay_delay)) ) ) - { - is.id = mi->settings.id; - is.name = GNUNET_strdup (name); - qs = TMH_db->update_instance (TMH_db->cls, - &is); - GNUNET_free (is.name); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - goto retry; - else - goto giveup; - } - } - qs = TMH_db->commit (TMH_db->cls); -retry: - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - continue; - break; - } /* for(... MAX_RETRIES) */ -giveup: - /* Update our 'settings' */ - GNUNET_free (mi->settings.name); - GNUNET_free (mi->settings.email); - GNUNET_free (mi->settings.phone); - GNUNET_free (mi->settings.website); - GNUNET_free (mi->settings.logo); - json_decref (mi->settings.address); - json_decref (mi->settings.jurisdiction); - is.id = mi->settings.id; - mi->settings = is; - mi->settings.address = json_incref (mi->settings.address); - mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); - mi->settings.name = GNUNET_strdup (name); - if (NULL != is.email) - mi->settings.email = GNUNET_strdup (is.email); - mi->settings.phone = is.phone; - is.phone = NULL; - if (NULL != is.website) - mi->settings.website = GNUNET_strdup (is.website); - if (NULL != is.logo) - mi->settings.logo = GNUNET_strdup (is.logo); - - GNUNET_JSON_parse_free (spec); - TMH_reload_instances (mi->settings.id); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -MHD_RESULT -TMH_private_patch_instances_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - - return patch_instances_ID (mi, - true, - connection, - hc); -} - - -MHD_RESULT -TMH_private_patch_instances_default_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi; - - mi = TMH_lookup_instance (hc->infix); - if (NULL == mi) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); - } - if (mi->deleted) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED, - hc->infix); - } - return patch_instances_ID (mi, - false, - connection, - hc); -} - - -/* end of taler-merchant-httpd_private-patch-instances-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-instances-ID.h b/src/backend/taler-merchant-httpd_private-patch-instances-ID.h @@ -1,59 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-patch-instances-ID.h - * @brief implementing POST /instances request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_INSTANCES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_INSTANCES_ID_H -#include "taler-merchant-httpd.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * This is the handler called using the instance's own authentication. - * - * @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_instances_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * PATCH configuration of an existing instance, given its configuration. - * This is the handler called using the default instance's authentication. - * - * @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_instances_default_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c b/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.c @@ -1,243 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-patch-orders-ID-forget.c - * @brief implementing PATCH /orders/$ORDER_ID/forget request handling - * @author Jonathan Buchanan - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-orders-ID-forget.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the UPDATE database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Forget part of the contract terms. - * - * @param cls pointer to the result of the forget operation. - * @param object_id name of the object to forget. - * @param parent parent of the object at @e object_id. - */ -static void -forget (void *cls, - const char *object_id, - json_t *parent) -{ - int *res = cls; - int ret; - - ret = TALER_JSON_contract_part_forget (parent, - object_id); - if (GNUNET_SYSERR == ret) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Matching path `%s' not forgettable!\n", - object_id); - *res = GNUNET_SYSERR; - } - if (GNUNET_NO == ret) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Matching path `%s' already forgotten!\n", - object_id); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Forgot `%s'\n", - object_id); - if (GNUNET_NO == *res) - *res = GNUNET_OK; - } -} - - -/** - * Forget fields of an order's contract terms. - * - * @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_orders_ID_forget (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *order_id = hc->infix; - enum GNUNET_DB_QueryStatus qs; - uint64_t order_serial; - - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - const json_t *fields; - json_t *contract_terms; - bool changed = false; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "forget order")) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - hc->instance->settings.id, - order_id, - &contract_terms, - &order_serial, - NULL); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "contract terms"); - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - continue; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - order_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - GNUNET_assert (NULL != contract_terms); - break; - } - - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("fields", - &fields), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - { - TMH_db->rollback (TMH_db->cls); - json_decref (contract_terms); - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - } - { - size_t index; - json_t *value; - - json_array_foreach (fields, index, value) { - int forget_status = GNUNET_NO; - int expand_status; - - if (! (json_is_string (value))) - { - TMH_db->rollback (TMH_db->cls); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT, - "field is not a string"); - } - expand_status = TALER_JSON_expand_path (contract_terms, - json_string_value (value), - &forget, - &forget_status); - if (GNUNET_SYSERR == forget_status) - { - /* We tried to forget a field that isn't forgettable */ - TMH_db->rollback (TMH_db->cls); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE, - json_string_value (value)); - } - if (GNUNET_OK == forget_status) - changed = true; - if (GNUNET_SYSERR == expand_status) - { - /* One of the paths was malformed and couldn't be expanded */ - TMH_db->rollback (TMH_db->cls); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT, - json_string_value (value)); - } - } - } - - if (! changed) - { - TMH_db->rollback (TMH_db->cls); - json_decref (contract_terms); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - qs = TMH_db->update_contract_terms (TMH_db->cls, - hc->instance->settings.id, - order_id, - contract_terms); - json_decref (contract_terms); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - else - { - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - } - if (0 > qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - } - - return TALER_MHD_reply_static (connection, - MHD_HTTP_OK, - NULL, - NULL, - 0); -} diff --git a/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.h b/src/backend/taler-merchant-httpd_private-patch-orders-ID-forget.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-patch-orders-ID-forget.h - * @brief implementing PATCH /orders/$ORDER_ID/forget request handling - * @author Jonathan Buchanan - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ORDERS_ID_FORGET_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_ORDERS_ID_FORGET_H -#include "taler-merchant-httpd.h" - - -/** - * Forget fields of an order's contract terms. - * - * @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_orders_ID_forget (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-otp-devices-ID.c b/src/backend/taler-merchant-httpd_private-patch-otp-devices-ID.c @@ -1,114 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-patch-otp-devices-ID.c - * @brief implementing PATCH /otp-devices/$ID request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-otp-devices-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_patch_otp_devices_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *device_id = hc->infix; - struct TALER_MERCHANTDB_OtpDeviceDetails tp = {0}; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("otp_device_description", - (const char **) &tp.otp_description), - TALER_JSON_spec_otp_type ("otp_algorithm", - &tp.otp_algorithm), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("otp_ctr", - &tp.otp_ctr), - NULL), - GNUNET_JSON_spec_mark_optional ( - - TALER_JSON_spec_otp_key ("otp_key", - (const char **) &tp.otp_key), - NULL), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != device_id); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - qs = TMH_db->update_otp (TMH_db->cls, - mi->settings.id, - device_id, - &tp); - { - MHD_RESULT ret = MHD_NO; - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "update_pos"); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, - device_id); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-patch-otp-devices-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-otp-devices-ID.h b/src/backend/taler-merchant-httpd_private-patch-otp-devices-ID.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-patch-otp-devices-ID.h - * @brief implementing PATCH /otp-devices/$ID request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_OTP_DEVICES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_OTP_DEVICES_ID_H - -#include "taler-merchant-httpd.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_otp_devices_ID (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 @@ -1,152 +0,0 @@ -/* - 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 "taler/platform.h" -#include "taler-merchant-httpd_private-patch-pot-ID.h" -#include <taler/taler_json_lib.h> - - -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; - size_t expected_pot_total_len; - struct TALER_Amount *expected_pot_totals; - bool no_expected_total; - size_t new_pot_total_len; - struct TALER_Amount *new_pot_totals; - 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_array ("expected_pot_total", - &expected_pot_total_len, - &expected_pot_totals), - &no_expected_total), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any_array ("new_pot_totals", - &new_pot_total_len, - &new_pot_totals), - &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, - expected_pot_total_len, - no_expected_total - ? NULL - : expected_pot_totals, - new_pot_total_len, - no_new_total - ? NULL - : new_pot_totals, - &conflict_total, - &conflict_name); - GNUNET_JSON_parse_free (spec); - 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 @@ -1,41 +0,0 @@ -/* - 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-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -1,482 +0,0 @@ -/* - This file is part of TALER - (C) 2020--2026 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-httpd_private-patch-products-ID.c - * @brief implementing PATCH /products/$ID request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-products-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_products_ID ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *product_id = hc->infix; - struct TALER_MERCHANTDB_ProductDetails pd = {0}; - const json_t *categories = NULL; - int64_t total_stock; - const char *unit_total_stock = NULL; - bool unit_total_stock_missing; - bool total_stock_missing; - struct TALER_Amount price; - bool price_missing; - bool unit_price_missing; - bool unit_allow_fraction; - bool unit_allow_fraction_missing; - uint32_t unit_precision_level; - bool unit_precision_missing; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - /* new in protocol v20, thus optional for backwards-compatibility */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("product_name", - (const char **) &pd.product_name), - NULL), - GNUNET_JSON_spec_string ("description", - (const char **) &pd.description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("description_i18n", - &pd.description_i18n), - NULL), - GNUNET_JSON_spec_string ("unit", - (const char **) &pd.unit), - // FIXME: deprecated API - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("price", - &price), - &price_missing), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any_array ("unit_price", - &pd.price_array_length, - &pd.price_array), - &unit_price_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("image", - (const char **) &pd.image), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("taxes", - &pd.taxes), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("categories", - &categories), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_total_stock", - &unit_total_stock), - &unit_total_stock_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_int64 ("total_stock", - &total_stock), - &total_stock_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("unit_allow_fraction", - &unit_allow_fraction), - &unit_allow_fraction_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("unit_precision_level", - &unit_precision_level), - &unit_precision_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("total_lost", - &pd.total_lost), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("product_group_id", - &pd.product_group_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("money_pot_id", - &pd.money_pot_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("address", - &pd.address), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("next_restock", - &pd.next_restock), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("minimum_age", - &pd.minimum_age), - NULL), - GNUNET_JSON_spec_end () - }; - MHD_RESULT ret; - size_t num_cats = 0; - uint64_t *cats = NULL; - bool no_instance; - ssize_t no_cat; - bool no_product; - bool lost_reduced; - bool sold_reduced; - bool stock_reduced; - bool no_group; - bool no_pot; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != product_id); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - /* For pre-v20 clients, we use the description given as the - product name; remove once we make product_name mandatory. */ - if (NULL == pd.product_name) - pd.product_name = pd.description; - } - if (! unit_price_missing) - { - if (! price_missing) - { - if (0 != TALER_amount_cmp (&price, - &pd.price_array[0])) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "price,unit_price mismatch"); - goto cleanup; - } - } - if (GNUNET_OK != - TMH_validate_unit_price_array (pd.price_array, - pd.price_array_length)) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_price"); - goto cleanup; - } - } - else - { - if (price_missing) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "price missing"); - goto cleanup; - } - pd.price_array = GNUNET_new_array (1, - struct TALER_Amount); - pd.price_array[0] = price; - pd.price_array_length = 1; - } - if (! unit_precision_missing) - { - if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_precision_level"); - goto cleanup; - } - } - { - bool default_allow_fractional; - uint32_t default_precision_level; - - if (GNUNET_OK != - TMH_unit_defaults_for_instance (mi, - pd.unit, - &default_allow_fractional, - &default_precision_level)) - { - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "unit defaults"); - goto cleanup; - } - if (unit_allow_fraction_missing) - unit_allow_fraction = default_allow_fractional; - if (unit_precision_missing) - unit_precision_level = default_precision_level; - - if (! unit_allow_fraction) - unit_precision_level = 0; - pd.fractional_precision_level = unit_precision_level; - } - { - const char *eparam; - if (GNUNET_OK != - TALER_MERCHANT_vk_process_quantity_inputs ( - TALER_MERCHANT_VK_STOCK, - unit_allow_fraction, - total_stock_missing, - total_stock, - unit_total_stock_missing, - unit_total_stock, - &pd.total_stock, - &pd.total_stock_frac, - &eparam)) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - eparam); - goto cleanup; - } - pd.allow_fractional_quantity = unit_allow_fraction; - } - if (NULL == pd.address) - pd.address = json_object (); - - if (! TMH_location_object_valid (pd.address)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); - goto cleanup; - } - num_cats = json_array_size (categories); - cats = GNUNET_new_array (num_cats, - uint64_t); - { - size_t idx; - json_t *val; - - json_array_foreach (categories, idx, val) - { - if (! json_is_integer (val)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "categories"); - goto cleanup; - } - cats[idx] = json_integer_value (val); - } - } - - if (NULL == pd.description_i18n) - pd.description_i18n = json_object (); - - if (! TALER_JSON_check_i18n (pd.description_i18n)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); - goto cleanup; - } - - if (NULL == pd.taxes) - pd.taxes = json_array (); - /* check taxes is well-formed */ - if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "taxes"); - goto cleanup; - } - - if (NULL == pd.image) - pd.image = (char *) ""; - if (! TALER_MERCHANT_image_data_url_valid (pd.image)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image"); - goto cleanup; - } - - if ( (pd.total_stock < pd.total_sold + pd.total_lost) || - (pd.total_sold + pd.total_lost < pd.total_sold) /* integer overflow */) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS, - NULL); - goto cleanup; - } - - qs = TMH_db->update_product (TMH_db->cls, - mi->settings.id, - product_id, - &pd, - num_cats, - cats, - &no_instance, - &no_cat, - &no_product, - &lost_reduced, - &sold_reduced, - &stock_reduced, - &no_group, - &no_pot); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - goto cleanup; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected problem in stored procedure"); - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - if (no_instance) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } - if (-1 != no_cat) - { - char cat_str[24]; - - GNUNET_snprintf (cat_str, - sizeof (cat_str), - "%llu", - (unsigned long long) no_cat); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, - cat_str); - goto cleanup; - } - if (no_product) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - product_id); - goto cleanup; - } - if (no_group) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, - NULL); - goto cleanup; - } - if (no_pot) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, - NULL); - goto cleanup; - } - if (lost_reduced) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED, - NULL); - goto cleanup; - } - if (sold_reduced) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED, - NULL); - goto cleanup; - } - if (stock_reduced) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED, - NULL); - goto cleanup; - } - /* success! */ - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -cleanup: - GNUNET_free (cats); - GNUNET_free (pd.price_array); - GNUNET_JSON_parse_free (spec); - return ret; -} - - -/* end of taler-merchant-httpd_private-patch-products-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.h b/src/backend/taler-merchant-httpd_private-patch-products-ID.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-patch-products-ID.h - * @brief implementing PATCH /products/$ID request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_PRODUCTS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_PRODUCTS_ID_H -#include "taler-merchant-httpd.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_products_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-report-ID.c b/src/backend/taler-merchant-httpd_private-patch-report-ID.c @@ -1,146 +0,0 @@ -/* - 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-report-ID.c - * @brief implementation of PATCH /private/reports/$REPORT_ID - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-report-ID.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> - -MHD_RESULT -TMH_private_patch_report (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *report_id_str = hc->infix; - unsigned long long report_id; - const char *description; - const char *program_section; - const char *mime_type; - const char *data_source; - const char *target_address; - struct GNUNET_TIME_Relative frequency; - struct GNUNET_TIME_Relative frequency_shift; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("description", - &description), - GNUNET_JSON_spec_string ("program_section", - &program_section), - GNUNET_JSON_spec_string ("mime_type", - &mime_type), - GNUNET_JSON_spec_string ("data_source", - &data_source), - GNUNET_JSON_spec_string ("target_address", - &target_address), - GNUNET_JSON_spec_relative_time ("report_frequency", - &frequency), - GNUNET_JSON_spec_relative_time ("report_frequency_shift", - &frequency_shift), - GNUNET_JSON_spec_end () - }; - - (void) rh; - { - char dummy; - - if (1 != sscanf (report_id_str, - "%llu%c", - &report_id, - &dummy)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "report_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; - } - } - if ('/' != data_source[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "data_source"); - - } - - qs = TMH_db->update_report (TMH_db->cls, - hc->instance->settings.id, - report_id, - program_section, - description, - mime_type, - data_source, - target_address, - frequency, - frequency_shift); - 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_report"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_REPORT_UNKNOWN, - report_id_str); - } - - /* FIXME-Optimization: Trigger MERCHANT_REPORT_UPDATE event inside of UPDATE transaction */ - { - struct GNUNET_DB_EventHeaderP ev = { - .size = htons (sizeof (ev)), - .type = htons (TALER_DBEVENT_MERCHANT_REPORT_UPDATE) - }; - - TMH_db->event_notify (TMH_db->cls, - &ev, - NULL, - 0); - } - - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} diff --git a/src/backend/taler-merchant-httpd_private-patch-report-ID.h b/src/backend/taler-merchant-httpd_private-patch-report-ID.h @@ -1,41 +0,0 @@ -/* - 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-report-ID.h - * @brief HTTP serving layer for updating reports - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_REPORT_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_REPORT_ID_H - -#include "taler-merchant-httpd.h" - -/** - * Handle PATCH /private/reports/$REPORT_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_report (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-templates-ID.c b/src/backend/taler-merchant-httpd_private-patch-templates-ID.c @@ -1,217 +0,0 @@ -/* - This file is part of TALER - (C) 2022, 2024 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-httpd_private-patch-templates-ID.c - * @brief implementing PATCH /templates/$ID request handling - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-templates-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * Determine the cause of the PATCH failure in more detail and report. - * - * @param connection connection to report on - * @param instance_id instance we are processing - * @param template_id ID of the product to patch - * @param tp template details we failed to set - */ -static MHD_RESULT -determine_cause (struct MHD_Connection *connection, - const char *instance_id, - const char *template_id, - const struct TALER_MERCHANTDB_TemplateDetails *tp) -{ - struct TALER_MERCHANTDB_TemplateDetails tpx; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_template (TMH_db->cls, - instance_id, - template_id, - &tpx); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - template_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* do below */ - } - - { - enum TALER_ErrorCode ec; - - ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - TALER_MERCHANTDB_template_details_free (&tpx); - GNUNET_break (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE != ec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - ec, - NULL); - } -} - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *template_id = hc->infix; - struct TALER_MERCHANTDB_TemplateDetails tp = {0}; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("template_description", - (const char **) &tp.template_description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("otp_id", - (const char **) &tp.otp_id), - NULL), - GNUNET_JSON_spec_json ("template_contract", - &tp.template_contract), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("editable_defaults", - &tp.editable_defaults), - NULL), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != template_id); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - if (! TALER_MERCHANT_template_contract_valid (tp.template_contract)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "template_contract"); - } - if (NULL != tp.editable_defaults) - { - const char *key; - json_t *val; - - json_object_foreach (tp.editable_defaults, key, val) - { - if (NULL != - json_object_get (tp.template_contract, - key)) - { - char *msg; - MHD_RESULT ret; - - GNUNET_break_op (0); - GNUNET_asprintf (&msg, - "editable_defaults::%s conflicts with template_contract", - key); - GNUNET_JSON_parse_free (spec); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - GNUNET_free (msg); - return ret; - } - } - } - - qs = TMH_db->update_template (TMH_db->cls, - mi->settings.id, - template_id, - &tp); - { - MHD_RESULT ret = MHD_NO; - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = determine_cause (connection, - mi->settings.id, - template_id, - &tp); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-patch-templates-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-templates-ID.h b/src/backend/taler-merchant-httpd_private-patch-templates-ID.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-patch-templates-ID.h - * @brief implementing PATCH /templates request handling - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TEMPLATES_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TEMPLATES_ID_H -#include "taler-merchant-httpd.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_templates_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-token-families-SLUG.c b/src/backend/taler-merchant-httpd_private-patch-token-families-SLUG.c @@ -1,159 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 2024 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-httpd_private-patch-token-families-SLUG.c - * @brief implementing PATCH /tokenfamilies/$SLUG request handling - * @author Christian Blättler - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-token-families-SLUG.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Handle a PATCH "/tokenfamilies/$slug" 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_token_family_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *slug = hc->infix; - struct TALER_MERCHANTDB_TokenFamilyDetails details = {0}; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("name", - (const char **) &details.name), - GNUNET_JSON_spec_string ("description", - (const char **) &details.description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("description_i18n", - &details.description_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("extra_data", - &details.extra_data), - NULL), - GNUNET_JSON_spec_timestamp ("valid_after", - &details.valid_after), - GNUNET_JSON_spec_timestamp ("valid_before", - &details.valid_before), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != slug); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - /* Ensure that valid_after is before valid_before */ - if (GNUNET_TIME_timestamp_cmp (details.valid_after, - >=, - details.valid_before)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "valid_after >= valid_before"); - } - - if (NULL == details.description_i18n) - { - details.description_i18n = json_object (); - GNUNET_assert (NULL != details.description_i18n); - } - if (! TALER_JSON_check_i18n (details.description_i18n)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); - } - - { - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT ret = MHD_NO; - - qs = TMH_db->update_token_family (TMH_db->cls, - mi->settings.id, - slug, - &details); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND, - slug); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-patch-token-families-SLUG.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-token-families-SLUG.h b/src/backend/taler-merchant-httpd_private-patch-token-families-SLUG.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-patch-token-families-SLUG.h - * @brief implementing PATCH /tokenfamilies/$SLUG request handling - * @author Christian Blättler - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TOKEN_FAMILIES_SLUG_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_TOKEN_FAMILIES_SLUG_H -#include "taler-merchant-httpd.h" - - -/** - * Handle a PATCH "/tokenfamilies/$slug" 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_token_family_SLUG (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-units-ID.c b/src/backend/taler-merchant-httpd_private-patch-units-ID.c @@ -1,242 +0,0 @@ -/* - 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-httpd_private-patch-units-ID.c - * @brief implement PATCH /private/units/$UNIT - * @author Bohdan Potuzhnyi - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-units-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - -#define TMH_MAX_UNIT_PRECISION_LEVEL 6 - - -MHD_RESULT -TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *unit_id = hc->infix; - struct TALER_MERCHANTDB_UnitDetails nud = { 0 }; - bool unit_allow_fraction_missing = true; - bool unit_precision_missing = true; - bool unit_active_missing = true; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_name_long", - (const char **) &nud.unit_name_long), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("unit_name_long_i18n", - &nud.unit_name_long_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_name_short", - (const char **) &nud.unit_name_short), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("unit_name_short_i18n", - &nud.unit_name_short_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("unit_allow_fraction", - &nud.unit_allow_fraction), - &unit_allow_fraction_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("unit_precision_level", - &nud.unit_precision_level), - &unit_precision_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("unit_active", - &nud.unit_active), - &unit_active_missing), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const bool *unit_allow_fraction_ptr = NULL; - const uint32_t *unit_precision_ptr = NULL; - const bool *unit_active_ptr = NULL; - enum GNUNET_DB_QueryStatus qs; - bool no_instance = false; - bool no_unit = false; - bool builtin_conflict = false; - MHD_RESULT ret = MHD_YES; - - (void) rh; - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != unit_id); - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - - if (NULL == nud.unit_name_long && - NULL == nud.unit_name_long_i18n && - NULL == nud.unit_name_short && - NULL == nud.unit_name_short_i18n && - unit_allow_fraction_missing && - unit_precision_missing && - unit_active_missing) - { - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - goto cleanup; - } - - if (! unit_precision_missing) - { - if (nud.unit_precision_level > TMH_MAX_UNIT_PRECISION_LEVEL) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_precision_level"); - goto cleanup; - } - unit_precision_ptr = &nud.unit_precision_level; - } - - if (! unit_allow_fraction_missing) - { - unit_allow_fraction_ptr = &nud.unit_allow_fraction; - if (! nud.unit_allow_fraction) - { - nud.unit_precision_level = 0; - unit_precision_missing = false; - unit_precision_ptr = &nud.unit_precision_level; - } - } - - if (! unit_active_missing) - unit_active_ptr = &nud.unit_active; - - if (NULL != nud.unit_name_long_i18n) - { - if (! TALER_JSON_check_i18n (nud.unit_name_long_i18n)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_name_long_i18n"); - goto cleanup; - } - } - - if (NULL != nud.unit_name_short_i18n) - { - if (! TALER_JSON_check_i18n (nud.unit_name_short_i18n)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_name_short_i18n"); - goto cleanup; - } - } - - qs = TMH_db->update_unit (TMH_db->cls, - mi->settings.id, - unit_id, - nud.unit_name_long, - nud.unit_name_long_i18n, - nud.unit_name_short, - nud.unit_name_short_i18n, - unit_allow_fraction_ptr, - unit_precision_ptr, - unit_active_ptr, - &no_instance, - &no_unit, - &builtin_conflict); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "update_unit"); - goto cleanup; - case GNUNET_DB_STATUS_HARD_ERROR: - default: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "update_unit"); - goto cleanup; - } - - if (no_instance) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } - if (no_unit) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_UNIT_UNKNOWN, - unit_id); - goto cleanup; - } - if (builtin_conflict) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN, - unit_id); - goto cleanup; - } - - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - -cleanup: - if (NULL != nud.unit_name_long_i18n) - { - json_decref (nud.unit_name_long_i18n); - nud.unit_name_long_i18n = NULL; - } - if (NULL != nud.unit_name_short_i18n) - { - json_decref (nud.unit_name_short_i18n); - nud.unit_name_short_i18n = NULL; - } - GNUNET_JSON_parse_free (spec); - return ret; -} - - -/* end of taler-merchant-httpd_private-patch-units-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-units-ID.h b/src/backend/taler-merchant-httpd_private-patch-units-ID.h @@ -1,33 +0,0 @@ -/* - 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-httpd_private-patch-units-ID.h - * @brief implement PATCH /private/units/$UNIT - * @author Bohdan Potuzhnyi - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_UNITS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_UNITS_ID_H - -#include "taler-merchant-httpd.h" - - -MHD_RESULT -TMH_private_patch_units_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-patch-units-ID.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-patch-webhooks-ID.c b/src/backend/taler-merchant-httpd_private-patch-webhooks-ID.c @@ -1,188 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-patch-webhooks-ID.c - * @brief implementing PATCH /webhooks/$ID request handling - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-patch-webhooks-ID.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Determine the cause of the PATCH failure in more detail and report. - * - * @param connection connection to report on - * @param instance_id instance we are processing - * @param webhook_id ID of the webhook to patch - * @param wb webhook details we failed to set - */ -static MHD_RESULT -determine_cause (struct MHD_Connection *connection, - const char *instance_id, - const char *webhook_id, - const struct TALER_MERCHANTDB_WebhookDetails *wb) -{ - struct TALER_MERCHANTDB_WebhookDetails wpx; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_webhook (TMH_db->cls, - instance_id, - webhook_id, - &wpx); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_WEBHOOK_UNKNOWN, - webhook_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; /* do below */ - } - - { - enum TALER_ErrorCode ec; - - ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - TALER_MERCHANTDB_webhook_details_free (&wpx); - GNUNET_break (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE != ec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - ec, - NULL); - } -} - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_webhooks_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *webhook_id = hc->infix; - struct TALER_MERCHANTDB_WebhookDetails wb = {0}; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("event_type", - (const char **) &wb.event_type), - TALER_JSON_spec_web_url ("url", - (const char **) &wb.url), - GNUNET_JSON_spec_string ("http_method", - (const char **) &wb.http_method), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("header_template", - (const char **) &wb.header_template), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("body_template", - (const char **) &wb.body_template), - NULL), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != webhook_id); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - - qs = TMH_db->update_webhook (TMH_db->cls, - mi->settings.id, - webhook_id, - &wb); - { - MHD_RESULT ret = MHD_NO; - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "unexpected serialization problem"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = determine_cause (connection, - mi->settings.id, - webhook_id, - &wb); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - break; - } - GNUNET_JSON_parse_free (spec); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-patch-webhooks-ID.c */ diff --git a/src/backend/taler-merchant-httpd_private-patch-webhooks-ID.h b/src/backend/taler-merchant-httpd_private-patch-webhooks-ID.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-patch-webhooks-ID.h - * @brief implementing PATCH /webhooks request handling - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_PATCH_WEBHOOKS_ID_H -#define TALER_MERCHANT_HTTPD_PRIVATE_PATCH_WEBHOOKS_ID_H -#include "taler-merchant-httpd.h" - - -/** - * PATCH configuration of an existing instance, given its configuration. - * - * @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_webhooks_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-account.c b/src/backend/taler-merchant-httpd_private-post-account.c @@ -1,470 +0,0 @@ -/* - This file is part of TALER - (C) 2020-2024 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-httpd_private-post-account.c - * @brief implementing POST /private/accounts request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-account.h" -#include "taler-merchant-httpd_helper.h" -#include "taler/taler_merchant_bank_lib.h" -#include <taler/taler_dbevents.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_mfa.h" -#include <regex.h> - -/** - * Maximum number of retries we do on serialization failures. - */ -#define MAX_RETRIES 5 - -/** - * Closure for account_cb(). - */ -struct PostAccountContext -{ - /** - * Payto URI of the account to add (from the request). - */ - struct TALER_FullPayto uri; - - /** - * Hash of the wire details (@e uri and @e salt). - * Set if @e have_same_account is true. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * Salt value used for hashing @e uri. - * Set if @e have_same_account is true. - */ - struct TALER_WireSaltP salt; - - /** - * Credit facade URL from the request. - */ - const char *credit_facade_url; - - /** - * Facade credentials from the request. - */ - const json_t *credit_facade_credentials; - - /** - * Wire subject metadata from the request. - */ - const char *extra_wire_subject_metadata; - - /** - * True if we have ANY account already and thus require MFA. - */ - bool have_any_account; - - /** - * True if we have exact match already and thus require MFA. - */ - bool have_same_account; - - /** - * True if we have an account with the same normalized payto - * already and thus the client can only do PATCH but not POST. - */ - bool have_conflicting_account; -}; - - -/** - * Callback invoked with information about a bank account. - * - * @param cls closure with a `struct PostAccountContext` - * @param merchant_priv private key of the merchant instance - * @param ad details about the account - */ -static void -account_cb ( - void *cls, - const struct TALER_MerchantPrivateKeyP *merchant_priv, - const struct TALER_MERCHANTDB_AccountDetails *ad) -{ - struct PostAccountContext *pac = cls; - - if (! ad->active) - return; - pac->have_any_account = true; - if ( (0 == TALER_full_payto_cmp (pac->uri, - ad->payto_uri) ) && - ( (pac->credit_facade_credentials == - ad->credit_facade_credentials) || - ( (NULL != pac->credit_facade_credentials) && - (NULL != ad->credit_facade_credentials) && - (1 == json_equal (pac->credit_facade_credentials, - ad->credit_facade_credentials)) ) ) && - ( (pac->extra_wire_subject_metadata == - ad->extra_wire_subject_metadata) || - ( (NULL != pac->extra_wire_subject_metadata) && - (NULL != ad->extra_wire_subject_metadata) && - (0 == strcmp (pac->extra_wire_subject_metadata, - ad->extra_wire_subject_metadata)) ) ) && - ( (pac->credit_facade_url == ad->credit_facade_url) || - ( (NULL != pac->credit_facade_url) && - (NULL != ad->credit_facade_url) && - (0 == strcmp (pac->credit_facade_url, - ad->credit_facade_url)) ) ) ) - { - pac->have_same_account = true; - pac->salt = ad->salt; - pac->h_wire = ad->h_wire; - return; - } - - if (0 == TALER_full_payto_normalize_and_cmp (pac->uri, - ad->payto_uri) ) - { - pac->have_conflicting_account = true; - return; - } -} - - -MHD_RESULT -TMH_private_post_account (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct PostAccountContext pac = { 0 }; - struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_full_payto_uri ("payto_uri", - &pac.uri), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("credit_facade_url", - &pac.credit_facade_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("extra_wire_subject_metadata", - &pac.extra_wire_subject_metadata), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("credit_facade_credentials", - &pac.credit_facade_credentials), - NULL), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - ispec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - { - char *err; - - if (NULL != - (err = TALER_payto_validate (pac.uri))) - { - MHD_RESULT mret; - - GNUNET_break_op (0); - mret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - err); - GNUNET_free (err); - return mret; - } - } - if (! TALER_is_valid_subject_metadata_string ( - pac.extra_wire_subject_metadata)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "extra_wire_subject_metadata"); - } - - { - char *apt = GNUNET_strdup (TMH_allowed_payment_targets); - char *method = TALER_payto_get_method (pac.uri.full_payto); - bool ok; - - ok = false; - for (const char *tok = strtok (apt, - " "); - NULL != tok; - tok = strtok (NULL, - " ")) - { - if (0 == strcmp ("*", - tok)) - ok = true; - if (0 == strcmp (method, - tok)) - ok = true; - if (ok) - break; - } - GNUNET_free (method); - GNUNET_free (apt); - if (! ok) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - "The payment target type is forbidden by policy"); - } - } - - if ( (NULL != TMH_payment_target_regex) && - (0 != - regexec (&TMH_payment_target_re, - pac.uri.full_payto, - 0, - NULL, - 0)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - "The specific account is forbidden by policy"); - } - - if ( (NULL == pac.credit_facade_url) != - (NULL == pac.credit_facade_credentials) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - (NULL == pac.credit_facade_url) - ? "credit_facade_url" - : "credit_facade_credentials"); - } - if ( (NULL != pac.credit_facade_url) || - (NULL != pac.credit_facade_credentials) ) - { - struct TALER_MERCHANT_BANK_AuthenticationData auth; - - if (GNUNET_OK != - TALER_MERCHANT_BANK_auth_parse_json (pac.credit_facade_credentials, - pac.credit_facade_url, - &auth)) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "credit_facade_url or credit_facade_credentials"); - } - TALER_MERCHANT_BANK_auth_free (&auth); - } - - TMH_db->preflight (TMH_db->cls); - for (unsigned int retries = 0; - retries < MAX_RETRIES; - retries++) - { - enum GNUNET_DB_QueryStatus qs; - struct TMH_WireMethod *wm; - - TMH_db->rollback (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "post-account")) - { - GNUNET_break (0); - break; - } - qs = TMH_db->select_accounts (TMH_db->cls, - mi->settings.id, - &account_cb, - &pac); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_accounts"); - case GNUNET_DB_STATUS_SOFT_ERROR: - continue; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - if (pac.have_same_account) - { - /* Idempotent request */ - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ( - "salt", - &pac.salt), - GNUNET_JSON_pack_data_auto ( - "h_wire", - &pac.h_wire)); - - } - - if (pac.have_conflicting_account) - { - /* Conflict, refuse request */ - TMH_db->rollback (TMH_db->cls); - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, - pac.uri.full_payto); - } - - if (pac.have_any_account) - { - /* MFA needed */ - enum GNUNET_GenericReturnValue ret; - - ret = TMH_mfa_check_simple (hc, - TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, - mi); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Account creation MFA check returned %d\n", - (int) ret); - if (GNUNET_OK != ret) - { - TMH_db->rollback (TMH_db->cls); - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - } - - /* convert provided payto URI into internal data structure with salts */ - wm = TMH_setup_wire_account (pac.uri, - pac.credit_facade_url, - pac.credit_facade_credentials); - GNUNET_assert (NULL != wm); - { - struct TALER_MERCHANTDB_AccountDetails ad = { - .payto_uri = wm->payto_uri, - .salt = wm->wire_salt, - .instance_id = mi->settings.id, - .h_wire = wm->h_wire, - .credit_facade_url = wm->credit_facade_url, - .credit_facade_credentials = wm->credit_facade_credentials, - .extra_wire_subject_metadata = (char *) pac.extra_wire_subject_metadata, - .active = wm->active - }; - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - }; - - qs = TMH_db->insert_account (TMH_db->cls, - &ad); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - TMH_wire_method_free (wm); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "insert_account"); - case GNUNET_DB_STATUS_SOFT_ERROR: - continue; - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TMH_wire_method_free (wm); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_account"); - } - - TMH_db->event_notify (TMH_db->cls, - &es, - NULL, - 0); - qs = TMH_db->commit (TMH_db->cls); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_wire_method_free (wm); - continue; - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_wire_method_free (wm); - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "post-account"); - } - /* Finally, also update our running process */ - GNUNET_CONTAINER_DLL_insert (mi->wm_head, - mi->wm_tail, - wm); - /* Note: we may not need to do this, as we notified - about the account change above. But also hardly hurts. */ - TMH_reload_instances (mi->settings.id); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_data_auto ("salt", - &wm-> - wire_salt - ), - GNUNET_JSON_pack_data_auto ("h_wire", - &wm->h_wire)); - } /* end retries */ - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "post-accounts"); -} - - -/* end of taler-merchant-httpd_private-post-account.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-account.h b/src/backend/taler-merchant-httpd_private-post-account.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - (C) 2020-2023 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-httpd_private-post-account.h - * @brief implementing POST /private/account request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_ACCOUNT_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_ACCOUNT_H - -#include "taler-merchant-httpd.h" - - -/** - * Add bank account to an instance. - * - * @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_account (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-categories.c b/src/backend/taler-merchant-httpd_private-post-categories.c @@ -1,170 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-post-categories.c - * @brief implementing POST /private/categories request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-categories.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -MHD_RESULT -TMH_private_post_categories (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *category_name; - const json_t *category_name_i18n; - uint64_t category_id; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("name", - &category_name), - GNUNET_JSON_spec_object_const ("name_i18n", - &category_name_i18n), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_assert (NULL != mi); - { - 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; - } - } - - /* finally, interact with DB until no serialization error */ - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - json_t *xcategory_name_i18n; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "POST /categories")) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->select_category_by_name (TMH_db->cls, - mi->settings.id, - category_name, - &xcategory_name_i18n, - &category_id); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - /* restart transaction */ - goto retry; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Good, we can proceed! */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* idempotency check: is etp == tp? */ - { - bool eq; - - eq = (1 == json_equal (xcategory_name_i18n, - category_name_i18n)); - json_decref (xcategory_name_i18n); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_REPLY_JSON_PACK (connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("category_id", - category_id)) - : TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_CATEGORIES_CONFLICT_CATEGORY_EXISTS, - category_name); - } - } /* end switch (qs) */ - - qs = TMH_db->insert_category (TMH_db->cls, - mi->settings.id, - category_name, - category_name_i18n, - &category_id); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - break; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } -retry: - GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - } /* for RETRIES loop */ - GNUNET_JSON_parse_free (spec); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - } - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("category_id", - category_id)); -} - - -/* end of taler-merchant-httpd_private-post-categories.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-categories.h b/src/backend/taler-merchant-httpd_private-post-categories.h @@ -1,45 +0,0 @@ -/* - This file is part of TALER - (C) 2024 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-httpd_private-post-categories.h - * @brief implementing POST /categories request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_CATEGORIES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_CATEGORIES_H - -#include "taler-merchant-httpd.h" - - -/** - * Generate a product category. - * - * @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_categories ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-donau-instance.c b/src/backend/taler-merchant-httpd_private-post-donau-instance.c @@ -1,352 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024, 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-httpd_private-post-donau-instance.c - * @brief implementation of POST /donau - * @author Bohdan Potuzhnyi - * @author Vlada Svirsh - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include "donau/donau_service.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include "taler/taler_merchant_service.h" -#include "taler-merchant-httpd_private-post-donau-instance.h" - -/** - * Context for the POST /donau request handler. - */ -struct PostDonauCtx -{ - /** - * Stored in a DLL. - */ - struct PostDonauCtx *next; - - /** - * Stored in a DLL. - */ - struct PostDonauCtx *prev; - - /** - * Connection to the MHD server - */ - struct MHD_Connection *connection; - - /** - * Context of the request handler. - */ - struct TMH_HandlerContext *hc; - - /** - * URL of the DONAU service - * to which the charity belongs. - */ - const char *donau_url; - - /** - * ID of the charity in the DONAU service. - */ - uint64_t charity_id; - - /** - * Handle returned by DONAU_charities_get(); needed to cancel on - * connection abort, etc. - */ - struct DONAU_CharityGetHandle *get_handle; - - /** - * Response to return. - */ - struct MHD_Response *response; - - /** - * HTTP status for @e response. - */ - unsigned int http_status; - - /** - * #GNUNET_YES if we are suspended, - * #GNUNET_NO if not, - * #GNUNET_SYSERR on shutdown - */ - enum GNUNET_GenericReturnValue suspended; -}; - - -/** - * Head of active pay context DLL. - */ -static struct PostDonauCtx *pdc_head; - -/** - * Tail of active pay context DLL. - */ -static struct PostDonauCtx *pdc_tail; - - -void -TMH_force_post_donau_resume () -{ - for (struct PostDonauCtx *pdc = pdc_head; - NULL != pdc; - pdc = pdc->next) - { - if (GNUNET_YES == pdc->suspended) - { - pdc->suspended = GNUNET_SYSERR; - MHD_resume_connection (pdc->connection); - } - } -} - - -/** - * Callback for DONAU_charities_get() to handle the response. - * - * @param cls closure with PostDonauCtx - * @param gcr response from Donau - */ -static void -donau_charity_get_cb (void *cls, - const struct DONAU_GetCharityResponse *gcr) -{ - struct PostDonauCtx *pdc = cls; - enum GNUNET_DB_QueryStatus qs; - - pdc->get_handle = NULL; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing DONAU charity get response"); - /* Anything but 200 => propagate Donau’s response. */ - if (MHD_HTTP_OK != gcr->hr.http_status) - { - pdc->http_status = MHD_HTTP_BAD_GATEWAY; - pdc->response = TALER_MHD_MAKE_JSON_PACK ( - TALER_MHD_PACK_EC (gcr->hr.ec), - GNUNET_JSON_pack_uint64 ("donau_http_status", - gcr->hr.http_status)); - pdc->suspended = GNUNET_NO; - MHD_resume_connection (pdc->connection); - TALER_MHD_daemon_trigger (); - return; - } - - if (0 != - GNUNET_memcmp (&gcr->details.ok.charity.charity_pub.eddsa_pub, - &pdc->hc->instance->merchant_pub.eddsa_pub)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Charity key at donau does not match our merchant key\n"); - pdc->http_status = MHD_HTTP_CONFLICT; - pdc->response = TALER_MHD_make_error ( - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "charity_pub != merchant_pub"); - MHD_resume_connection (pdc->connection); - TALER_MHD_daemon_trigger (); - return; - } - - qs = TMH_db->insert_donau_instance (TMH_db->cls, - pdc->donau_url, - &gcr->details.ok.charity, - pdc->charity_id); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - pdc->response = TALER_MHD_make_error ( - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_donau_instance"); - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - pdc->response = TALER_MHD_make_error ( - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_donau_instance"); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* presumably idempotent + concurrent, no need to notify, but still respond */ - pdc->http_status = MHD_HTTP_NO_CONTENT; - pdc->response = MHD_create_response_from_buffer_static (0, - NULL); - TALER_MHD_add_global_headers (pdc->response, - false); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - { - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_DONAU_KEYS) - }; - - TMH_db->event_notify (TMH_db->cls, - &es, - pdc->donau_url, - strlen (pdc->donau_url) + 1); - pdc->http_status = MHD_HTTP_NO_CONTENT; - pdc->response = MHD_create_response_from_buffer_static (0, - NULL); - TALER_MHD_add_global_headers (pdc->response, - false); - break; - } - } - pdc->suspended = GNUNET_NO; - MHD_resume_connection (pdc->connection); - TALER_MHD_daemon_trigger (); -} - - -/** - * Cleanup function for the PostDonauCtx. - * - * @param cls closure with PostDonauCtx - */ -static void -post_donau_cleanup (void *cls) -{ - struct PostDonauCtx *pdc = cls; - - if (pdc->get_handle) - { - DONAU_charity_get_cancel (pdc->get_handle); - pdc->get_handle = NULL; - } - GNUNET_CONTAINER_DLL_remove (pdc_head, - pdc_tail, - pdc); - GNUNET_free (pdc); -} - - -/** - * Handle a POST "/donau" 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_donau_instance (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct PostDonauCtx *pdc = hc->ctx; - - if (NULL == pdc) - { - enum GNUNET_DB_QueryStatus qs; - - pdc = GNUNET_new (struct PostDonauCtx); - pdc->connection = connection; - pdc->hc = hc; - hc->ctx = pdc; - hc->cc = &post_donau_cleanup; - GNUNET_CONTAINER_DLL_insert (pdc_head, - pdc_tail, - pdc); - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("donau_url", - &pdc->donau_url), - GNUNET_JSON_spec_uint64 ("charity_id", - &pdc->charity_id), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - TALER_MHD_parse_json_data (connection, - hc->request_body, - spec)) - { - GNUNET_break_op (0); - return MHD_NO; - } - } - qs = TMH_db->check_donau_instance (TMH_db->cls, - &hc->instance->merchant_pub, - pdc->donau_url, - pdc->charity_id); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - pdc->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "check_donau_instance"); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - pdc->http_status = MHD_HTTP_NO_CONTENT; - pdc->response = MHD_create_response_from_buffer_static (0, - NULL); - TALER_MHD_add_global_headers (pdc->response, - false); - goto respond; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* normal case, continue below */ - break; - } - - { - struct DONAU_CharityPrivateKeyP cp; - - /* Merchant private key IS our charity private key */ - cp.eddsa_priv = hc->instance->merchant_priv.eddsa_priv; - pdc->get_handle = - DONAU_charity_get (TMH_curl_ctx, - pdc->donau_url, - pdc->charity_id, - &cp, - &donau_charity_get_cb, - pdc); - } - if (NULL == pdc->get_handle) - { - GNUNET_break (0); - GNUNET_free (pdc); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_SERVICE_UNAVAILABLE, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "Failed to initiate Donau lookup"); - } - pdc->suspended = GNUNET_YES; - MHD_suspend_connection (connection); - return MHD_YES; - } -respond: - if (NULL != pdc->response) - { - MHD_RESULT res; - - GNUNET_break (GNUNET_NO == pdc->suspended); - res = MHD_queue_response (pdc->connection, - pdc->http_status, - pdc->response); - MHD_destroy_response (pdc->response); - return res; - } - GNUNET_break (GNUNET_SYSERR == pdc->suspended); - return MHD_NO; -} diff --git a/src/backend/taler-merchant-httpd_private-post-donau-instance.h b/src/backend/taler-merchant-httpd_private-post-donau-instance.h @@ -1,49 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024 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-httpd_private-post-donau-instance.h - * @brief implementation of POST /donau - * @author Bohdan Potuzhnyi - * @author Vlada Svirsh - */ - -#ifndef TALER_MERCHANT_HTTPD_POST_DONAU_INSTANCE_H -#define TALER_MERCHANT_HTTPD_POST_DONAU_INSTANCE_H - -#include "taler-merchant-httpd.h" - - -/** - * Resume all connections suspended on Donau-interaction during shutdown. - */ -void -TMH_force_post_donau_resume (void); - - -/** - * Handle a POST "/donau" 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_donau_instance (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-groups.c b/src/backend/taler-merchant-httpd_private-post-groups.c @@ -1,88 +0,0 @@ -/* - 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-groups.c - * @brief implementation of POST /private/groups - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-groups.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_post_groups (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *group_name; - const char *description; - enum GNUNET_DB_QueryStatus qs; - uint64_t group_id; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("group_name", - &group_name), - GNUNET_JSON_spec_string ("description", - &description), - 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; - } - } - - qs = TMH_db->insert_product_group (TMH_db->cls, - hc->instance->settings.id, - group_name, - description, - &group_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, - "insert_product_group"); - } - 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_PRODUCT_GROUP_CONFLICTING_NAME, - group_name); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("group_serial_id", - group_id)); -} diff --git a/src/backend/taler-merchant-httpd_private-post-groups.h b/src/backend/taler-merchant-httpd_private-post-groups.h @@ -1,41 +0,0 @@ -/* - 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-groups.h - * @brief HTTP serving layer for creating product groups - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_GROUPS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_GROUPS_H - -#include "taler-merchant-httpd.h" - -/** - * Handle POST /private/groups 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_groups (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c @@ -1,342 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-post-instances-ID-auth.c - * @brief implementing POST /instances/$ID/auth request handling - * @author Christian Grothoff - * @author Florian Dold - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-instances-ID-auth.h" -#include "taler-merchant-httpd_auth.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_mfa.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Change the authentication settings of an instance. - * - * @param mi instance to modify settings of - * @param connection the MHD connection to handle - * @param[in,out] hc context with further information about the request - * @param auth_override The authentication settings for this instance - * do not apply due to administrative action. Do not check - * against the DB value when updating the auth token. - * @param tcs set of multi-factor authorizations required - * @return MHD result code - */ -static MHD_RESULT -post_instances_ID_auth (struct TMH_MerchantInstance *mi, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - bool auth_override, - enum TEH_TanChannelSet tcs) -{ - struct TALER_MERCHANTDB_InstanceAuthSettings ias; - const char *auth_pw = NULL; - json_t *jauth = hc->request_body; - - { - enum GNUNET_GenericReturnValue ret; - - ret = TMH_check_auth_config (connection, - jauth, - &auth_pw); - if (GNUNET_OK != ret) - return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; - } - - if ( (0 != (tcs & TEH_TCS_SMS) && - ( (NULL == mi->settings.phone) || - (NULL == TMH_helper_sms) || - (! mi->settings.phone_validated) ) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot reset password: SMS factor not available\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_MFA_MISSING, - "phone_number"); - } - if ( (0 != (tcs & TEH_TCS_EMAIL) && - ( (NULL == mi->settings.email) || - (NULL == TMH_helper_email) || - (! mi->settings.email_validated) ) ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot reset password: E-mail factor not available\n"); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_MFA_MISSING, - "email"); - } - if (! auth_override) - { - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized - - switch (tcs) - { - case TEH_TCS_NONE: - ret = GNUNET_OK; - break; - case TEH_TCS_SMS: - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, - true, - TALER_MERCHANT_MFA_CHANNEL_SMS, - mi->settings.phone, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - case TEH_TCS_EMAIL: - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, - true, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - mi->settings.email, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - case TEH_TCS_EMAIL_AND_SMS: - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, - true, - TALER_MERCHANT_MFA_CHANNEL_SMS, - mi->settings.phone, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - mi->settings.email, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - } - if (GNUNET_OK != ret) - { - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - } - - if (NULL == auth_pw) - { - memset (&ias.auth_salt, - 0, - sizeof (ias.auth_salt)); - memset (&ias.auth_hash, - 0, - sizeof (ias.auth_hash)); - } - else - { - TMH_compute_auth (auth_pw, - &ias.auth_salt, - &ias.auth_hash); - } - - /* Store the new auth information in the database */ - { - enum GNUNET_DB_QueryStatus qs; - - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "post /instances/$ID/auth")) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - - /* Make the authentication update a serializable operation. - We first check that the authentication information - that the caller's request authenticated with - is still up to date. - Otherwise, we've detected a conflicting update - to the authentication. */ - { - struct TALER_MERCHANTDB_InstanceAuthSettings db_ias; - enum TALER_ErrorCode ec; - - qs = TMH_db->lookup_instance_auth (TMH_db->cls, - mi->settings.id, - &db_ias); - - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Instance got purged. */ - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - goto retry; - case GNUNET_DB_STATUS_HARD_ERROR: - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* Success! */ - break; - } - - if (! auth_override) - { - // FIXME are we sure what the scope here is? - ec = TMH_check_token (hc->auth_token, - mi->settings.id, - &hc->auth_scope); - if (TALER_EC_NONE != ec) - { - TMH_db->rollback (TMH_db->cls); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Refusing auth change: `%s'\n", - TALER_ErrorCode_get_hint (ec)); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, - NULL); - } - } - } - - qs = TMH_db->update_instance_auth (TMH_db->cls, - mi->settings.id, - &ias); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - } - goto retry; - } - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -retry: - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; /* success! -- or hard failure */ - } /* for .. MAX_RETRIES */ - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - } - /* Finally, also update our running process */ - mi->auth = ias; - } - TMH_reload_instances (mi->settings.id); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -MHD_RESULT -TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - - return post_instances_ID_auth (mi, - connection, - hc, - false, - TEH_TCS_NONE); -} - - -MHD_RESULT -TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - - return post_instances_ID_auth (mi, - connection, - hc, - false, - TEH_mandatory_tan_channels); -} - - -MHD_RESULT -TMH_private_post_instances_default_ID_auth ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi; - MHD_RESULT ret; - - if ( (NULL == hc->infix) || - (0 == strcmp ("admin", - hc->infix)) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_MFA_MISSING, - "not allowed for 'admin' account"); - } - mi = TMH_lookup_instance (hc->infix); - if (NULL == mi) - { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); - } - ret = post_instances_ID_auth (mi, - connection, - hc, - true, - TEH_TCS_NONE); - return ret; -} - - -/* end of taler-merchant-httpd_private-post-instances-ID-auth.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.h b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.h @@ -1,80 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-post-instances-ID-auth.h - * @brief implements POST /instances/$ID/auth request handling - * @author Christian Grothoff - * @author Florian Dold - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_AUTH_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_AUTH_H -#include "taler-merchant-httpd.h" - - -/** - * Change the instance's auth settings. - * This is the handler called using the instance's own authentication. - * - * @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_instances_ID_auth ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Change the instance's auth settings. - * This is the handler called using the default instance's authentication. - * - * @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_instances_default_ID_auth ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Change the instance's auth settings. - * This is the public handler used to reset a password if - * the original password was forgotten. Always requires - * 2-FA to be configured for the account with two additional - * factors. - * - * @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_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c @@ -1,192 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2023, 2025 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-post-instances-ID-token.c - * @brief implementing POST /instances/$ID/token request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-instances-ID-token.h" -#include "taler-merchant-httpd_auth.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_mfa.h" -#include <taler/taler_json_lib.h> - - -/** - * Default duration for the validity of a login token. - */ -#define DEFAULT_DURATION GNUNET_TIME_UNIT_DAYS - - -MHD_RESULT -TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - json_t *jtoken = hc->request_body; - const char *scope; - const char *description; - enum TMH_AuthScope iscope = TMH_AS_NONE; - bool refreshable = false; - struct TALER_MERCHANTDB_LoginTokenP btoken; - struct GNUNET_TIME_Relative duration - = DEFAULT_DURATION; - struct GNUNET_TIME_Timestamp expiration_time; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("scope", - &scope), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("duration", - &duration), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("refreshable", - &refreshable), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("description", - &description), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_DB_QueryStatus qs; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - jtoken, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) ? MHD_YES : MHD_NO; - } - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &btoken, - sizeof (btoken)); - expiration_time = GNUNET_TIME_relative_to_timestamp (duration); - { - char *tmp_scope; - char *scope_prefix; - char *scope_suffix; - - tmp_scope = GNUNET_strdup (scope); - scope_prefix = strtok (tmp_scope, - ":"); - scope_suffix = strtok (NULL, - ":"); - /* We allow <SCOPE>:REFRESHABLE syntax */ - if ( (NULL != scope_suffix) && - (0 == strcasecmp (scope_suffix, - "refreshable"))) - refreshable = true; - iscope = TMH_get_scope_by_name (scope_prefix); - if (TMH_AS_NONE == iscope) - { - GNUNET_break_op (0); - GNUNET_free (tmp_scope); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "scope"); - } - GNUNET_free (tmp_scope); - } - if (refreshable) - iscope |= TMH_AS_REFRESHABLE; - if (! TMH_scope_is_subset (hc->auth_scope, - iscope)) - { - /* more permissions requested for the new token, not allowed */ - GNUNET_break_op (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT, - NULL); - } - if (NULL == description) - { - description = ""; - } - - { - enum GNUNET_GenericReturnValue ret = - TMH_mfa_check_simple (hc, - TALER_MERCHANT_MFA_CO_AUTH_TOKEN_CREATION, - mi); - - if (GNUNET_OK != ret) - { - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - } - - qs = TMH_db->insert_login_token (TMH_db->cls, - mi->settings.id, - &btoken, - GNUNET_TIME_timestamp_get (), - expiration_time, - iscope, - description); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_login_token"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - { - char *tok; - MHD_RESULT ret; - char *val; - - val = GNUNET_STRINGS_data_to_string_alloc (&btoken, - sizeof (btoken)); - GNUNET_asprintf (&tok, - RFC_8959_PREFIX "%s", - val); - GNUNET_free (val); - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("access_token", - tok), - GNUNET_JSON_pack_string ("token", - tok), - GNUNET_JSON_pack_string ("scope", - scope), - GNUNET_JSON_pack_bool ("refreshable", - refreshable), - GNUNET_JSON_pack_timestamp ("expiration", - expiration_time)); - GNUNET_free (tok); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-post-instances-ID-token.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-token.h b/src/backend/taler-merchant-httpd_private-post-instances-ID-token.h @@ -1,45 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2023 Taler Systems SA - - GNU 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. - - GNU 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-httpd_private-post-instances-ID-token.h - * @brief implements POST /instances/$ID/token request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_TOKEN_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_ID_TOKEN_H - -#include "taler-merchant-httpd.h" - - -/** - * Obtain a login token for an instance. - * - * @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_instances_ID_token (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-instances.c b/src/backend/taler-merchant-httpd_private-post-instances.c @@ -1,694 +0,0 @@ -/* - This file is part of TALER - (C) 2020-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-httpd_private-post-instances.c - * @brief implementing POST /instances request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-instances.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_auth.h" -#include "taler-merchant-httpd_mfa.h" -#include "taler/taler_merchant_bank_lib.h" -#include <taler/taler_dbevents.h> -#include <taler/taler_json_lib.h> -#include <regex.h> - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Generate an instance, given its configuration. - * - * @param rh context of the handler - * @param connection the MHD connection to handle - * @param[in,out] hc context with further information about the request - * @param login_token_expiration set to how long a login token validity - * should be, use zero if no login token should be created - * @param validation_needed true if self-provisioned and - * email/phone registration is required before the - * instance can become fully active - * @return MHD result code - */ -static MHD_RESULT -post_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc, - struct GNUNET_TIME_Relative login_token_expiration, - bool validation_needed) -{ - struct TALER_MERCHANTDB_InstanceSettings is = { 0 }; - struct TALER_MERCHANTDB_InstanceAuthSettings ias; - const char *auth_password = NULL; - struct TMH_WireMethod *wm_head = NULL; - struct TMH_WireMethod *wm_tail = NULL; - const json_t *jauth; - const char *iphone = NULL; - bool no_pay_delay; - bool no_refund_delay; - bool no_transfer_delay; - bool no_rounding_interval; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("id", - (const char **) &is.id), - GNUNET_JSON_spec_string ("name", - (const char **) &is.name), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("email", - (const char **) &is.email), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("phone_number", - &iphone), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("website", - (const char **) &is.website), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("logo", - (const char **) &is.logo), - NULL), - GNUNET_JSON_spec_object_const ("auth", - &jauth), - GNUNET_JSON_spec_json ("address", - &is.address), - GNUNET_JSON_spec_json ("jurisdiction", - &is.jurisdiction), - GNUNET_JSON_spec_bool ("use_stefan", - &is.use_stefan), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("default_pay_delay", - &is.default_pay_delay), - &no_pay_delay), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("default_refund_delay", - &is.default_refund_delay), - &no_refund_delay), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay", - &is.default_wire_transfer_delay), - &no_transfer_delay), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_time_rounder_interval ( - "default_wire_transfer_rounding_interval", - &is.default_wire_transfer_rounding_interval), - &no_rounding_interval), - GNUNET_JSON_spec_end () - }; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - if (no_pay_delay) - is.default_pay_delay = TMH_default_pay_delay; - if (no_refund_delay) - is.default_refund_delay = TMH_default_refund_delay; - if (no_transfer_delay) - is.default_wire_transfer_delay = TMH_default_wire_transfer_delay; - if (no_rounding_interval) - is.default_wire_transfer_rounding_interval - = TMH_default_wire_transfer_rounding_interval; - if (GNUNET_TIME_relative_is_forever (is.default_pay_delay)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "default_pay_delay"); - } - if (GNUNET_TIME_relative_is_forever (is.default_refund_delay)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "default_refund_delay"); - } - if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "default_wire_transfer_delay"); - } - if (NULL != iphone) - { - is.phone = TALER_MERCHANT_phone_validate_normalize (iphone, - false); - if (NULL == is.phone) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "phone_number"); - } - if ( (NULL != TMH_phone_regex) && - (0 != - regexec (&TMH_phone_rx, - is.phone, - 0, - NULL, - 0)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "phone_number"); - } - } - if ( (NULL != is.email) && - (! TALER_MERCHANT_email_valid (is.email)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "email"); - } - - { - enum GNUNET_GenericReturnValue ret; - - ret = TMH_check_auth_config (connection, - jauth, - &auth_password); - if (GNUNET_OK != ret) - { - GNUNET_free (is.phone); - GNUNET_JSON_parse_free (spec); - return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; - } - } - - /* check 'id' well-formed */ - { - static bool once; - static regex_t reg; - bool id_wellformed = true; - - if (! once) - { - once = true; - GNUNET_assert (0 == - regcomp (&reg, - "^[A-Za-z0-9][A-Za-z0-9_.@-]+$", - REG_EXTENDED)); - } - - if (0 != regexec (&reg, - is.id, - 0, NULL, 0)) - id_wellformed = false; - if (! id_wellformed) - { - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "id"); - } - } - - if (! TMH_location_object_valid (is.address)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); - } - - if (! TMH_location_object_valid (is.jurisdiction)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "jurisdiction"); - } - - if ( (NULL != is.logo) && - (! TALER_MERCHANT_image_data_url_valid (is.logo)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "logo"); - } - - { - /* Test if an instance of this id is known */ - struct TMH_MerchantInstance *mi; - - mi = TMH_lookup_instance (is.id); - if (NULL != mi) - { - if (mi->deleted) - { - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED, - is.id); - } - /* Check for idempotency */ - if ( (0 == strcmp (mi->settings.id, - is.id)) && - (0 == strcmp (mi->settings.name, - is.name)) && - ((mi->settings.email == is.email) || - (NULL != is.email && NULL != mi->settings.email && - 0 == strcmp (mi->settings.email, - is.email))) && - ((mi->settings.website == is.website) || - (NULL != is.website && NULL != mi->settings.website && - 0 == strcmp (mi->settings.website, - is.website))) && - ((mi->settings.logo == is.logo) || - (NULL != is.logo && NULL != mi->settings.logo && - 0 == strcmp (mi->settings.logo, - is.logo))) && - ( ( (NULL != auth_password) && - (GNUNET_OK == - TMH_check_auth (auth_password, - &mi->auth.auth_salt, - &mi->auth.auth_hash)) ) || - ( (NULL == auth_password) && - (GNUNET_YES == - GNUNET_is_zero (&mi->auth.auth_hash))) ) && - (1 == json_equal (mi->settings.address, - is.address)) && - (1 == json_equal (mi->settings.jurisdiction, - is.jurisdiction)) && - (mi->settings.use_stefan == is.use_stefan) && - (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay, - ==, - is.default_wire_transfer_delay)) && - (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay, - ==, - is.default_pay_delay)) && - (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay, - ==, - is.default_refund_delay)) ) - { - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, - is.id); - } - } - - /* Check MFA is satisfied */ - if (validation_needed) - { - enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; - - if ( (0 != (TEH_TCS_SMS & TEH_mandatory_tan_channels)) && - (NULL == is.phone) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); /* does nothing... */ - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "phone_number"); - - } - if ( (0 != (TEH_TCS_EMAIL & TEH_mandatory_tan_channels)) && - (NULL == is.email) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "email"); - } - switch (TEH_mandatory_tan_channels) - { - case TEH_TCS_NONE: - GNUNET_assert (0); - ret = GNUNET_OK; - break; - case TEH_TCS_SMS: - is.phone_validated = true; - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, - true, - TALER_MERCHANT_MFA_CHANNEL_SMS, - is.phone, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - case TEH_TCS_EMAIL: - is.email_validated = true; - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, - true, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - is.email, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - case TEH_TCS_EMAIL_AND_SMS: - is.phone_validated = true; - is.email_validated = true; - ret = TMH_mfa_challenges_do (hc, - TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, - true, - TALER_MERCHANT_MFA_CHANNEL_SMS, - is.phone, - TALER_MERCHANT_MFA_CHANNEL_EMAIL, - is.email, - TALER_MERCHANT_MFA_CHANNEL_NONE); - break; - } - if (GNUNET_OK != ret) - { - GNUNET_JSON_parse_free (spec); - GNUNET_free (is.phone); - return (GNUNET_NO == ret) - ? MHD_YES - : MHD_NO; - } - } - - /* handle authentication token setup */ - if (NULL == auth_password) - { - memset (&ias.auth_salt, - 0, - sizeof (ias.auth_salt)); - memset (&ias.auth_hash, - 0, - sizeof (ias.auth_hash)); - } - else - { - /* Sets 'auth_salt' and 'auth_hash' */ - TMH_compute_auth (auth_password, - &ias.auth_salt, - &ias.auth_hash); - } - - /* create in-memory data structure */ - { - struct TMH_MerchantInstance *mi; - enum GNUNET_DB_QueryStatus qs; - - mi = GNUNET_new (struct TMH_MerchantInstance); - mi->wm_head = wm_head; - mi->wm_tail = wm_tail; - mi->settings = is; - mi->settings.address = json_incref (mi->settings.address); - mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction); - mi->settings.id = GNUNET_strdup (is.id); - mi->settings.name = GNUNET_strdup (is.name); - if (NULL != is.email) - mi->settings.email = GNUNET_strdup (is.email); - mi->settings.phone = is.phone; - is.phone = NULL; - if (NULL != is.website) - mi->settings.website = GNUNET_strdup (is.website); - if (NULL != is.logo) - mi->settings.logo = GNUNET_strdup (is.logo); - mi->auth = ias; - mi->validation_needed = validation_needed; - GNUNET_CRYPTO_eddsa_key_create (&mi->merchant_priv.eddsa_priv); - GNUNET_CRYPTO_eddsa_key_get_public (&mi->merchant_priv.eddsa_priv, - &mi->merchant_pub.eddsa_pub); - - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "post /instances")) - { - mi->rc = 1; - TMH_instance_decref (mi); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->insert_instance (TMH_db->cls, - &mi->merchant_pub, - &mi->merchant_priv, - &mi->settings, - &mi->auth, - validation_needed); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - { - MHD_RESULT ret; - - TMH_db->rollback (TMH_db->cls); - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - is.id); - mi->rc = 1; - TMH_instance_decref (mi); - GNUNET_JSON_parse_free (spec); - return ret; - } - case GNUNET_DB_STATUS_SOFT_ERROR: - goto retry; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - { - MHD_RESULT ret; - - TMH_db->rollback (TMH_db->cls); - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS, - is.id); - mi->rc = 1; - TMH_instance_decref (mi); - GNUNET_JSON_parse_free (spec); - return ret; - } - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* handled below */ - break; - } - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; -retry: - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; /* success! -- or hard failure */ - } /* for .. MAX_RETRIES */ - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - mi->rc = 1; - TMH_instance_decref (mi); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - } - /* Finally, also update our running process */ - GNUNET_assert (GNUNET_OK == - TMH_add_instance (mi)); - TMH_reload_instances (mi->settings.id); - } - GNUNET_JSON_parse_free (spec); - if (GNUNET_TIME_relative_is_zero (login_token_expiration)) - { - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - - { - struct TALER_MERCHANTDB_LoginTokenP btoken; - enum TMH_AuthScope iscope = TMH_AS_REFRESHABLE | TMH_AS_SPA; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp expiration_time; - bool refreshable = true; - - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &btoken, - sizeof (btoken)); - expiration_time - = GNUNET_TIME_relative_to_timestamp (login_token_expiration); - qs = TMH_db->insert_login_token (TMH_db->cls, - is.id, - &btoken, - GNUNET_TIME_timestamp_get (), - expiration_time, - iscope, - "login token from instance creation"); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - return TALER_MHD_reply_with_ec (connection, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_login_token"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - { - char *tok; - MHD_RESULT ret; - char *val; - - val = GNUNET_STRINGS_data_to_string_alloc (&btoken, - sizeof (btoken)); - GNUNET_asprintf (&tok, - RFC_8959_PREFIX "%s", - val); - GNUNET_free (val); - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("access_token", - tok), - GNUNET_JSON_pack_string ("token", - tok), - GNUNET_JSON_pack_string ("scope", - TMH_get_name_by_scope (iscope, - &refreshable)), - GNUNET_JSON_pack_bool ("refreshable", - refreshable), - GNUNET_JSON_pack_timestamp ("expiration", - expiration_time)); - GNUNET_free (tok); - return ret; - } - } -} - - -/** - * Generate an instance, given its configuration. - * - * @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_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - return post_instances (rh, - connection, - hc, - GNUNET_TIME_UNIT_ZERO, - false); -} - - -/** - * Generate an instance, given its configuration. - * Public handler to be used when self-provisioning. - * - * @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_public_post_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct GNUNET_TIME_Relative expiration; - - TALER_MHD_parse_request_rel_time (connection, - "token_validity_ms", - &expiration); - if (GNUNET_YES != - TMH_have_self_provisioning) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, - "Self-provisioning is not enabled"); - } - - return post_instances (rh, - connection, - hc, - expiration, - TEH_TCS_NONE != - TEH_mandatory_tan_channels); -} - - -/* end of taler-merchant-httpd_private-post-instances.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-instances.h b/src/backend/taler-merchant-httpd_private-post-instances.h @@ -1,59 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-post-instances.h - * @brief implementing POST /instances request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_INSTANCES_H -#include "taler-merchant-httpd.h" - - -/** - * Generate an instance, given its configuration. - * - * @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_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -/** - * Generate an instance, given its configuration. - * Public handler to be used when self-provisioning. - * - * @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_public_post_instances (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c @@ -1,466 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2024 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-httpd_private-post-orders-ID-refund.c - * @brief Handle request to increase the refund for an order - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include "taler-merchant-httpd_private-post-orders-ID-refund.h" -#include "taler-merchant-httpd_private-get-orders.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_exchanges.h" - - -/** - * How often do we retry the non-trivial refund INSERT database - * transaction? - */ -#define MAX_RETRIES 5 - - -/** - * Use database to notify other clients about the - * @a order_id being refunded - * - * @param hc handler context we operate in - * @param amount the (total) refunded amount - */ -static void -trigger_refund_notification ( - struct TMH_HandlerContext *hc, - const struct TALER_Amount *amount) -{ - { - const char *as; - struct TMH_OrderRefundEventP refund_eh = { - .header.size = htons (sizeof (refund_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND), - .merchant_pub = hc->instance->merchant_pub - }; - - /* Resume clients that may wait for this refund */ - as = TALER_amount2s (amount); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Awakening clients on %s waiting for refund of no more than %s\n", - hc->infix, - as); - GNUNET_CRYPTO_hash (hc->infix, - strlen (hc->infix), - &refund_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &refund_eh.header, - as, - strlen (as)); - } - { - struct TMH_OrderPayEventP pay_eh = { - .header.size = htons (sizeof (pay_eh)), - .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED), - .merchant_pub = hc->instance->merchant_pub - }; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Notifying clients about status change of order %s\n", - hc->infix); - GNUNET_CRYPTO_hash (hc->infix, - strlen (hc->infix), - &pay_eh.h_order_id); - TMH_db->event_notify (TMH_db->cls, - &pay_eh.header, - NULL, - 0); - } -} - - -/** - * Make a taler://refund URI - * - * @param connection MHD connection to take host and path from - * @param instance_id merchant's instance ID, must not be NULL - * @param order_id order ID to show a refund for, must not be NULL - * @returns the URI, must be freed with #GNUNET_free - */ -static char * -make_taler_refund_uri (struct MHD_Connection *connection, - const char *instance_id, - const char *order_id) -{ - struct GNUNET_Buffer buf; - - GNUNET_assert (NULL != instance_id); - GNUNET_assert (NULL != order_id); - if (GNUNET_OK != - TMH_taler_uri_by_connection (connection, - "refund", - instance_id, - &buf)) - { - GNUNET_break (0); - return NULL; - } - GNUNET_buffer_write_path (&buf, - order_id); - GNUNET_buffer_write_path (&buf, - ""); /* Trailing slash */ - return GNUNET_buffer_reap_str (&buf); -} - - -/** - * Wrapper around #TMH_EXCHANGES_get_limit() that - * determines the refund limit for a given @a exchange_url - * - * @param cls unused - * @param exchange_url base URL of the exchange to get - * the refund limit for - * @param[in,out] amount lowered to the maximum refund - * allowed at the exchange - */ -static void -get_refund_limit (void *cls, - const char *exchange_url, - struct TALER_Amount *amount) -{ - (void) cls; - TMH_EXCHANGES_get_limit (exchange_url, - TALER_KYCLOGIC_KYC_TRIGGER_REFUND, - amount); -} - - -/** - * Handle request for increasing the refund associated with - * a contract. - * - * @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_orders_ID_refund ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TALER_Amount refund; - const char *reason; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("refund", - &refund), - GNUNET_JSON_spec_string ("reason", - &reason), - GNUNET_JSON_spec_end () - }; - enum TALER_MERCHANTDB_RefundStatus rs; - struct TALER_PrivateContractHashP h_contract; - json_t *contract_terms; - struct GNUNET_TIME_Timestamp timestamp; - - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - { - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - } - - { - enum GNUNET_DB_QueryStatus qs; - uint64_t order_serial; - struct GNUNET_TIME_Timestamp refund_deadline; - struct GNUNET_TIME_Timestamp wire_deadline; - - qs = TMH_db->lookup_contract_terms (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &contract_terms, - &order_serial, - NULL); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_contract_terms"); - } - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, - hc->infix); - } - if (GNUNET_OK != - TALER_JSON_contract_hash (contract_terms, - &h_contract)) - { - GNUNET_break (0); - json_decref (contract_terms); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "Could not hash contract terms"); - } - { - struct GNUNET_JSON_Specification cspec[] = { - GNUNET_JSON_spec_timestamp ("refund_deadline", - &refund_deadline), - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &wire_deadline), - GNUNET_JSON_spec_timestamp ("timestamp", - &timestamp), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_YES != - GNUNET_JSON_parse (contract_terms, - cspec, - NULL, NULL)) - { - GNUNET_break (0); - json_decref (contract_terms); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - "mandatory fields missing"); - } - if (GNUNET_TIME_timestamp_cmp (timestamp, - ==, - refund_deadline)) - { - /* refund was never allowed, so we should refuse hard */ - json_decref (contract_terms); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT, - NULL); - } - if (GNUNET_TIME_absolute_is_past (refund_deadline.abs_time)) - { - /* it is too late for refunds */ - /* NOTE: We MAY still be lucky that the exchange did not yet - wire the funds, so we will try to give the refund anyway */ - } - if (GNUNET_TIME_absolute_is_past (wire_deadline.abs_time)) - { - /* it is *really* too late for refunds */ - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_GONE, - TALER_EC_MERCHANT_PRIVATE_POST_REFUND_AFTER_WIRE_DEADLINE, - NULL); - } - } - } - - TMH_db->preflight (TMH_db->cls); - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "increase refund")) - { - GNUNET_break (0); - json_decref (contract_terms); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - rs = TMH_db->increase_refund (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &refund, - &get_refund_limit, - NULL, - reason); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "increase refund returned %d\n", - rs); - if (TALER_MERCHANTDB_RS_SUCCESS != rs) - TMH_db->rollback (TMH_db->cls); - if (TALER_MERCHANTDB_RS_SOFT_ERROR == rs) - continue; - if (TALER_MERCHANTDB_RS_SUCCESS == rs) - { - enum GNUNET_DB_QueryStatus qs; - json_t *rargs; - - rargs = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("timestamp", - timestamp), - GNUNET_JSON_pack_string ("order_id", - hc->infix), - GNUNET_JSON_pack_object_incref ("contract_terms", - contract_terms), - TALER_JSON_pack_amount ("refund_amount", - &refund), - GNUNET_JSON_pack_string ("reason", - reason) - ); - GNUNET_assert (NULL != rargs); - qs = TMH_trigger_webhook ( - hc->instance->settings.id, - "refund", - rargs); - json_decref (rargs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - rs = TALER_MERCHANTDB_RS_HARD_ERROR; - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - continue; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - qs = TMH_db->commit (TMH_db->cls); - break; - } - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - GNUNET_break (0); - rs = TALER_MERCHANTDB_RS_HARD_ERROR; - break; - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - continue; - trigger_refund_notification (hc, - &refund); - } - break; - } /* retries loop */ - json_decref (contract_terms); - - switch (rs) - { - case TALER_MERCHANTDB_RS_LEGAL_FAILURE: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Refund amount %s exceeded legal limits of the exchanges involved\n", - TALER_amount2s (&refund)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - TALER_EC_MERCHANT_POST_ORDERS_ID_REFUND_EXCHANGE_TRANSACTION_LIMIT_VIOLATION, - NULL); - case TALER_MERCHANTDB_RS_BAD_CURRENCY: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Refund amount %s is not in the currency of the original payment\n", - TALER_amount2s (&refund)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - "Order was paid in a different currency"); - case TALER_MERCHANTDB_RS_TOO_HIGH: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Refusing refund amount %s that is larger than original payment\n", - TALER_amount2s (&refund)); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT, - "Amount above payment"); - case TALER_MERCHANTDB_RS_SOFT_ERROR: - case TALER_MERCHANTDB_RS_HARD_ERROR: - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - case TALER_MERCHANTDB_RS_NO_SUCH_ORDER: - /* We know the order exists from the - "lookup_contract_terms" at the beginning; - so if we get 'no such order' here, it - must be read as "no PAID order" */ - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID, - hc->infix); - case TALER_MERCHANTDB_RS_SUCCESS: - /* continued below */ - break; - } /* end switch */ - - { - uint64_t order_serial; - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->lookup_order_summary (TMH_db->cls, - hc->instance->settings.id, - hc->infix, - &timestamp, - &order_serial); - if (0 >= qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL); - } - TMH_notify_order_change (hc->instance, - TMH_OSF_CLAIMED - | TMH_OSF_PAID - | TMH_OSF_REFUNDED, - timestamp, - order_serial); - } - { - MHD_RESULT ret; - char *taler_refund_uri; - - taler_refund_uri = make_taler_refund_uri (connection, - hc->instance->settings.id, - hc->infix); - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("taler_refund_uri", - taler_refund_uri), - GNUNET_JSON_pack_data_auto ("h_contract", - &h_contract)); - GNUNET_free (taler_refund_uri); - return ret; - } -} - - -/* end of taler-merchant-httpd_private-post-orders-ID-refund.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.h b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2016, 2017, 2020 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-httpd_private-post-orders-ID-refund.h - * @brief Handle request to increase the refund for an order - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_ID_REFUND_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_ID_REFUND_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Handle request for increasing the refund associated with - * a contract. - * - * @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_orders_ID_refund (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -1,4899 +0,0 @@ -/* - This file is part of TALER - (C) 2014-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-httpd_private-post-orders.c - * @brief the POST /orders handler - * @author Christian Grothoff - * @author Marcello Stanisci - * @author Christian Blättler - */ -#include "taler/platform.h" -#include <gnunet/gnunet_common.h> -#include <gnunet/gnunet_db_lib.h> -#include <gnunet/gnunet_json_lib.h> -#include <gnunet/gnunet_time_lib.h> -#include <jansson.h> -#include <microhttpd.h> -#include <string.h> -#include <taler/taler_error_codes.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_util.h> -#include <taler/taler_merchant_util.h> -#include <time.h> -#include "taler-merchant-httpd.h" -#include "taler-merchant-httpd_private-post-orders.h" -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_contract.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_private-get-orders.h" - -#include "taler/taler_merchantdb_plugin.h" - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - -/** - * Maximum number of inventory products per order. - */ -#define MAX_PRODUCTS 1024 - -/** - * What is the label under which we find/place the merchant's - * jurisdiction in the locations list by default? - */ -#define STANDARD_LABEL_MERCHANT_JURISDICTION "_mj" - -/** - * What is the label under which we find/place the merchant's - * address in the locations list by default? - */ -#define STANDARD_LABEL_MERCHANT_ADDRESS "_ma" - -/** - * How long do we wait at most for /keys from the exchange(s)? - * Ensures that we do not block forever just because some exchange - * fails to respond *or* because our taler-merchant-keyscheck - * refuses a forced download. - */ -#define MAX_KEYS_WAIT \ - GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) - -/** - * Generate the base URL for the given merchant instance. - * - * @param connection the MHD connection - * @param instance_id the merchant instance ID - * @returns the merchant instance's base URL - */ -static char * -make_merchant_base_url (struct MHD_Connection *connection, - const char *instance_id) -{ - struct GNUNET_Buffer buf; - - if (GNUNET_OK != - TMH_base_url_by_connection (connection, - instance_id, - &buf)) - return NULL; - GNUNET_buffer_write_path (&buf, - ""); - return GNUNET_buffer_reap_str (&buf); -} - - -/** - * Information about a product we are supposed to add to the order - * based on what we know it from our inventory. - */ -struct InventoryProduct -{ - /** - * Identifier of the product in the inventory. - */ - const char *product_id; - - /** - * Number of units of the product to add to the order (integer part). - */ - uint64_t quantity; - - /** - * Fractional part of the quantity in units of 1/1000000 of the base value. - */ - uint32_t quantity_frac; - - /** - * True if the integer quantity field was missing in the request. - */ - bool quantity_missing; - - /** - * String representation of the quantity, if supplied. - */ - const char *unit_quantity; - - /** - * True if the string quantity field was missing in the request. - */ - bool unit_quantity_missing; - - /** - * Money pot associated with the product. 0 for none. - */ - uint64_t product_money_pot; - -}; - - -/** - * Handle for a rekey operation where we (re)request - * the /keys from the exchange. - */ -struct RekeyExchange -{ - /** - * Kept in a DLL. - */ - struct RekeyExchange *prev; - - /** - * Kept in a DLL. - */ - struct RekeyExchange *next; - - /** - * order this is for. - */ - struct OrderContext *oc; - - /** - * Base URL of the exchange. - */ - char *url; - - /** - * Request for keys. - */ - struct TMH_EXCHANGES_KeysOperation *fo; - -}; - - -/** - * Data structure where we evaluate the viability of a given - * wire method for this order. - */ -struct WireMethodCandidate -{ - /** - * Kept in a DLL. - */ - struct WireMethodCandidate *next; - - /** - * Kept in a DLL. - */ - struct WireMethodCandidate *prev; - - /** - * The wire method we are evaluating. - */ - const struct TMH_WireMethod *wm; - - /** - * List of exchanges to use when we use this wire method. - */ - json_t *exchanges; - - /** - * Array of maximum amounts that could be paid over all available exchanges - * for this @a wm. Used to determine if this order creation requests exceeds - * legal limits. - */ - struct TALER_Amount *total_exchange_limits; - - /** - * Length of the @e total_exchange_limits array. - */ - unsigned int num_total_exchange_limits; - -}; - - -/** - * Information we keep per order we are processing. - */ -struct OrderContext -{ - /** - * Information set in the #ORDER_PHASE_PARSE_REQUEST phase. - */ - struct - { - /** - * Order field of the request - */ - json_t *order; - - /** - * Set to how long refunds will be allowed. - */ - struct GNUNET_TIME_Relative refund_delay; - - /** - * RFC8905 payment target type to find a matching merchant account - */ - const char *payment_target; - - /** - * Shared key to use with @e pos_algorithm. - */ - char *pos_key; - - /** - * Selected algorithm (by template) when we are to - * generate an OTP code for payment confirmation. - */ - enum TALER_MerchantConfirmationAlgorithm pos_algorithm; - - /** - * Hash of the POST request data, used to detect - * idempotent requests. - */ - struct TALER_MerchantPostDataHashP h_post_data; - - /** - * Length of the @e inventory_products array. - */ - unsigned int inventory_products_length; - - /** - * Specifies that some products are to be included in the - * order from the inventory. For these inventory management - * is performed (so the products must be in stock). - */ - struct InventoryProduct *inventory_products; - - /** - * Length of the @e uuids array. - */ - unsigned int uuids_length; - - /** - * array of UUIDs used to reserve products from @a inventory_products. - */ - struct GNUNET_Uuid *uuids; - - /** - * Claim token for the request. - */ - struct TALER_ClaimTokenP claim_token; - - /** - * Session ID (optional) to use for the order. - */ - const char *session_id; - - } parse_request; - - /** - * Information set in the #ORDER_PHASE_PARSE_ORDER phase. - */ - struct - { - - /** - * Our order ID. - */ - char *order_id; - - /** - * Summary of the contract. - */ - const char *summary; - - /** - * Internationalized summary. - */ - const json_t *summary_i18n; - - /** - * URL that will show that the contract was successful - * after it has been paid for. - */ - const char *fulfillment_url; - - /** - * Message shown to the customer after paying for the contract. - * Either fulfillment_url or fulfillment_message must be specified. - */ - const char *fulfillment_message; - - /** - * Map from IETF BCP 47 language tags to localized fulfillment messages. - */ - const json_t *fulfillment_message_i18n; - - /** - * Length of the @e products array. - */ - size_t products_len; - - /** - * Array of products that are being sold. - */ - struct TALER_MERCHANT_ProductSold *products; - - /** - * URL where the same contract could be ordered again (if available). - */ - const char *public_reorder_url; - - /** - * Merchant base URL. - */ - char *merchant_base_url; - - /** - * Timestamp of the order. - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * Deadline for refunds. - */ - struct GNUNET_TIME_Timestamp refund_deadline; - - /** - * Payment deadline. - */ - struct GNUNET_TIME_Timestamp pay_deadline; - - /** - * Wire transfer deadline. - */ - struct GNUNET_TIME_Timestamp wire_deadline; - - /** - * Wire transfer round-up interval to apply. - */ - enum GNUNET_TIME_RounderInterval wire_deadline_rounder; - - /** - * Delivery date. - */ - struct GNUNET_TIME_Timestamp delivery_date; - - /** - * Delivery location. - */ - const json_t *delivery_location; - - /** - * Specifies for how long the wallet should try to get an - * automatic refund for the purchase. - */ - struct GNUNET_TIME_Relative auto_refund; - - /** - * Nonce generated by the wallet and echoed by the merchant - * in this field when the proposal is generated. - */ - const char *nonce; - - /** - * Extra data that is only interpreted by the merchant frontend. - */ - const json_t *extra; - - /** - * Minimum age required by the order. - */ - uint32_t minimum_age; - - /** - * Money pot to increment for whatever order payment amount - * is not yet assigned to a pot via the Product. - */ - uint64_t order_default_money_pot; - - /** - * Version of the contract terms. - */ - enum TALER_MERCHANT_ContractVersion version; - - /** - * Details present depending on @e version. - */ - union - { - /** - * Details only present for v0. - */ - struct - { - /** - * Gross amount value of the contract. Used to - * compute @e max_stefan_fee. - */ - struct TALER_Amount brutto; - - /** - * Tip included by the customer (part of the total amount). - */ - struct TALER_Amount tip; - - /** - * True if @e tip was not provided. - */ - bool no_tip; - - /** - * Maximum fee as given by the client request. - */ - struct TALER_Amount max_fee; - } v0; - - /** - * Details only present for v1. - */ - struct - { - /** - * Array of contract choices. Is null for v0 contracts. - */ - const json_t *choices; - } v1; - } details; - - } parse_order; - - /** - * Information set in the #ORDER_PHASE_PARSE_CHOICES phase. - */ - struct - { - /** - * Array of possible specific contracts the wallet/customer may choose - * from by selecting the respective index when signing the deposit - * confirmation. - */ - struct TALER_MERCHANT_ContractChoice *choices; - - /** - * Length of the @e choices array. - */ - unsigned int choices_len; - - /** - * Array of token families referenced in the contract. - */ - struct TALER_MERCHANT_ContractTokenFamily *token_families; - - /** - * Length of the @e token_families array. - */ - unsigned int token_families_len; - } parse_choices; - - /** - * Information set in the #ORDER_PHASE_MERGE_INVENTORY phase. - */ - struct - { - /** - * Merged array of products in the @e order. - */ - json_t *products; - } merge_inventory; - - /** - * Information set in the #ORDER_PHASE_ADD_PAYMENT_DETAILS phase. - */ - struct - { - - /** - * DLL of wire methods under evaluation. - */ - struct WireMethodCandidate *wmc_head; - - /** - * DLL of wire methods under evaluation. - */ - struct WireMethodCandidate *wmc_tail; - - /** - * Array of maximum amounts that appear in the contract choices - * per currency. - * Determines the maximum amounts that a client could pay for this - * order and which we must thus make sure is acceptable for the - * selected wire method/account if possible. - */ - struct TALER_Amount *max_choice_limits; - - /** - * Length of the @e max_choice_limits array. - */ - unsigned int num_max_choice_limits; - - /** - * Set to true if we may need an exchange. True if any amount is non-zero. - */ - bool need_exchange; - - } add_payment_details; - - /** - * Information set in the #ORDER_PHASE_SELECT_WIRE_METHOD phase. - */ - struct - { - - /** - * Array of exchanges we find acceptable for this order and wire method. - */ - json_t *exchanges; - - /** - * Wire method (and our bank account) we have selected - * to be included for this order. - */ - const struct TMH_WireMethod *wm; - - } select_wire_method; - - /** - * Information set in the #ORDER_PHASE_SET_EXCHANGES phase. - */ - struct - { - - /** - * Forced requests to /keys to update our exchange - * information. - */ - struct RekeyExchange *pending_reload_head; - - /** - * Forced requests to /keys to update our exchange - * information. - */ - struct RekeyExchange *pending_reload_tail; - - /** - * How long do we wait at most until giving up on getting keys? - */ - struct GNUNET_TIME_Absolute keys_timeout; - - /** - * Task to wake us up on @e keys_timeout. - */ - struct GNUNET_SCHEDULER_Task *wakeup_task; - - /** - * Array of reasons why a particular exchange may be - * limited or not be eligible. - */ - json_t *exchange_rejections; - - /** - * Did we previously force reloading of /keys from - * all exchanges? Set to 'true' to prevent us from - * doing it again (and again...). - */ - bool forced_reload; - - /** - * Did we find a working exchange? - */ - bool exchange_ok; - - /** - * Did we find an exchange that justifies - * reloading keys? - */ - bool promising_exchange; - - /** - * Set to true once we have attempted to load exchanges - * for the first time. - */ - bool exchanges_tried; - - /** - * Details depending on the contract version. - */ - union - { - - /** - * Details for contract v0. - */ - struct - { - /** - * Maximum fee for @e order based on STEFAN curves. - * Used to set @e max_fee if not provided as part of - * @e order. - */ - struct TALER_Amount max_stefan_fee; - - } v0; - - /** - * Details for contract v1. - */ - struct - { - /** - * Maximum fee for @e order based on STEFAN curves by - * contract choice. - * Used to set @e max_fee if not provided as part of - * @e order. - */ - struct TALER_Amount *max_stefan_fees; - - } v1; - - } details; - - } set_exchanges; - - /** - * Information set in the #ORDER_PHASE_SET_MAX_FEE phase. - */ - struct - { - - /** - * Details depending on the contract version. - */ - union - { - - /** - * Details for contract v0. - */ - struct - { - /** - * Maximum fee - */ - struct TALER_Amount max_fee; - } v0; - - /** - * Details for contract v1. - */ - struct - { - /** - * Maximum fees by contract choice. - */ - struct TALER_Amount *max_fees; - - } v1; - - } details; - } set_max_fee; - - /** - * Information set in the #ORDER_PHASE_EXECUTE_ORDER phase. - */ - struct - { - /** - * Which product (by offset) is out of stock, UINT_MAX if all were in-stock. - */ - unsigned int out_of_stock_index; - - /** - * Set to a previous claim token *if* @e idempotent - * is also true. - */ - struct TALER_ClaimTokenP token; - - /** - * Set to true if the order was idempotent and there - * was an equivalent one before. - */ - bool idempotent; - - /** - * Set to true if the order is in conflict with a - * previous order with the same order ID. - */ - bool conflict; - } execute_order; - - struct - { - /** - * Contract terms to store in the database. - */ - json_t *contract; - } serialize_order; - - /** - * Connection of the request. - */ - struct MHD_Connection *connection; - - /** - * Kept in a DLL while suspended. - */ - struct OrderContext *next; - - /** - * Kept in a DLL while suspended. - */ - struct OrderContext *prev; - - /** - * Handler context for the request. - */ - struct TMH_HandlerContext *hc; - - /** - * #GNUNET_YES if suspended. - */ - enum GNUNET_GenericReturnValue suspended; - - /** - * Current phase of setting up the order. - */ - enum - { - ORDER_PHASE_PARSE_REQUEST, - ORDER_PHASE_PARSE_ORDER, - ORDER_PHASE_PARSE_CHOICES, - ORDER_PHASE_MERGE_INVENTORY, - ORDER_PHASE_ADD_PAYMENT_DETAILS, - ORDER_PHASE_SET_EXCHANGES, - ORDER_PHASE_SELECT_WIRE_METHOD, - ORDER_PHASE_SET_MAX_FEE, - ORDER_PHASE_SERIALIZE_ORDER, - ORDER_PHASE_SALT_FORGETTABLE, - ORDER_PHASE_CHECK_CONTRACT, - ORDER_PHASE_EXECUTE_ORDER, - - /** - * Processing is done, we should return #MHD_YES. - */ - ORDER_PHASE_FINISHED_MHD_YES, - - /** - * Processing is done, we should return #MHD_NO. - */ - ORDER_PHASE_FINISHED_MHD_NO - } phase; - - -}; - - -/** - * Kept in a DLL while suspended. - */ -static struct OrderContext *oc_head; - -/** - * Kept in a DLL while suspended. - */ -static struct OrderContext *oc_tail; - - -void -TMH_force_orders_resume () -{ - struct OrderContext *oc; - - while (NULL != (oc = oc_head)) - { - GNUNET_CONTAINER_DLL_remove (oc_head, - oc_tail, - oc); - oc->suspended = GNUNET_SYSERR; - MHD_resume_connection (oc->connection); - } -} - - -/** - * Add the given @a val to the @a array. Adds the - * amount to a given entry in @a array if one with the same - * currency exists, otherwise extends the @a array. - * - * @param[in,out] array pointer to array of amounts - * @param[in,out] array_len length of @a array - * @param val amount to add - * @param cap cap for the sums to enforce, can be NULL - */ -static void -add_to_currency_vector (struct TALER_Amount **array, - unsigned int *array_len, - const struct TALER_Amount *val, - const struct TALER_Amount *cap) -{ - for (unsigned int i = 0; i<*array_len; i++) - { - struct TALER_Amount *ai = &(*array)[i]; - - if (GNUNET_OK == - TALER_amount_cmp_currency (ai, - val)) - { - enum TALER_AmountArithmeticResult aar; - - aar = TALER_amount_add (ai, - ai, - val); - /* If we have a cap, we tolerate the overflow */ - GNUNET_assert ( (aar >= 0) || - ( (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) && - (NULL != cap) ) ); - if (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) - { - *ai = *cap; - } - else if (NULL != cap) - GNUNET_assert (GNUNET_OK == - TALER_amount_min (ai, - ai, - cap)); - return; - } - } - GNUNET_array_append (*array, - *array_len, - *val); - if (NULL != cap) - { - struct TALER_Amount *ai = &(*array)[(*array_len) - 1]; - - GNUNET_assert (GNUNET_OK == - TALER_amount_min (ai, - ai, - cap)); - } -} - - -/** - * Update the phase of @a oc based on @a mret. - * - * @param[in,out] oc order to update phase for - * @param mret #MHD_NO to close with #MHD_NO - * #MHD_YES to close with #MHD_YES - */ -static void -finalize_order (struct OrderContext *oc, - MHD_RESULT mret) -{ - oc->phase = (MHD_YES == mret) - ? ORDER_PHASE_FINISHED_MHD_YES - : ORDER_PHASE_FINISHED_MHD_NO; -} - - -/** - * Update the phase of @a oc based on @a ret. - * - * @param[in,out] oc order to update phase for - * @param ret #GNUNET_SYSERR to close with #MHD_NO - * #GNUNET_NO to close with #MHD_YES - * #GNUNET_OK is not allowed! - */ -static void -finalize_order2 (struct OrderContext *oc, - enum GNUNET_GenericReturnValue ret) -{ - GNUNET_assert (GNUNET_OK != ret); - oc->phase = (GNUNET_NO == ret) - ? ORDER_PHASE_FINISHED_MHD_YES - : ORDER_PHASE_FINISHED_MHD_NO; -} - - -/** - * Generate an error response for @a oc. - * - * @param[in,out] oc order context to respond to - * @param http_status HTTP status code to set - * @param ec error code to set - * @param detail error message detail to set - */ -static void -reply_with_error (struct OrderContext *oc, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *detail) -{ - MHD_RESULT mret; - - mret = TALER_MHD_reply_with_error (oc->connection, - http_status, - ec, - detail); - finalize_order (oc, - mret); -} - - -/** - * Clean up memory used by @a wmc. - * - * @param[in,out] oc order context the WMC is part of - * @param[in] wmc wire method candidate to free - */ -static void -free_wmc (struct OrderContext *oc, - struct WireMethodCandidate *wmc) -{ - GNUNET_CONTAINER_DLL_remove (oc->add_payment_details.wmc_head, - oc->add_payment_details.wmc_tail, - wmc); - GNUNET_array_grow (wmc->total_exchange_limits, - wmc->num_total_exchange_limits, - 0); - json_decref (wmc->exchanges); - GNUNET_free (wmc); -} - - -/** - * Clean up memory used by @a cls. - * - * @param[in] cls the `struct OrderContext` to clean up - */ -static void -clean_order (void *cls) -{ - struct OrderContext *oc = cls; - struct RekeyExchange *rx; - - while (NULL != oc->add_payment_details.wmc_head) - free_wmc (oc, - oc->add_payment_details.wmc_head); - while (NULL != (rx = oc->set_exchanges.pending_reload_head)) - { - GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, - oc->set_exchanges.pending_reload_tail, - rx); - TMH_EXCHANGES_keys4exchange_cancel (rx->fo); - GNUNET_free (rx->url); - GNUNET_free (rx); - } - GNUNET_array_grow (oc->add_payment_details.max_choice_limits, - oc->add_payment_details.num_max_choice_limits, - 0); - if (NULL != oc->set_exchanges.wakeup_task) - { - GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); - oc->set_exchanges.wakeup_task = NULL; - } - if (NULL != oc->select_wire_method.exchanges) - { - json_decref (oc->select_wire_method.exchanges); - oc->select_wire_method.exchanges = NULL; - } - if (NULL != oc->set_exchanges.exchange_rejections) - { - json_decref (oc->set_exchanges.exchange_rejections); - oc->set_exchanges.exchange_rejections = NULL; - } - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - GNUNET_free (oc->set_max_fee.details.v1.max_fees); - GNUNET_free (oc->set_exchanges.details.v1.max_stefan_fees); - break; - } - if (NULL != oc->merge_inventory.products) - { - json_decref (oc->merge_inventory.products); - oc->merge_inventory.products = NULL; - } - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - TALER_MERCHANT_contract_choice_free (&oc->parse_choices.choices[i]); - } - GNUNET_array_grow (oc->parse_choices.choices, - oc->parse_choices.choices_len, - 0); - for (size_t i = 0; i<oc->parse_order.products_len; i++) - { - TALER_MERCHANT_product_sold_free (&oc->parse_order.products[i]); - } - GNUNET_free (oc->parse_order.products); - oc->parse_order.products_len = 0; - for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) - { - struct TALER_MERCHANT_ContractTokenFamily *mctf - = &oc->parse_choices.token_families[i]; - - GNUNET_free (mctf->slug); - GNUNET_free (mctf->name); - GNUNET_free (mctf->description); - json_decref (mctf->description_i18n); - switch (mctf->kind) - { - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: - GNUNET_break (0); - break; - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: - for (size_t j = 0; j<mctf->details.subscription.trusted_domains_len; j++) - GNUNET_free (mctf->details.subscription.trusted_domains[j]); - GNUNET_free (mctf->details.subscription.trusted_domains); - break; - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: - for (size_t j = 0; j<mctf->details.discount.expected_domains_len; j++) - GNUNET_free (mctf->details.discount.expected_domains[j]); - GNUNET_free (mctf->details.discount.expected_domains); - break; - } - for (unsigned int j = 0; j<mctf->keys_len; j++) - { - GNUNET_CRYPTO_blind_sign_pub_decref (mctf->keys[j].pub.public_key); - } - GNUNET_array_grow (mctf->keys, - mctf->keys_len, - 0); - } - GNUNET_array_grow (oc->parse_choices.token_families, - oc->parse_choices.token_families_len, - 0); - GNUNET_array_grow (oc->parse_request.inventory_products, - oc->parse_request.inventory_products_length, - 0); - GNUNET_array_grow (oc->parse_request.uuids, - oc->parse_request.uuids_length, - 0); - GNUNET_free (oc->parse_request.pos_key); - json_decref (oc->parse_request.order); - json_decref (oc->serialize_order.contract); - GNUNET_free (oc->parse_order.order_id); - GNUNET_free (oc->parse_order.merchant_base_url); - GNUNET_free (oc); -} - - -/* ***************** ORDER_PHASE_EXECUTE_ORDER **************** */ - -/** - * Compute remaining stock (integer and fractional parts) for a product. - * - * @param pd product details with current totals/sold/lost - * @param[out] available_value remaining whole units (normalized, non-negative) - * @param[out] available_frac remaining fractional units (0..TALER_MERCHANT_UNIT_FRAC_BASE-1) - */ -static void -compute_available_quantity (const struct TALER_MERCHANTDB_ProductDetails *pd, - uint64_t *available_value, - uint32_t *available_frac) -{ - int64_t value; - int64_t frac; - - GNUNET_assert (NULL != available_value); - GNUNET_assert (NULL != available_frac); - - if ( (INT64_MAX == pd->total_stock) && - (INT32_MAX == pd->total_stock_frac) ) - { - *available_value = pd->total_stock; - *available_frac = pd->total_stock_frac; - return; - } - - value = (int64_t) pd->total_stock - - (int64_t) pd->total_sold - - (int64_t) pd->total_lost; - frac = (int64_t) pd->total_stock_frac - - (int64_t) pd->total_sold_frac - - (int64_t) pd->total_lost_frac; - - if (frac < 0) - { - int64_t borrow = ((-frac) + TALER_MERCHANT_UNIT_FRAC_BASE - 1) - / TALER_MERCHANT_UNIT_FRAC_BASE; - value -= borrow; - frac += borrow * (int64_t) TALER_MERCHANT_UNIT_FRAC_BASE; - } - else if (frac >= TALER_MERCHANT_UNIT_FRAC_BASE) - { - int64_t carry = frac / TALER_MERCHANT_UNIT_FRAC_BASE; - value += carry; - frac -= carry * (int64_t) TALER_MERCHANT_UNIT_FRAC_BASE; - } - - if (value < 0) - { - value = 0; - frac = 0; - } - - *available_value = (uint64_t) value; - *available_frac = (uint32_t) frac; -} - - -/** - * Execute the database transaction to setup the order. - * - * @param[in,out] oc order context - * @return transaction status, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if @a uuids were insufficient to reserve required inventory - */ -static enum GNUNET_DB_QueryStatus -execute_transaction (struct OrderContext *oc) -{ - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Timestamp timestamp; - uint64_t order_serial; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "insert_order")) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - - /* Test if we already have an order with this id */ - { - json_t *contract_terms; - struct TALER_MerchantPostDataHashP orig_post; - - qs = TMH_db->lookup_order (TMH_db->cls, - oc->hc->instance->settings.id, - oc->parse_order.order_id, - &oc->execute_order.token, - &orig_post, - &contract_terms); - /* If yes, check for idempotency */ - if (0 > qs) - { - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - TMH_db->rollback (TMH_db->cls); - json_decref (contract_terms); - /* Comparing the contract terms is sufficient because all the other - params get added to it at some point. */ - if (0 == GNUNET_memcmp (&orig_post, - &oc->parse_request.h_post_data)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order creation idempotent\n"); - oc->execute_order.idempotent = true; - return qs; - } - GNUNET_break_op (0); - oc->execute_order.conflict = true; - return qs; - } - } - - /* Setup order */ - qs = TMH_db->insert_order (TMH_db->cls, - oc->hc->instance->settings.id, - oc->parse_order.order_id, - oc->parse_request.session_id, - &oc->parse_request.h_post_data, - oc->parse_order.pay_deadline, - &oc->parse_request.claim_token, - oc->serialize_order.contract, /* called 'contract terms' at database. */ - oc->parse_request.pos_key, - oc->parse_request.pos_algorithm); - if (qs <= 0) - { - /* qs == 0: probably instance does not exist (anymore) */ - TMH_db->rollback (TMH_db->cls); - return qs; - } - /* Migrate locks from UUIDs to new order: first release old locks */ - for (unsigned int i = 0; i<oc->parse_request.uuids_length; i++) - { - qs = TMH_db->unlock_inventory (TMH_db->cls, - &oc->parse_request.uuids[i]); - if (qs < 0) - { - TMH_db->rollback (TMH_db->cls); - return qs; - } - /* qs == 0 is OK here, that just means we did not HAVE any lock under this - UUID */ - } - /* Migrate locks from UUIDs to new order: acquire new locks - (note: this can basically ONLY fail on serializability OR - because the UUID locks were insufficient for the desired - quantities). */ - for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) - { - qs = TMH_db->insert_order_lock ( - TMH_db->cls, - oc->hc->instance->settings.id, - oc->parse_order.order_id, - oc->parse_request.inventory_products[i].product_id, - oc->parse_request.inventory_products[i].quantity, - oc->parse_request.inventory_products[i].quantity_frac); - if (qs < 0) - { - TMH_db->rollback (TMH_db->cls); - return qs; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* qs == 0: lock acquisition failed due to insufficient stocks */ - TMH_db->rollback (TMH_db->cls); - oc->execute_order.out_of_stock_index = i; /* indicate which product is causing the issue */ - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; - } - } - oc->execute_order.out_of_stock_index = UINT_MAX; - - /* Get the order serial and timestamp for the order we just created to - update long-poll clients. */ - qs = TMH_db->lookup_order_summary (TMH_db->cls, - oc->hc->instance->settings.id, - oc->parse_order.order_id, - &timestamp, - &order_serial); - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) - { - TMH_db->rollback (TMH_db->cls); - return qs; - } - - { - json_t *jhook; - - jhook = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), - GNUNET_JSON_pack_object_incref ("contract", - oc->serialize_order.contract), - GNUNET_JSON_pack_string ("instance_id", - oc->hc->instance->settings.id) - ); - GNUNET_assert (NULL != jhook); - qs = TMH_trigger_webhook (oc->hc->instance->settings.id, - "order_created", - jhook); - json_decref (jhook); - if (0 > qs) - { - TMH_db->rollback (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - return qs; - GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "failed to trigger webhooks"); - return qs; - } - } - - TMH_notify_order_change (oc->hc->instance, - TMH_OSF_NONE, - timestamp, - order_serial); - /* finally, commit transaction (note: if it fails, we ALSO re-acquire - the UUID locks, which is exactly what we want) */ - qs = TMH_db->commit (TMH_db->cls); - if (0 > qs) - return qs; - return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; /* 1 == success! */ -} - - -/** - * The request was successful, generate the #MHD_HTTP_OK response. - * - * @param[in,out] oc context to update - * @param claim_token claim token to use, NULL if none - */ -static void -yield_success_response (struct OrderContext *oc, - const struct TALER_ClaimTokenP *claim_token) -{ - MHD_RESULT ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - oc->connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), - GNUNET_JSON_pack_timestamp ("pay_deadline", - oc->parse_order.pay_deadline), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_data_auto ( - "token", - claim_token))); - finalize_order (oc, - ret); -} - - -/** - * Transform an order into a proposal and store it in the - * database. Write the resulting proposal or an error message - * of a MHD connection. - * - * @param[in,out] oc order context - */ -static void -phase_execute_order (struct OrderContext *oc) -{ - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; - enum GNUNET_DB_QueryStatus qs; - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Executing database transaction to create order '%s' for instance '%s'\n", - oc->parse_order.order_id, - settings->id); - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - TMH_db->preflight (TMH_db->cls); - qs = execute_transaction (oc); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } - if (0 >= qs) - { - /* Special report if retries insufficient */ - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - { - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - return; - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - /* should be: contract (!) with same order ID - already exists */ - reply_with_error ( - oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, - oc->parse_order.order_id); - return; - } - /* Other hard transaction error (disk full, etc.) */ - GNUNET_break (0); - reply_with_error ( - oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return; - } - - /* DB transaction succeeded, check for idempotent */ - if (oc->execute_order.idempotent) - { - yield_success_response (oc, - GNUNET_is_zero (&oc->execute_order.token) - ? NULL - : &oc->execute_order.token); - return; - } - if (oc->execute_order.conflict) - { - reply_with_error ( - oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, - oc->parse_order.order_id); - return; - } - - /* DB transaction succeeded, check for out-of-stock */ - if (oc->execute_order.out_of_stock_index < UINT_MAX) - { - /* We had a product that has insufficient quantities, - generate the details for the response. */ - struct TALER_MERCHANTDB_ProductDetails pd; - MHD_RESULT ret; - const struct InventoryProduct *ip; - size_t num_categories = 0; - uint64_t *categories = NULL; - uint64_t available_quantity; - uint32_t available_quantity_frac; - char requested_quantity_buf[64]; - char available_quantity_buf[64]; - - ip = &oc->parse_request.inventory_products[ - oc->execute_order.out_of_stock_index]; - memset (&pd, - 0, - sizeof (pd)); - qs = TMH_db->lookup_product ( - TMH_db->cls, - oc->hc->instance->settings.id, - ip->product_id, - &pd, - &num_categories, - &categories); - switch (qs) - { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - GNUNET_free (categories); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order creation failed: product out of stock\n"); - - compute_available_quantity (&pd, - &available_quantity, - &available_quantity_frac); - TALER_MERCHANT_vk_format_fractional_string ( - TALER_MERCHANT_VK_QUANTITY, - ip->quantity, - ip->quantity_frac, - sizeof (requested_quantity_buf), - requested_quantity_buf); - TALER_MERCHANT_vk_format_fractional_string ( - TALER_MERCHANT_VK_QUANTITY, - available_quantity, - available_quantity_frac, - sizeof (available_quantity_buf), - available_quantity_buf); - ret = TALER_MHD_REPLY_JSON_PACK ( - oc->connection, - MHD_HTTP_GONE, - GNUNET_JSON_pack_string ( - "product_id", - ip->product_id), - GNUNET_JSON_pack_uint64 ( - "requested_quantity", - ip->quantity), - GNUNET_JSON_pack_string ( - "unit_requested_quantity", - requested_quantity_buf), - GNUNET_JSON_pack_uint64 ( - "available_quantity", - available_quantity), - GNUNET_JSON_pack_string ( - "unit_available_quantity", - available_quantity_buf), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ( - "restock_expected", - pd.next_restock))); - TALER_MERCHANTDB_product_details_free (&pd); - finalize_order (oc, - ret); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order creation failed: unknown product out of stock\n"); - finalize_order (oc, - TALER_MHD_REPLY_JSON_PACK ( - oc->connection, - MHD_HTTP_GONE, - GNUNET_JSON_pack_string ( - "product_id", - ip->product_id), - GNUNET_JSON_pack_uint64 ( - "requested_quantity", - ip->quantity), - GNUNET_JSON_pack_uint64 ( - "available_quantity", - 0))); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - reply_with_error ( - oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - return; - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - reply_with_error ( - oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - return; - } - GNUNET_break (0); - oc->phase = ORDER_PHASE_FINISHED_MHD_NO; - return; - } /* end 'out of stock' case */ - - /* Everything in-stock, generate positive response */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order creation succeeded\n"); - yield_success_response (oc, - GNUNET_is_zero (&oc->parse_request.claim_token) - ? NULL - : &oc->parse_request.claim_token); -} - - -/* ***************** ORDER_PHASE_CHECK_CONTRACT **************** */ - - -/** - * Check that the contract is now well-formed. Upon success, continue - * processing with execute_order(). - * - * @param[in,out] oc order context - */ -static void -phase_check_contract (struct OrderContext *oc) -{ - struct TALER_PrivateContractHashP h_control; - - switch (TALER_JSON_contract_hash (oc->serialize_order.contract, - &h_control)) - { - case GNUNET_SYSERR: - GNUNET_break (0); - reply_with_error ( - oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "could not compute hash of serialized order"); - return; - case GNUNET_NO: - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "order contained unallowed values"); - return; - case GNUNET_OK: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Contract hash is %s\n", - GNUNET_h2s (&h_control.hash)); - oc->phase++; - return; - } - GNUNET_assert (0); -} - - -/* ***************** ORDER_PHASE_SALT_FORGETTABLE **************** */ - - -/** - * Modify the final contract terms adding salts for - * items that are forgettable. - * - * @param[in,out] oc order context - */ -static void -phase_salt_forgettable (struct OrderContext *oc) -{ - if (GNUNET_OK != - TALER_JSON_contract_seed_forgettable (oc->parse_request.order, - oc->serialize_order.contract)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_JSON_INVALID, - "could not compute hash of order due to bogus forgettable fields"); - return; - } - oc->phase++; -} - - -/* ***************** ORDER_PHASE_SERIALIZE_ORDER **************** */ - -/** - * Get rounded time interval. @a start is calculated by rounding - * @a ts down to the nearest multiple of @a precision. - * - * @param precision rounding precision. - * year, month, day, hour, minute are supported. - * @param ts timestamp to round - * @param[out] start start of the interval - * @return #GNUNET_OK on success, #GNUNET_SYSERR on error - */ -static enum GNUNET_GenericReturnValue -get_rounded_time_interval_down (struct GNUNET_TIME_Relative precision, - struct GNUNET_TIME_Timestamp ts, - struct GNUNET_TIME_Timestamp *start) -{ - enum GNUNET_TIME_RounderInterval ri; - - ri = GNUNET_TIME_relative_to_round_interval (precision); - if ( (GNUNET_TIME_RI_NONE == ri) && - (! GNUNET_TIME_relative_is_zero (precision)) ) - { - *start = ts; - return GNUNET_SYSERR; - } - *start = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_round_down (ts.abs_time, - ri)); - return GNUNET_OK; -} - - -/** - * Get rounded time interval. @a start is calculated by rounding - * @a ts up to the nearest multiple of @a precision. - * - * @param precision rounding precision. - * year, month, day, hour, minute are supported. - * @param ts timestamp to round - * @param[out] start start of the interval - * @return #GNUNET_OK on success, #GNUNET_SYSERR on error - */ -static enum GNUNET_GenericReturnValue -get_rounded_time_interval_up (struct GNUNET_TIME_Relative precision, - struct GNUNET_TIME_Timestamp ts, - struct GNUNET_TIME_Timestamp *start) -{ - enum GNUNET_TIME_RounderInterval ri; - - ri = GNUNET_TIME_relative_to_round_interval (precision); - if ( (GNUNET_TIME_RI_NONE == ri) && - (! GNUNET_TIME_relative_is_zero (precision)) ) - { - *start = ts; - return GNUNET_SYSERR; - } - *start = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_round_up (ts.abs_time, - ri)); - return GNUNET_OK; -} - - -/** - * Find the family entry for the family of the given @a slug - * in @a oc. - * - * @param[in] oc order context to search - * @param slug slug to search for - * @return NULL if @a slug was not found - */ -static struct TALER_MERCHANT_ContractTokenFamily * -find_family (const struct OrderContext *oc, - const char *slug) -{ - for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) - { - if (0 == strcmp (oc->parse_choices.token_families[i].slug, - slug)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Token family %s already in order\n", - slug); - return &oc->parse_choices.token_families[i]; - } - } - return NULL; -} - - -/** - * Function called with each applicable family key that should - * be added to the respective token family of the order. - * - * @param cls a `struct OrderContext *` to expand - * @param tfkd token family key details to add to the contract - */ -static void -add_family_key (void *cls, - const struct TALER_MERCHANTDB_TokenFamilyKeyDetails *tfkd) -{ - struct OrderContext *oc = cls; - const struct TALER_MERCHANTDB_TokenFamilyDetails *tf = &tfkd->token_family; - struct TALER_MERCHANT_ContractTokenFamily *family; - - family = find_family (oc, - tf->slug); - if (NULL == family) - { - /* Family not yet in our contract terms, create new entry */ - struct TALER_MERCHANT_ContractTokenFamily new_family = { - .slug = GNUNET_strdup (tf->slug), - .name = GNUNET_strdup (tf->name), - .description = GNUNET_strdup (tf->description), - .description_i18n = json_incref (tf->description_i18n), - }; - - switch (tf->kind) - { - case TALER_MERCHANTDB_TFK_Subscription: - { - json_t *tdomains = json_object_get (tf->extra_data, - "trusted_domains"); - json_t *dom; - size_t i; - - new_family.kind = TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION; - new_family.critical = true; - new_family.details.subscription.trusted_domains_len - = json_array_size (tdomains); - GNUNET_assert (new_family.details.subscription.trusted_domains_len - < UINT_MAX); - new_family.details.subscription.trusted_domains - = GNUNET_new_array ( - new_family.details.subscription.trusted_domains_len, - char *); - json_array_foreach (tdomains, i, dom) - { - const char *val; - - val = json_string_value (dom); - GNUNET_break (NULL != val); - if (NULL != val) - new_family.details.subscription.trusted_domains[i] - = GNUNET_strdup (val); - } - break; - } - case TALER_MERCHANTDB_TFK_Discount: - { - json_t *edomains = json_object_get (tf->extra_data, - "expected_domains"); - json_t *dom; - size_t i; - - new_family.kind = TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT; - new_family.critical = false; - new_family.details.discount.expected_domains_len - = json_array_size (edomains); - GNUNET_assert (new_family.details.discount.expected_domains_len - < UINT_MAX); - new_family.details.discount.expected_domains - = GNUNET_new_array ( - new_family.details.discount.expected_domains_len, - char *); - json_array_foreach (edomains, i, dom) - { - const char *val; - - val = json_string_value (dom); - GNUNET_break (NULL != val); - if (NULL != val) - new_family.details.discount.expected_domains[i] - = GNUNET_strdup (val); - } - break; - } - } - GNUNET_array_append (oc->parse_choices.token_families, - oc->parse_choices.token_families_len, - new_family); - family = &oc->parse_choices.token_families[ - oc->parse_choices.token_families_len - 1]; - } - if (NULL == tfkd->pub.public_key) - return; - for (unsigned int i = 0; i<family->keys_len; i++) - { - if (TALER_token_issue_pub_cmp (&family->keys[i].pub, - &tfkd->pub)) - { - /* A matching key is already in the list. */ - return; - } - } - - { - struct TALER_MERCHANT_ContractTokenFamilyKey key; - - TALER_token_issue_pub_copy (&key.pub, - &tfkd->pub); - key.valid_after = tfkd->signature_validity_start; - key.valid_before = tfkd->signature_validity_end; - GNUNET_array_append (family->keys, - family->keys_len, - key); - } -} - - -/** - * Check if the token family with the given @a slug is already present in the - * list of token families for this order. If not, fetch its details and add it - * to the list. - * - * @param[in,out] oc order context - * @param slug slug of the token family - * @return #GNUNET_OK on success, #GNUNET_SYSERR on error - */ -static enum GNUNET_GenericReturnValue -add_input_token_family (struct OrderContext *oc, - const char *slug) -{ - struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - struct GNUNET_TIME_Timestamp end = oc->parse_order.pay_deadline; - enum GNUNET_DB_QueryStatus qs; - enum TALER_ErrorCode ec = TALER_EC_INVALID; /* make compiler happy */ - unsigned int http_status = 0; /* make compiler happy */ - - qs = TMH_db->lookup_token_family_keys (TMH_db->cls, - oc->hc->instance->settings.id, - slug, - now, - end, - &add_family_key, - oc); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ec = TALER_EC_GENERIC_DB_FETCH_FAILED; - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Input token family slug %s unknown\n", - slug); - http_status = MHD_HTTP_NOT_FOUND; - ec = TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN; - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return GNUNET_OK; - } - reply_with_error (oc, - http_status, - ec, - slug); - return GNUNET_SYSERR; -} - - -/** - * Find the index of a key in the @a family that is valid at - * the time @a valid_at. - * - * @param family to search - * @param valid_at time when the key must be valid - * @param[out] key_index index to initialize - * @return #GNUNET_OK if a matching key was found - */ -static enum GNUNET_GenericReturnValue -find_key_index (struct TALER_MERCHANT_ContractTokenFamily *family, - struct GNUNET_TIME_Timestamp valid_at, - unsigned int *key_index) -{ - for (unsigned int i = 0; i<family->keys_len; i++) - { - if ( (GNUNET_TIME_timestamp_cmp (family->keys[i].valid_after, - <=, - valid_at)) && - (GNUNET_TIME_timestamp_cmp (family->keys[i].valid_before, - >=, - valid_at)) ) - { - /* The token family and a matching key already exist. */ - *key_index = i; - return GNUNET_OK; - } - } - return GNUNET_NO; -} - - -/** - * Create fresh key pair based on @a cipher_spec. - * - * @param cipher_spec which kind of key pair should we generate - * @param[out] priv set to new private key - * @param[out] pub set to new public key - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -create_key (const char *cipher_spec, - struct TALER_TokenIssuePrivateKey *priv, - struct TALER_TokenIssuePublicKey *pub) -{ - unsigned int len; - char dummy; - - if (0 == strcmp ("cs", - cipher_spec)) - { - GNUNET_CRYPTO_blind_sign_keys_create ( - &priv->private_key, - &pub->public_key, - GNUNET_CRYPTO_BSA_CS); - return GNUNET_OK; - } - if (1 == - sscanf (cipher_spec, - "rsa(%u)%c", - &len, - &dummy)) - { - GNUNET_CRYPTO_blind_sign_keys_create ( - &priv->private_key, - &pub->public_key, - GNUNET_CRYPTO_BSA_RSA, - len); - return GNUNET_OK; - } - return GNUNET_SYSERR; -} - - -/** - * Check if the token family with the given @a slug is already present in the - * list of token families for this order. If not, fetch its details and add it - * to the list. Also checks if there is a public key with that expires after - * the payment deadline. If not, generates a new key pair and stores it in - * the database. - * - * @param[in,out] oc order context - * @param slug slug of the token family - * @param valid_at time when the token returned must be valid - * @param[out] key_index set to the index of the respective public - * key in the @a slug's token family keys array. - * @return #GNUNET_OK on success, #GNUNET_SYSERR on error - */ -static enum GNUNET_GenericReturnValue -add_output_token_family (struct OrderContext *oc, - const char *slug, - struct GNUNET_TIME_Timestamp valid_at, - unsigned int *key_index) -{ - struct TALER_MERCHANTDB_TokenFamilyKeyDetails key_details; - struct TALER_MERCHANT_ContractTokenFamily *family; - enum GNUNET_DB_QueryStatus qs; - - family = find_family (oc, - slug); - if ( (NULL != family) && - (GNUNET_OK == - find_key_index (family, - valid_at, - key_index)) ) - return GNUNET_OK; - qs = TMH_db->lookup_token_family_key (TMH_db->cls, - oc->hc->instance->settings.id, - slug, - valid_at, - oc->parse_order.pay_deadline, - &key_details); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_token_family_key"); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* Single-statement transaction shouldn't possibly cause serialization errors. - Thus treating like a hard error. */ - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "lookup_token_family_key"); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Output token family slug %s unknown\n", - slug); - reply_with_error (oc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, - slug); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Lookup of token family %s at %llu yielded %s\n", - slug, - (unsigned long long) valid_at.abs_time.abs_value_us, - NULL == key_details.pub.public_key ? "no key" : "a key"); - - if (NULL == family) - { - add_family_key (oc, - &key_details); - family = find_family (oc, - slug); - GNUNET_assert (NULL != family); - } - /* we don't need the full family details anymore */ - GNUNET_free (key_details.token_family.slug); - GNUNET_free (key_details.token_family.name); - GNUNET_free (key_details.token_family.description); - json_decref (key_details.token_family.description_i18n); - json_decref (key_details.token_family.extra_data); - - if (NULL != key_details.pub.public_key) - { - /* lookup_token_family_key must have found a matching key, - and it must have been added. Find and use the index. */ - GNUNET_CRYPTO_blind_sign_pub_decref (key_details.pub.public_key); - GNUNET_CRYPTO_blind_sign_priv_decref (key_details.priv.private_key); - GNUNET_free (key_details.token_family.cipher_spec); - GNUNET_assert (GNUNET_OK == - find_key_index (family, - valid_at, - key_index)); - return GNUNET_OK; - } - - /* No suitable key exists, create one! */ - { - struct TALER_MERCHANT_ContractTokenFamilyKey key; - enum GNUNET_DB_QueryStatus iqs; - struct TALER_TokenIssuePrivateKey token_priv; - struct GNUNET_TIME_Timestamp key_expires; - struct GNUNET_TIME_Timestamp round_start; - - if (GNUNET_OK != - get_rounded_time_interval_down ( - key_details.token_family.validity_granularity, - valid_at, - &round_start)) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unsupported validity granularity interval %s found in database for token family %s!\n", - GNUNET_TIME_relative2s ( - key_details.token_family.validity_granularity, - false), - slug); - GNUNET_free (key_details.token_family.cipher_spec); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "get_rounded_time_interval_down failed"); - return GNUNET_SYSERR; - } - if (GNUNET_TIME_relative_cmp ( - key_details.token_family.duration, - <, - GNUNET_TIME_relative_add ( - key_details.token_family.validity_granularity, - key_details.token_family.start_offset))) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Inconsistent duration %s found in database for token family %s (below validity granularity plus start_offset)!\n", - GNUNET_TIME_relative2s (key_details.token_family.duration, - false), - slug); - GNUNET_free (key_details.token_family.cipher_spec); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "duration, validty_granularity and start_offset inconsistent for token family"); - return GNUNET_SYSERR; - } - key.valid_after - = GNUNET_TIME_timestamp_max ( - GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_subtract ( - round_start.abs_time, - key_details.token_family.start_offset)), - key_details.token_family.valid_after); - key.valid_before - = GNUNET_TIME_timestamp_min ( - GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add ( - key.valid_after.abs_time, - key_details.token_family.duration)), - key_details.token_family.valid_before); - GNUNET_assert (GNUNET_OK == - get_rounded_time_interval_down ( - key_details.token_family.validity_granularity, - key.valid_before, - &key_expires)); - if (GNUNET_TIME_timestamp_cmp ( - key_expires, - ==, - round_start)) - { - /* valid_before does not actually end after the - next rounded validity period would start; - determine next rounded validity period - start point and extend valid_before to cover - the full validity period */ - GNUNET_assert ( - GNUNET_OK == - get_rounded_time_interval_up ( - key_details.token_family.validity_granularity, - key.valid_before, - &key_expires)); - /* This should basically always end up being key_expires */ - key.valid_before = GNUNET_TIME_timestamp_max (key.valid_before, - key_expires); - } - if (GNUNET_OK != - create_key (key_details.token_family.cipher_spec, - &token_priv, - &key.pub)) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unsupported cipher family %s found in database for token family %s!\n", - key_details.token_family.cipher_spec, - slug); - GNUNET_free (key_details.token_family.cipher_spec); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "invalid cipher stored in local database for token family"); - return GNUNET_SYSERR; - } - GNUNET_free (key_details.token_family.cipher_spec); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Storing new key for slug %s of %s\n", - slug, - oc->hc->instance->settings.id); - iqs = TMH_db->insert_token_family_key (TMH_db->cls, - oc->hc->instance->settings.id, - slug, - &key.pub, - &token_priv, - key_expires, - key.valid_after, - key.valid_before); - GNUNET_CRYPTO_blind_sign_priv_decref (token_priv.private_key); - switch (iqs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SOFT_ERROR: - /* Single-statement transaction shouldn't possibly cause serialization errors. - Thus treating like a hard error. */ - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - return GNUNET_SYSERR; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - *key_index = family->keys_len; - GNUNET_array_append (family->keys, - family->keys_len, - key); - } - return GNUNET_OK; -} - - -/** - * Build JSON array that represents all of the token families - * in the contract. - * - * @param[in] oc v1-style order context - * @return JSON array with token families for the contract - */ -static json_t * -output_token_families (struct OrderContext *oc) -{ - json_t *token_families = json_object (); - - GNUNET_assert (NULL != token_families); - for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) - { - const struct TALER_MERCHANT_ContractTokenFamily *family - = &oc->parse_choices.token_families[i]; - json_t *jfamily; - - jfamily = TALER_MERCHANT_json_from_token_family (family); - - GNUNET_assert (jfamily != NULL); - - GNUNET_assert (0 == - json_object_set_new (token_families, - family->slug, - jfamily)); - } - return token_families; -} - - -/** - * Build JSON array that represents all of the contract choices - * in the contract. - * - * @param[in] oc v1-style order context - * @return JSON array with token families for the contract - */ -static json_t * -output_contract_choices (struct OrderContext *oc) -{ - json_t *choices = json_array (); - - GNUNET_assert (NULL != choices); - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - oc->parse_choices.choices[i].max_fee = - oc->set_max_fee.details.v1.max_fees[i]; - GNUNET_assert (0 == json_array_append_new ( - choices, - TALER_MERCHANT_json_from_contract_choice ( - &oc->parse_choices.choices[i], - false))); - } - - return choices; -} - - -/** - * Serialize order into @a oc->serialize_order.contract, - * ready to be stored in the database. Upon success, continue - * processing with check_contract(). - * - * @param[in,out] oc order context - */ -static void -phase_serialize_order (struct OrderContext *oc) -{ - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; - json_t *merchant; - - merchant = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - settings->name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("website", - settings->website)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("email", - settings->email)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("logo", - settings->logo))); - GNUNET_assert (NULL != merchant); - { - json_t *loca; - - /* Handle merchant address */ - loca = settings->address; - if (NULL != loca) - { - loca = json_deep_copy (loca); - GNUNET_assert (NULL != loca); - GNUNET_assert (0 == - json_object_set_new (merchant, - "address", - loca)); - } - } - { - json_t *juri; - - /* Handle merchant jurisdiction */ - juri = settings->jurisdiction; - if (NULL != juri) - { - juri = json_deep_copy (juri); - GNUNET_assert (NULL != juri); - GNUNET_assert (0 == - json_object_set_new (merchant, - "jurisdiction", - juri)); - } - } - - oc->serialize_order.contract = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_int64 ("version", - oc->parse_order.version), - GNUNET_JSON_pack_string ("summary", - oc->parse_order.summary), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ( - "summary_i18n", - (json_t *) oc->parse_order.summary_i18n)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("public_reorder_url", - oc->parse_order.public_reorder_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_message", - oc->parse_order.fulfillment_message)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ( - "fulfillment_message_i18n", - (json_t *) oc->parse_order.fulfillment_message_i18n)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - oc->parse_order.fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_uint64 ("minimum_age", - oc->parse_order.minimum_age)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_uint64 ("default_money_pot", - oc->parse_order.order_default_money_pot)), - GNUNET_JSON_pack_array_incref ("products", - oc->merge_inventory.products), - GNUNET_JSON_pack_data_auto ("h_wire", - &oc->select_wire_method.wm->h_wire), - GNUNET_JSON_pack_string ("wire_method", - oc->select_wire_method.wm->wire_method), - GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), - GNUNET_JSON_pack_timestamp ("timestamp", - oc->parse_order.timestamp), - GNUNET_JSON_pack_timestamp ("pay_deadline", - oc->parse_order.pay_deadline), - GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", - oc->parse_order.wire_deadline), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("delivery_date", - oc->parse_order.delivery_date)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ( - "delivery_location", - (json_t *) oc->parse_order.delivery_location)), - GNUNET_JSON_pack_string ("merchant_base_url", - oc->parse_order.merchant_base_url), - GNUNET_JSON_pack_object_steal ("merchant", - merchant), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &oc->hc->instance->merchant_pub), - GNUNET_JSON_pack_array_incref ("exchanges", - oc->select_wire_method.exchanges), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("extra", - (json_t *) oc->parse_order.extra)) - ); - - { - json_t *xtra; - - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - xtra = GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("max_fee", - &oc->set_max_fee.details.v0.max_fee), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - oc->parse_order.details.v0.no_tip - ? NULL - : &oc->parse_order.details.v0.tip)), - TALER_JSON_pack_amount ("amount", - &oc->parse_order.details.v0.brutto)); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - { - json_t *token_families = output_token_families (oc); - json_t *choices = output_contract_choices (oc); - - if ( (NULL == token_families) || - (NULL == choices) ) - { - GNUNET_break (0); - return; - } - xtra = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_array_steal ("choices", - choices), - GNUNET_JSON_pack_object_steal ("token_families", - token_families)); - break; - } - default: - GNUNET_assert (0); - } - GNUNET_assert (0 == - json_object_update (oc->serialize_order.contract, - xtra)); - json_decref (xtra); - } - - - /* Pack does not work here, because it doesn't set zero-values for timestamps */ - GNUNET_assert (0 == - json_object_set_new (oc->serialize_order.contract, - "refund_deadline", - GNUNET_JSON_from_timestamp ( - oc->parse_order.refund_deadline))); - /* auto_refund should only be set if it is not 0 */ - if (! GNUNET_TIME_relative_is_zero (oc->parse_order.auto_refund)) - { - /* Pack does not work here, because it sets zero-values for relative times */ - GNUNET_assert (0 == - json_object_set_new (oc->serialize_order.contract, - "auto_refund", - GNUNET_JSON_from_time_rel ( - oc->parse_order.auto_refund))); - } - - oc->phase++; -} - - -/* ***************** ORDER_PHASE_SET_MAX_FEE **************** */ - - -/** - * Set @a max_fee in @a oc based on @a max_stefan_fee value if not overridden - * by @a client_fee. If neither is set, set the fee to zero using currency - * from @a brutto. - * - * @param[in,out] oc order context - * @param brutto brutto amount to compute fee for - * @param client_fee client-given fee override (or invalid) - * @param max_stefan_fee maximum STEFAN fee of any exchange - * @param max_fee set to the maximum stefan fee - */ -static void -compute_fee (struct OrderContext *oc, - const struct TALER_Amount *brutto, - const struct TALER_Amount *client_fee, - const struct TALER_Amount *max_stefan_fee, - struct TALER_Amount *max_fee) -{ - const struct TALER_MERCHANTDB_InstanceSettings *settings - = &oc->hc->instance->settings; - - if (GNUNET_OK == - TALER_amount_is_valid (client_fee)) - { - *max_fee = *client_fee; - return; - } - if ( (settings->use_stefan) && - (NULL != max_stefan_fee) && - (GNUNET_OK == - TALER_amount_is_valid (max_stefan_fee)) ) - { - *max_fee = *max_stefan_fee; - return; - } - GNUNET_assert ( - GNUNET_OK == - TALER_amount_set_zero (brutto->currency, - max_fee)); -} - - -/** - * Initialize "set_max_fee" in @a oc based on STEFAN value or client - * preference. Upon success, continue processing in next phase. - * - * @param[in,out] oc order context - */ -static void -phase_set_max_fee (struct OrderContext *oc) -{ - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - compute_fee (oc, - &oc->parse_order.details.v0.brutto, - &oc->parse_order.details.v0.max_fee, - &oc->set_exchanges.details.v0.max_stefan_fee, - &oc->set_max_fee.details.v0.max_fee); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - oc->set_max_fee.details.v1.max_fees - = GNUNET_new_array (oc->parse_choices.choices_len, - struct TALER_Amount); - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - compute_fee (oc, - &oc->parse_choices.choices[i].amount, - &oc->parse_choices.choices[i].max_fee, - NULL != oc->set_exchanges.details.v1.max_stefan_fees - ? &oc->set_exchanges.details.v1.max_stefan_fees[i] - : NULL, - &oc->set_max_fee.details.v1.max_fees[i]); - break; - default: - GNUNET_break (0); - break; - } - oc->phase++; -} - - -/* ***************** ORDER_PHASE_SELECT_WIRE_METHOD **************** */ - -/** - * Check that the @a brutto amount is at or below the - * limits we have for the respective wire method candidate. - * - * @param wmc wire method candidate to check - * @param brutto amount to check - * @return true if the amount is OK, false if it is too high - */ -static bool -check_limits (struct WireMethodCandidate *wmc, - const struct TALER_Amount *brutto) -{ - for (unsigned int i = 0; i<wmc->num_total_exchange_limits; i++) - { - const struct TALER_Amount *total_exchange_limit - = &wmc->total_exchange_limits[i]; - - if (GNUNET_OK != - TALER_amount_cmp_currency (brutto, - total_exchange_limit)) - continue; - if (1 != - TALER_amount_cmp (brutto, - total_exchange_limit)) - return true; - } - return false; -} - - -/** - * Phase to select a wire method that will be acceptable for the order. - * If none is "perfect" (allows all choices), might jump back to the - * previous phase to force "/keys" downloads to see if that helps. - * - * @param[in,out] oc order context - */ -static void -phase_select_wire_method (struct OrderContext *oc) -{ - const struct TALER_Amount *ea; - struct WireMethodCandidate *best = NULL; - unsigned int max_choices = 0; - unsigned int want_choices = 0; - - for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; - NULL != wmc; - wmc = wmc->next) - { - unsigned int num_choices = 0; - - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - want_choices = 1; - ea = &oc->parse_order.details.v0.brutto; - if (TALER_amount_is_zero (ea) || - check_limits (wmc, - ea)) - num_choices++; - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - want_choices = oc->parse_choices.choices_len; - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - ea = &oc->parse_choices.choices[i].amount; - if (TALER_amount_is_zero (ea) || - check_limits (wmc, - ea)) - num_choices++; - } - break; - default: - GNUNET_assert (0); - } - if (num_choices > max_choices) - { - best = wmc; - max_choices = num_choices; - } - } - - if ( (want_choices > max_choices) && - (oc->set_exchanges.promising_exchange) && - (! oc->set_exchanges.forced_reload) ) - { - oc->set_exchanges.exchange_ok = false; - /* Not all choices in the contract can work with these - exchanges, try again with forcing /keys download */ - for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; - NULL != wmc; - wmc = wmc->next) - { - json_array_clear (wmc->exchanges); - GNUNET_array_grow (wmc->total_exchange_limits, - wmc->num_total_exchange_limits, - 0); - } - oc->phase = ORDER_PHASE_SET_EXCHANGES; - return; - } - - if ( (NULL == best) && - (NULL != oc->parse_request.payment_target) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create order: lacking suitable exchanges for payment target `%s'\n", - oc->parse_request.payment_target); - reply_with_error ( - oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, - oc->parse_request.payment_target); - return; - } - - if (NULL == best) - { - MHD_RESULT mret; - - /* We actually do not have ANY workable exchange(s) */ - mret = TALER_MHD_reply_json_steal ( - oc->connection, - GNUNET_JSON_PACK ( - TALER_JSON_pack_ec ( - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS), - GNUNET_JSON_pack_array_incref ( - "exchange_rejections", - oc->set_exchanges.exchange_rejections)), - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS); - finalize_order (oc, - mret); - return; - } - - if (want_choices > max_choices) - { - /* Some choices are unpayable */ - GNUNET_log ( - GNUNET_ERROR_TYPE_WARNING, - "Creating order, but some choices do not work with the selected wire method\n"); - } - if ( (0 == json_array_size (best->exchanges)) && - (oc->add_payment_details.need_exchange) ) - { - /* We did not find any reasonable exchange */ - GNUNET_log ( - GNUNET_ERROR_TYPE_WARNING, - "Creating order, but only for choices without payment\n"); - } - - oc->select_wire_method.wm - = best->wm; - oc->select_wire_method.exchanges - = json_incref (best->exchanges); - oc->phase++; -} - - -/* ***************** ORDER_PHASE_SET_EXCHANGES **************** */ - -/** - * Exchange `/keys` processing is done, resume handling - * the order. - * - * @param[in,out] oc context to resume - */ -static void -resume_with_keys (struct OrderContext *oc) -{ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming order processing after /keys downloads\n"); - GNUNET_assert (GNUNET_YES == oc->suspended); - GNUNET_CONTAINER_DLL_remove (oc_head, - oc_tail, - oc); - oc->suspended = GNUNET_NO; - MHD_resume_connection (oc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -/** - * Given a @a brutto amount for exchange with @a keys, set the - * @a stefan_fee. Note that @a stefan_fee is updated to the maximum - * of the input and the computed fee. - * - * @param[in,out] keys exchange keys - * @param brutto some brutto amount the client is to pay - * @param[in,out] stefan_fee set to STEFAN fee to be paid by the merchant - */ -static void -compute_stefan_fee (const struct TALER_EXCHANGE_Keys *keys, - const struct TALER_Amount *brutto, - struct TALER_Amount *stefan_fee) -{ - struct TALER_Amount net; - - if (GNUNET_SYSERR != - TALER_EXCHANGE_keys_stefan_b2n (keys, - brutto, - &net)) - { - struct TALER_Amount fee; - - TALER_EXCHANGE_keys_stefan_round (keys, - &net); - if (-1 == TALER_amount_cmp (brutto, - &net)) - { - /* brutto < netto! */ - /* => after rounding, there is no real difference */ - net = *brutto; - } - GNUNET_assert (0 <= - TALER_amount_subtract (&fee, - brutto, - &net)); - if ( (GNUNET_OK != - TALER_amount_is_valid (stefan_fee)) || - (-1 == TALER_amount_cmp (stefan_fee, - &fee)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Updated STEFAN-based fee to %s\n", - TALER_amount2s (&fee)); - *stefan_fee = fee; - } - } -} - - -/** - * Update MAX STEFAN fees based on @a keys. - * - * @param[in,out] oc order context to update - * @param keys keys to derive STEFAN fees from - */ -static void -update_stefan (struct OrderContext *oc, - const struct TALER_EXCHANGE_Keys *keys) -{ - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - compute_stefan_fee (keys, - &oc->parse_order.details.v0.brutto, - &oc->set_exchanges.details.v0.max_stefan_fee); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - oc->set_exchanges.details.v1.max_stefan_fees - = GNUNET_new_array (oc->parse_choices.choices_len, - struct TALER_Amount); - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - if (0 == strcasecmp (keys->currency, - oc->parse_choices.choices[i].amount.currency)) - compute_stefan_fee (keys, - &oc->parse_choices.choices[i].amount, - &oc->set_exchanges.details.v1.max_stefan_fees[i]); - break; - default: - GNUNET_assert (0); - } -} - - -/** - * Check our KYC status at all exchanges as our current limit is - * too low and we failed to create an order. - * - * @param oc order context - * @param wmc wire method candidate to notify for - * @param exchange_url exchange to notify about - */ -static void -notify_kyc_required (const struct OrderContext *oc, - const struct WireMethodCandidate *wmc, - const char *exchange_url) -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) - }; - char *hws; - char *extra; - - hws = GNUNET_STRINGS_data_to_string_alloc ( - &wmc->wm->h_wire, - sizeof (wmc->wm->h_wire)); - - GNUNET_asprintf (&extra, - "%s %s", - hws, - exchange_url); - TMH_db->event_notify (TMH_db->cls, - &es, - extra, - strlen (extra) + 1); - GNUNET_free (extra); - GNUNET_free (hws); -} - - -/** - * Add a reason why a particular exchange was rejected to our - * response data. - * - * @param[in,out] oc order context to update - * @param exchange_url exchange this is about - * @param ec error code to set for the exchange - */ -static void -add_rejection (struct OrderContext *oc, - const char *exchange_url, - enum TALER_ErrorCode ec) -{ - if (NULL == oc->set_exchanges.exchange_rejections) - { - oc->set_exchanges.exchange_rejections = json_array (); - GNUNET_assert (NULL != oc->set_exchanges.exchange_rejections); - } - GNUNET_assert (0 == - json_array_append_new ( - oc->set_exchanges.exchange_rejections, - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("exchange_url", - exchange_url), - TALER_JSON_pack_ec (ec)))); -} - - -/** - * Checks the limits that apply for this @a exchange and - * the @a wmc and if the exchange is acceptable at all, adds it - * to the list of exchanges for the @a wmc. - * - * @param oc context of the order - * @param exchange internal handle for the exchange - * @param exchange_url base URL of this exchange - * @param wmc wire method to evaluate this exchange for - * @return true if the exchange is acceptable for the contract - */ -static bool -get_acceptable (struct OrderContext *oc, - const struct TMH_Exchange *exchange, - const char *exchange_url, - struct WireMethodCandidate *wmc) -{ - const struct TALER_Amount *max_needed = NULL; - unsigned int priority = 42; /* make compiler happy */ - json_t *j_exchange; - enum TMH_ExchangeStatus res; - struct TALER_Amount max_amount; - - for (unsigned int i = 0; - i<oc->add_payment_details.num_max_choice_limits; - i++) - { - struct TALER_Amount *val = &oc->add_payment_details.max_choice_limits[i]; - - if (0 == strcasecmp (val->currency, - TMH_EXCHANGES_get_currency (exchange))) - { - max_needed = val; - break; - } - } - if (NULL == max_needed) - { - /* exchange currency not relevant for any of our choices, skip it */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange %s with currency `%s' is not applicable to this order\n", - exchange_url, - TMH_EXCHANGES_get_currency (exchange)); - add_rejection (oc, - exchange_url, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH); - return false; - } - - max_amount = *max_needed; - res = TMH_exchange_check_debit ( - oc->hc->instance->settings.id, - exchange, - wmc->wm, - &max_amount); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange %s evaluated at %d with max %s\n", - exchange_url, - res, - TALER_amount2s (&max_amount)); - if (TALER_amount_is_zero (&max_amount)) - { - if (! TALER_amount_is_zero (max_needed)) - { - /* Trigger re-checking the current deposit limit when - * paying non-zero amount with zero deposit limit */ - notify_kyc_required (oc, - wmc, - exchange_url); - } - /* If deposit is impossible, we don't list the - * exchange in the contract terms. */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange %s deposit limit is zero, skipping it\n", - exchange_url); - add_rejection (oc, - exchange_url, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED); - return false; - } - switch (res) - { - case TMH_ES_OK: - case TMH_ES_RETRY_OK: - priority = 1024; /* high */ - oc->set_exchanges.exchange_ok = true; - break; - case TMH_ES_NO_ACC: - if (oc->set_exchanges.forced_reload) - priority = 0; /* fresh negative response */ - else - priority = 512; /* stale negative response */ - break; - case TMH_ES_NO_CURR: - if (oc->set_exchanges.forced_reload) - priority = 0; /* fresh negative response */ - else - priority = 512; /* stale negative response */ - break; - case TMH_ES_NO_KEYS: - if (oc->set_exchanges.forced_reload) - priority = 256; /* fresh, no accounts yet */ - else - priority = 768; /* stale, no accounts yet */ - break; - case TMH_ES_NO_ACC_RETRY_OK: - if (oc->set_exchanges.forced_reload) - { - priority = 0; /* fresh negative response */ - } - else - { - oc->set_exchanges.promising_exchange = true; - priority = 512; /* stale negative response */ - } - break; - case TMH_ES_NO_CURR_RETRY_OK: - if (oc->set_exchanges.forced_reload) - priority = 0; /* fresh negative response */ - else - priority = 512; /* stale negative response */ - break; - case TMH_ES_NO_KEYS_RETRY_OK: - if (oc->set_exchanges.forced_reload) - { - priority = 256; /* fresh, no accounts yet */ - } - else - { - oc->set_exchanges.promising_exchange = true; - priority = 768; /* stale, no accounts yet */ - } - break; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange %s deposit limit is %s, adding it!\n", - exchange_url, - TALER_amount2s (&max_amount)); - - j_exchange = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("url", - exchange_url), - GNUNET_JSON_pack_uint64 ("priority", - priority), - TALER_JSON_pack_amount ("max_contribution", - &max_amount), - GNUNET_JSON_pack_data_auto ("master_pub", - TMH_EXCHANGES_get_master_pub (exchange))); - GNUNET_assert (NULL != j_exchange); - /* Add exchange to list of exchanges for this wire method - candidate */ - GNUNET_assert (0 == - json_array_append_new (wmc->exchanges, - j_exchange)); - add_to_currency_vector (&wmc->total_exchange_limits, - &wmc->num_total_exchange_limits, - &max_amount, - max_needed); - return true; -} - - -/** - * Function called with the result of a #TMH_EXCHANGES_keys4exchange() - * operation. - * - * @param cls closure with our `struct RekeyExchange *` - * @param keys the keys of the exchange - * @param exchange representation of the exchange - */ -static void -keys_cb ( - void *cls, - struct TALER_EXCHANGE_Keys *keys, - struct TMH_Exchange *exchange) -{ - struct RekeyExchange *rx = cls; - struct OrderContext *oc = rx->oc; - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; - bool applicable = false; - - rx->fo = NULL; - GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, - oc->set_exchanges.pending_reload_tail, - rx); - if (NULL == keys) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to download %skeys\n", - rx->url); - oc->set_exchanges.promising_exchange = true; - add_rejection (oc, - rx->url, - TALER_EC_MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE); - goto cleanup; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got response for %skeys\n", - rx->url); - - /* Evaluate the use of this exchange for each wire method candidate */ - for (unsigned int j = 0; j<keys->accounts_len; j++) - { - struct TALER_FullPayto full_payto = keys->accounts[j].fpayto_uri; - char *wire_method = TALER_payto_get_method (full_payto.full_payto); - - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Exchange `%s' has wire method `%s'\n", - rx->url, - wire_method); - for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; - NULL != wmc; - wmc = wmc->next) - { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Order could use wire method `%s'\n", - wmc->wm->wire_method); - if (0 == strcmp (wmc->wm->wire_method, - wire_method) ) - { - applicable |= get_acceptable (oc, - exchange, - rx->url, - wmc); - } - } - GNUNET_free (wire_method); - } - if ( (! applicable) && - (! oc->set_exchanges.forced_reload) ) - { - /* Checks for 'forced_reload' to not log the error *again* - if we forced a re-load and are encountering the - applicability error a 2nd time */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Exchange `%s' %u wire methods are not applicable to this order\n", - rx->url, - keys->accounts_len); - add_rejection (oc, - rx->url, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED) - ; - } - if (applicable && - settings->use_stefan) - update_stefan (oc, - keys); -cleanup: - GNUNET_free (rx->url); - GNUNET_free (rx); - if (NULL != oc->set_exchanges.pending_reload_head) - return; - resume_with_keys (oc); -} - - -/** - * Force re-downloading of /keys from @a exchange, - * we currently have no acceptable exchange, so we - * should try to get one. - * - * @param cls closure with our `struct OrderContext` - * @param url base URL of the exchange - * @param exchange internal handle for the exchange - */ -static void -get_exchange_keys (void *cls, - const char *url, - const struct TMH_Exchange *exchange) -{ - struct OrderContext *oc = cls; - struct RekeyExchange *rx; - - rx = GNUNET_new (struct RekeyExchange); - rx->oc = oc; - rx->url = GNUNET_strdup (url); - GNUNET_CONTAINER_DLL_insert (oc->set_exchanges.pending_reload_head, - oc->set_exchanges.pending_reload_tail, - rx); - if (oc->set_exchanges.forced_reload) - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Forcing download of %skeys\n", - url); - rx->fo = TMH_EXCHANGES_keys4exchange (url, - oc->set_exchanges.forced_reload, - &keys_cb, - rx); -} - - -/** - * Task run when we are timing out on /keys and will just - * proceed with what we got. - * - * @param cls our `struct OrderContext *` to resume - */ -static void -wakeup_timeout (void *cls) -{ - struct OrderContext *oc = cls; - - oc->set_exchanges.wakeup_task = NULL; - GNUNET_assert (GNUNET_YES == oc->suspended); - GNUNET_CONTAINER_DLL_remove (oc_head, - oc_tail, - oc); - MHD_resume_connection (oc->connection); - oc->suspended = GNUNET_NO; - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ -} - - -/** - * Set list of acceptable exchanges in @a oc. Upon success, continues - * processing with add_payment_details(). - * - * @param[in,out] oc order context - * @return true to suspend execution - */ -static bool -phase_set_exchanges (struct OrderContext *oc) -{ - if (NULL != oc->set_exchanges.wakeup_task) - { - GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); - oc->set_exchanges.wakeup_task = NULL; - } - - if (! oc->add_payment_details.need_exchange) - { - /* Total amount is zero, so we don't actually need exchanges! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order total is zero, no need for exchanges\n"); - oc->select_wire_method.exchanges = json_array (); - GNUNET_assert (NULL != oc->select_wire_method.exchanges); - /* Pick first one, doesn't matter as the amount is zero */ - oc->select_wire_method.wm = oc->hc->instance->wm_head; - oc->phase = ORDER_PHASE_SET_MAX_FEE; - return false; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Trying to find exchanges\n"); - if (NULL == oc->set_exchanges.pending_reload_head) - { - if (! oc->set_exchanges.exchanges_tried) - { - oc->set_exchanges.exchanges_tried = true; - oc->set_exchanges.keys_timeout - = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); - TMH_exchange_get_trusted (&get_exchange_keys, - oc); - } - else if ( (! oc->set_exchanges.forced_reload) && - (oc->set_exchanges.promising_exchange) && - (! oc->set_exchanges.exchange_ok) ) - { - for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; - NULL != wmc; - wmc = wmc->next) - GNUNET_break (0 == - json_array_clear (wmc->exchanges)); - /* Try one more time with forcing /keys download */ - oc->set_exchanges.forced_reload = true; - TMH_exchange_get_trusted (&get_exchange_keys, - oc); - } - } - if (GNUNET_TIME_absolute_is_past (oc->set_exchanges.keys_timeout)) - { - struct RekeyExchange *rx; - - while (NULL != (rx = oc->set_exchanges.pending_reload_head)) - { - GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, - oc->set_exchanges.pending_reload_tail, - rx); - TMH_EXCHANGES_keys4exchange_cancel (rx->fo); - GNUNET_free (rx->url); - GNUNET_free (rx); - } - } - if (NULL != oc->set_exchanges.pending_reload_head) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Still trying to (re)load %skeys\n", - oc->set_exchanges.pending_reload_head->url); - oc->set_exchanges.wakeup_task - = GNUNET_SCHEDULER_add_at (oc->set_exchanges.keys_timeout, - &wakeup_timeout, - oc); - MHD_suspend_connection (oc->connection); - oc->suspended = GNUNET_YES; - GNUNET_CONTAINER_DLL_insert (oc_head, - oc_tail, - oc); - return true; /* reloads pending */ - } - oc->phase++; - return false; -} - - -/* ***************** ORDER_PHASE_ADD_PAYMENT_DETAILS **************** */ - -/** - * Process the @a payment_target and add the details of how the - * order could be paid to @a order. On success, continue - * processing with add_payment_fees(). - * - * @param[in,out] oc order context - */ -static void -phase_add_payment_details (struct OrderContext *oc) -{ - /* First, determine the maximum amounts that could be paid per currency */ - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - GNUNET_array_append (oc->add_payment_details.max_choice_limits, - oc->add_payment_details.num_max_choice_limits, - oc->parse_order.details.v0.brutto); - if (! TALER_amount_is_zero ( - &oc->parse_order.details.v0.brutto)) - { - oc->add_payment_details.need_exchange = true; - } - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - const struct TALER_Amount *amount - = &oc->parse_choices.choices[i].amount; - bool found = false; - - if (! TALER_amount_is_zero (amount)) - { - oc->add_payment_details.need_exchange = true; - } - for (unsigned int j = 0; j<oc->add_payment_details.num_max_choice_limits; - j++) - { - struct TALER_Amount *mx = &oc->add_payment_details.max_choice_limits[j]; - if (GNUNET_YES == - TALER_amount_cmp_currency (mx, - amount)) - { - TALER_amount_max (mx, - mx, - amount); - found = true; - break; - } - } - if (! found) - { - GNUNET_array_append (oc->add_payment_details.max_choice_limits, - oc->add_payment_details.num_max_choice_limits, - *amount); - } - } - break; - default: - GNUNET_assert (0); - } - - /* Then, create a candidate for each available wire method */ - for (struct TMH_WireMethod *wm = oc->hc->instance->wm_head; - NULL != wm; - wm = wm->next) - { - struct WireMethodCandidate *wmc; - - /* Locate wire method that has a matching payment target */ - if (! wm->active) - continue; /* ignore inactive methods */ - if ( (NULL != oc->parse_request.payment_target) && - (0 != strcasecmp (oc->parse_request.payment_target, - wm->wire_method) ) ) - continue; /* honor client preference */ - wmc = GNUNET_new (struct WireMethodCandidate); - wmc->wm = wm; - wmc->exchanges = json_array (); - GNUNET_assert (NULL != wmc->exchanges); - GNUNET_CONTAINER_DLL_insert (oc->add_payment_details.wmc_head, - oc->add_payment_details.wmc_tail, - wmc); - } - - if (NULL == oc->add_payment_details.wmc_head) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No wire method available for instance '%s'\n", - oc->hc->instance->settings.id); - reply_with_error (oc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, - oc->parse_request.payment_target); - return; - } - - /* next, we'll evaluate available exchanges */ - oc->phase++; -} - - -/* ***************** ORDER_PHASE_MERGE_INVENTORY **************** */ - - -/** - * Helper function to sort uint64_t array with qsort(). - * - * @param a pointer to element to compare - * @param b pointer to element to compare - * @return 0 on equal, -1 on smaller, 1 on larger - */ -static int -uint64_cmp (const void *a, - const void *b) -{ - uint64_t ua = *(const uint64_t *) a; - uint64_t ub = *(const uint64_t *) b; - - if (ua < ub) - return -1; - if (ua > ub) - return 1; - return 0; -} - - -/** - * Merge the inventory products into products, querying the - * database about the details of those products. Upon success, - * continue processing by calling add_payment_details(). - * - * @param[in,out] oc order context to process - */ -static void -phase_merge_inventory (struct OrderContext *oc) -{ - uint64_t pots[oc->parse_order.products_len + 1]; - size_t pots_off = 0; - - if (0 != oc->parse_order.order_default_money_pot) - pots[pots_off++] = oc->parse_order.order_default_money_pot; - /** - * parse_request.inventory_products => instructions to add products to contract terms - * parse_order.products => contains products that are not from the backend-managed inventory. - */ - oc->merge_inventory.products = json_array (); - for (size_t i = 0; i<oc->parse_order.products_len; i++) - { - GNUNET_assert ( - 0 == - json_array_append_new ( - oc->merge_inventory.products, - TALER_MERCHANT_product_sold_serialize (&oc->parse_order.products[i]))); - if (0 != oc->parse_order.products[i].product_money_pot) - pots[pots_off++] = oc->parse_order.products[i].product_money_pot; - } - - /* make sure pots array only has distinct elements */ - qsort (pots, - pots_off, - sizeof (uint64_t), - &uint64_cmp); - { - size_t e = 0; - - for (size_t i = 1; i<pots_off; i++) - { - if (pots[e] != pots[i]) - pots[++e] = pots[i]; - } - if (pots_off > 0) - e++; - pots_off = e; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Found %u unique money pots in order\n", - (unsigned int) pots_off); - - /* check if all money pots exist; note that we do NOT treat - the inventory products to this check, as (1) the foreign key - constraint should ensure this, and (2) if the money pot - were deleted (concurrently), the value is specified to be - considered 0 (aka none) and so we can proceed anyway. */ - if (pots_off > 0) - { - enum GNUNET_DB_QueryStatus qs; - uint64_t pot_missing; - - qs = TMH_db->check_money_pots (TMH_db->cls, - oc->hc->instance->settings.id, - pots_off, - pots, - &pot_missing); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "check_money_pots"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* great, good case! */ - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "All money pots exist\n"); - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - { - char mstr[32]; - - GNUNET_snprintf (mstr, - sizeof (mstr), - "%llu", - (unsigned long long) pot_missing); - reply_with_error (oc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, - mstr); - return; - } - } - } - - /* Populate products from inventory product array and database */ - { - GNUNET_assert (NULL != oc->merge_inventory.products); - for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) - { - struct InventoryProduct *ip - = &oc->parse_request.inventory_products[i]; - struct TALER_MERCHANTDB_ProductDetails pd; - enum GNUNET_DB_QueryStatus qs; - size_t num_categories = 0; - uint64_t *categories = NULL; - - qs = TMH_db->lookup_product (TMH_db->cls, - oc->hc->instance->settings.id, - ip->product_id, - &pd, - &num_categories, - &categories); - if (qs <= 0) - { - enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - unsigned int http_status = 0; - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ec = TALER_EC_GENERIC_DB_FETCH_FAILED; - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Product %s from order unknown\n", - ip->product_id); - http_status = MHD_HTTP_NOT_FOUND; - ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* case listed to make compilers happy */ - GNUNET_assert (0); - } - reply_with_error (oc, - http_status, - ec, - ip->product_id); - return; - } - GNUNET_free (categories); - oc->parse_order.minimum_age - = GNUNET_MAX (oc->parse_order.minimum_age, - pd.minimum_age); - { - const char *eparam; - - if ( (! ip->quantity_missing) && - (ip->quantity > (uint64_t) INT64_MAX) ) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "quantity"); - TALER_MERCHANTDB_product_details_free (&pd); - return; - } - if (GNUNET_OK != - TALER_MERCHANT_vk_process_quantity_inputs ( - TALER_MERCHANT_VK_QUANTITY, - pd.allow_fractional_quantity, - ip->quantity_missing, - (int64_t) ip->quantity, - ip->unit_quantity_missing, - ip->unit_quantity, - &ip->quantity, - &ip->quantity_frac, - &eparam)) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - eparam); - TALER_MERCHANTDB_product_details_free (&pd); - return; - } - } - { - struct TALER_MERCHANT_ProductSold ps = { - .product_id = (char *) ip->product_id, - .product_name = pd.product_name, - .description = pd.description, - .description_i18n = pd.description_i18n, - .unit_quantity.integer = ip->quantity, - .unit_quantity.fractional = ip->quantity_frac, - .prices_length = pd.price_array_length, - .prices = GNUNET_new_array (pd.price_array_length, - struct TALER_Amount), - .prices_are_net = pd.price_is_net, - .image = pd.image, - .taxes = pd.taxes, - .delivery_date = oc->parse_order.delivery_date, - .product_money_pot = pd.money_pot_id, - .unit = pd.unit, - - }; - json_t *p; - char unit_quantity_buf[64]; - - for (size_t j = 0; j<pd.price_array_length; j++) - { - struct TALER_Amount atomic_amount; - - TALER_amount_set_zero (pd.price_array[j].currency, - &atomic_amount); - atomic_amount.fraction = 1; - GNUNET_assert ( - GNUNET_OK == - TALER_MERCHANT_amount_multiply_by_quantity ( - &ps.prices[j], - &pd.price_array[j], - &ps.unit_quantity, - TALER_MERCHANT_ROUND_UP, - &atomic_amount)); - } - - TALER_MERCHANT_vk_format_fractional_string ( - TALER_MERCHANT_VK_QUANTITY, - ip->quantity, - ip->quantity_frac, - sizeof (unit_quantity_buf), - unit_quantity_buf); - if (0 != pd.money_pot_id) - pots[pots_off++] = pd.money_pot_id; - p = TALER_MERCHANT_product_sold_serialize (&ps); - GNUNET_assert (NULL != p); - GNUNET_free (ps.prices); - GNUNET_assert (0 == - json_array_append_new (oc->merge_inventory.products, - p)); - } - TALER_MERCHANTDB_product_details_free (&pd); - } - } - - /* check if final product list is well-formed */ - if (! TMH_products_array_valid (oc->merge_inventory.products)) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order:products"); - return; - } - oc->phase++; -} - - -/* ***************** ORDER_PHASE_PARSE_CHOICES **************** */ - -#ifdef HAVE_DONAU_DONAU_SERVICE_H -/** - * Callback function that is called for each donau instance. - * It simply adds the provided donau_url to the json. - * - * @param cls closure with our `struct TALER_MERCHANT_ContractOutput *` - * @param donau_url the URL of the donau instance - */ -static void -add_donau_url (void *cls, - const char *donau_url) -{ - struct TALER_MERCHANT_ContractOutput *output = cls; - - GNUNET_array_append (output->details.donation_receipt.donau_urls, - output->details.donation_receipt.donau_urls_len, - GNUNET_strdup (donau_url)); -} - - -/** - * Add the donau output to the contract output. - * - * @param oc order context - * @param output contract output to add donau URLs to - */ -static bool -add_donau_output (struct OrderContext *oc, - struct TALER_MERCHANT_ContractOutput *output) -{ - enum GNUNET_DB_QueryStatus qs; - - qs = TMH_db->select_donau_instances_filtered ( - TMH_db->cls, - output->details.donation_receipt.amount.currency, - &add_donau_url, - output); - if (qs < 0) - { - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "donau url parsing db call"); - for (unsigned int i = 0; - i < output->details.donation_receipt.donau_urls_len; - i++) - GNUNET_free (output->details.donation_receipt.donau_urls[i]); - GNUNET_array_grow (output->details.donation_receipt.donau_urls, - output->details.donation_receipt.donau_urls_len, - 0); - return false; - } - return true; -} - - -#endif - -/** - * Parse contract choices. Upon success, continue - * processing with merge_inventory(). - * - * @param[in,out] oc order context - */ -static void -phase_parse_choices (struct OrderContext *oc) -{ - const json_t *jchoices; - - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - oc->phase++; - return; - case TALER_MERCHANT_CONTRACT_VERSION_1: - /* handle below */ - break; - default: - GNUNET_assert (0); - } - - jchoices = oc->parse_order.details.v1.choices; - - if (! json_is_array (jchoices)) - GNUNET_assert (0); - if (0 == json_array_size (jchoices)) - { - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "choices"); - return; - } - GNUNET_array_grow (oc->parse_choices.choices, - oc->parse_choices.choices_len, - json_array_size (jchoices)); - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - struct TALER_MERCHANT_ContractChoice *choice - = &oc->parse_choices.choices[i]; - const char *error_name; - unsigned int error_line; - const json_t *jinputs; - const json_t *joutputs; - bool no_fee; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("amount", - &choice->amount), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("tip", - &choice->tip), - &choice->no_tip), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("max_fee", - &choice->max_fee), - &no_fee), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("description", - &choice->description), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("description_i18n", - &choice->description_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("inputs", - &jinputs), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("outputs", - &joutputs), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - - ret = GNUNET_JSON_parse (json_array_get (jchoices, - i), - spec, - &error_name, - &error_line); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Choice parsing failed: %s:%u\n", - error_name, - error_line); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "choice"); - return; - } - if ( (! no_fee) && - (GNUNET_OK != - TALER_amount_cmp_currency (&choice->amount, - &choice->max_fee)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "different currencies used for 'max_fee' and 'amount' currency"); - return; - } - if ( (! choice->no_tip) && - (GNUNET_OK != - TALER_amount_cmp_currency (&choice->amount, - &choice->tip)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "tip and amount"); - return; - } - - if (! TMH_test_exchange_configured_for_currency ( - choice->amount.currency)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, - choice->amount.currency); - return; - } - - if (NULL != jinputs) - { - const json_t *jinput; - size_t idx; - json_array_foreach ((json_t *) jinputs, idx, jinput) - { - struct TALER_MERCHANT_ContractInput input = { - .details.token.count = 1 - }; - - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_input ((json_t *) jinput, - &input, - idx, - true)) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "input"); - return; - } - - switch (input.type) - { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == input.details.token.count) - continue; - - if (GNUNET_OK != - add_input_token_family (oc, - input.details.token.token_family_slug)) - - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, - input.details.token.token_family_slug); - return; - } - - GNUNET_array_append (choice->inputs, - choice->inputs_len, - input); - continue; - } - GNUNET_assert (0); - } - } - - if (NULL != joutputs) - { - const json_t *joutput; - size_t idx; - json_array_foreach ((json_t *) joutputs, idx, joutput) - { - struct TALER_MERCHANT_ContractOutput output = { - .details.token.count = 1 - }; - - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_output ((json_t *) joutput, - &output, - idx, - true)) - { - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "output"); - return; - } - - switch (output.type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: -#ifdef HAVE_DONAU_DONAU_SERVICE_H - output.details.donation_receipt.amount = choice->amount; - if (! add_donau_output (oc, - &output)) - { - GNUNET_break (0); - return; - } - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); -#endif - continue; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == output.details.token.count) - continue; - - if (0 == output.details.token.valid_at.abs_time.abs_value_us) - output.details.token.valid_at - = GNUNET_TIME_timestamp_get (); - if (GNUNET_OK != - add_output_token_family (oc, - output.details.token.token_family_slug, - output.details.token.valid_at, - &output.details.token.key_index)) - - { - /* note: reply_with_error() was already called */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not handle output token family `%s'\n", - output.details.token.token_family_slug); - return; - } - - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); - continue; - } - GNUNET_assert (0); - } - } - } - oc->phase++; -} - - -/* ***************** ORDER_PHASE_PARSE_ORDER **************** */ - - -/** - * Parse the order field of the request. Upon success, continue - * processing with parse_choices(). - * - * @param[in,out] oc order context - */ -static void -phase_parse_order (struct OrderContext *oc) -{ - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; - const char *merchant_base_url = NULL; - uint64_t version = 0; - const json_t *jmerchant = NULL; - const json_t *products = NULL; - const char *order_id = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("version", - &version), - NULL), - GNUNET_JSON_spec_string ("summary", - &oc->parse_order.summary), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("products", - &products), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("summary_i18n", - &oc->parse_order.summary_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("order_id", - &order_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_message", - &oc->parse_order.fulfillment_message), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("fulfillment_message_i18n", - &oc->parse_order.fulfillment_message_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_url", - &oc->parse_order.fulfillment_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("public_reorder_url", - &oc->parse_order.public_reorder_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("merchant_base_url", - &merchant_base_url), - NULL), - /* For sanity check, this field must NOT be present */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("merchant", - &jmerchant), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("timestamp", - &oc->parse_order.timestamp), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &oc->parse_order.refund_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("pay_deadline", - &oc->parse_order.pay_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &oc->parse_order.wire_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("delivery_location", - &oc->parse_order.delivery_location), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &oc->parse_order.delivery_date), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("minimum_age", - &oc->parse_order.minimum_age), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("auto_refund", - &oc->parse_order.auto_refund), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("extra", - &oc->parse_order.extra), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("order_default_money_pot", - &oc->parse_order.order_default_money_pot), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - bool computed_refund_deadline; - - oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; - oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS; - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - spec); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - if ( (NULL != products) && - (0 != (oc->parse_order.products_len = json_array_size (products))) ) - { - size_t i; - json_t *p; - - oc->parse_order.products - = GNUNET_new_array (oc->parse_order.products_len, - struct TALER_MERCHANT_ProductSold); - json_array_foreach (products, i, p) - { - if (GNUNET_OK != - TALER_MERCHANT_parse_product_sold (p, - &oc->parse_order.products[i])) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order.products"); - return; - } - } - } - if (NULL != order_id) - { - size_t len = strlen (order_id); - - for (size_t i = 0; i<len; i++) - { - char c = order_id[i]; - - if (! ( ( (c >= 'A') && - (c <= 'Z') ) || - ( (c >= 'a') && - (c <= 'z') ) || - ( (c >= '0') && - (c <= '9') ) || - (c == '-') || - (c == '_') || - (c == '.') || - (c == ':') ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Invalid character `%c' in order ID `%s'\n", - c, - order_id); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "Invalid character in order_id"); - return; - } - } - } - switch (version) - { - case 0: - { - bool no_fee; - const json_t *choices = NULL; - struct GNUNET_JSON_Specification specv0[] = { - TALER_JSON_spec_amount_any ( - "amount", - &oc->parse_order.details.v0.brutto), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ( - "tip", - &oc->parse_order.details.v0.tip), - &oc->parse_order.details.v0.no_tip), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ( - "max_fee", - &oc->parse_order.details.v0.max_fee), - &no_fee), - /* for sanity check, must be *absent*! */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("choices", - &choices), - NULL), - GNUNET_JSON_spec_end () - }; - - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - specv0); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - if ( (! no_fee) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, - &oc->parse_order.details.v0.max_fee)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "different currencies used for 'max_fee' and 'amount' currency"); - return; - } - if ( (! oc->parse_order.details.v0.no_tip) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, - &oc->parse_order.details.v0.tip)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "tip and amount"); - return; - } - if (! TMH_test_exchange_configured_for_currency ( - oc->parse_order.details.v0.brutto.currency)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, - oc->parse_order.details.v0.brutto.currency); - return; - } - if (NULL != choices) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, - "choices array must be null for v0 contracts"); - return; - } - oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0; - break; - } - case 1: - { - struct GNUNET_JSON_Specification specv1[] = { - GNUNET_JSON_spec_array_const ( - "choices", - &oc->parse_order.details.v1.choices), - GNUNET_JSON_spec_end () - }; - - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - specv1); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1; - break; - } - default: - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_VERSION_MALFORMED, - "invalid version specified in order, supported are null, '0' or '1'"); - return; - } - - /* Add order_id if it doesn't exist. */ - if (NULL != order_id) - { - oc->parse_order.order_id = GNUNET_strdup (order_id); - } - else - { - char buf[256]; - time_t timer; - struct tm *tm_info; - size_t off; - uint64_t rand; - char *last; - - time (&timer); - tm_info = localtime (&timer); - if (NULL == tm_info) - { - GNUNET_JSON_parse_free (spec); - reply_with_error ( - oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME, - NULL); - return; - } - off = strftime (buf, - sizeof (buf) - 1, - "%Y.%j", - tm_info); - /* Check for error state of strftime */ - GNUNET_assert (0 != off); - buf[off++] = '-'; - rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, - UINT64_MAX); - last = GNUNET_STRINGS_data_to_string (&rand, - sizeof (uint64_t), - &buf[off], - sizeof (buf) - off); - GNUNET_assert (NULL != last); - *last = '\0'; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Assigning order ID `%s' server-side\n", - buf); - - oc->parse_order.order_id = GNUNET_strdup (buf); - GNUNET_assert (NULL != oc->parse_order.order_id); - } - - /* Patch fulfillment URL with order_id (implements #6467). */ - if (NULL != oc->parse_order.fulfillment_url) - { - const char *pos; - - pos = strstr (oc->parse_order.fulfillment_url, - "${ORDER_ID}"); - if (NULL != pos) - { - /* replace ${ORDER_ID} with the real order_id */ - char *nurl; - - /* We only allow one placeholder */ - if (strstr (pos + strlen ("${ORDER_ID}"), - "${ORDER_ID}")) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "fulfillment_url"); - return; - } - - GNUNET_asprintf (&nurl, - "%.*s%s%s", - /* first output URL until ${ORDER_ID} */ - (int) (pos - oc->parse_order.fulfillment_url), - oc->parse_order.fulfillment_url, - /* replace ${ORDER_ID} with the right order_id */ - oc->parse_order.order_id, - /* append rest of original URL */ - pos + strlen ("${ORDER_ID}")); - - oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); - - GNUNET_free (nurl); - } - } - - if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) || - (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) ) - { - oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp ( - settings->default_pay_delay); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Pay deadline was zero (or never), setting to %s\n", - GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); - } - else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST, - NULL); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Pay deadline is %s\n", - GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); - - /* Check soundness of refund deadline, and that a timestamp - * is actually present. */ - { - struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - - /* Add timestamp if it doesn't exist (or is zero) */ - if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time)) - { - oc->parse_order.timestamp = now; - } - - /* If no refund_deadline given, set one based on refund_delay. */ - if (GNUNET_TIME_absolute_is_never ( - oc->parse_order.refund_deadline.abs_time)) - { - if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Refund delay is zero, no refunds are possible for this order\n"); - oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; - } - else - { - computed_refund_deadline = true; - oc->parse_order.refund_deadline - = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (oc->parse_order.pay_deadline.abs_time, - oc->parse_request.refund_delay)); - } - } - - if ( (! GNUNET_TIME_absolute_is_zero ( - oc->parse_order.delivery_date.abs_time)) && - (GNUNET_TIME_absolute_is_past ( - oc->parse_order.delivery_date.abs_time)) ) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST, - NULL); - return; - } - } - - if ( (! GNUNET_TIME_absolute_is_zero ( - oc->parse_order.refund_deadline.abs_time)) && - (GNUNET_TIME_absolute_is_past ( - oc->parse_order.refund_deadline.abs_time)) ) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST, - NULL); - return; - } - - if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) - { - struct GNUNET_TIME_Absolute start; - - start = GNUNET_TIME_absolute_max ( - oc->parse_order.refund_deadline.abs_time, - oc->parse_order.pay_deadline.abs_time); - oc->parse_order.wire_deadline - = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_round_up ( - GNUNET_TIME_absolute_add ( - start, - settings->default_wire_transfer_delay), - settings->default_wire_transfer_rounding_interval)); - if (GNUNET_TIME_absolute_is_never ( - oc->parse_order.wire_deadline.abs_time)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER, - "order:wire_transfer_deadline"); - return; - } - } - else if (computed_refund_deadline) - { - /* if we computed the refund_deadline from default settings - and did have a configured wire_deadline, make sure that - the refund_deadline is at or below the wire_deadline. */ - oc->parse_order.refund_deadline - = GNUNET_TIME_timestamp_min (oc->parse_order.refund_deadline, - oc->parse_order.wire_deadline); - } - if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, - <, - oc->parse_order.refund_deadline)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE, - "order:wire_transfer_deadline;order:refund_deadline"); - return; - } - - if (NULL != merchant_base_url) - { - if (('\0' == *merchant_base_url) || - ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "merchant_base_url is not valid"); - return; - } - oc->parse_order.merchant_base_url - = GNUNET_strdup (merchant_base_url); - } - else - { - char *url; - - url = make_merchant_base_url (oc->connection, - settings->id); - if (NULL == url) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "order:merchant_base_url"); - return; - } - oc->parse_order.merchant_base_url = url; - } - - /* Merchant information must not already be present */ - if (NULL != jmerchant) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "'merchant' field already set, but must be provided by backend"); - return; - } - - if ( (NULL != oc->parse_order.delivery_location) && - (! TMH_location_object_valid (oc->parse_order.delivery_location)) ) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "delivery_location"); - return; - } - - oc->phase++; -} - - -/* ***************** ORDER_PHASE_PARSE_REQUEST **************** */ - -/** - * Parse the client request. Upon success, - * continue processing by calling parse_order(). - * - * @param[in,out] oc order context to process - */ -static void -phase_parse_request (struct OrderContext *oc) -{ - const json_t *ip = NULL; - const json_t *uuid = NULL; - const char *otp_id = NULL; - bool create_token = true; /* default */ - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_json ("order", - &oc->parse_request.order), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("refund_delay", - &oc->parse_request.refund_delay), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("payment_target", - &oc->parse_request.payment_target), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("inventory_products", - &ip), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("session_id", - &oc->parse_request.session_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("lock_uuids", - &uuid), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("create_token", - &create_token), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("otp_id", - &otp_id), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - - oc->parse_request.refund_delay - = oc->hc->instance->settings.default_refund_delay; - ret = TALER_MHD_parse_json_data (oc->connection, - oc->hc->request_body, - spec); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Refund delay is %s\n", - GNUNET_TIME_relative2s (oc->parse_request.refund_delay, - false)); - TMH_db->expire_locks (TMH_db->cls); - if (NULL != otp_id) - { - struct TALER_MERCHANTDB_OtpDeviceDetails td; - enum GNUNET_DB_QueryStatus qs; - - memset (&td, - 0, - sizeof (td)); - qs = TMH_db->select_otp (TMH_db->cls, - oc->hc->instance->settings.id, - otp_id, - &td); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "select_otp"); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "select_otp"); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - reply_with_error (oc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, - otp_id); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - oc->parse_request.pos_key = td.otp_key; - oc->parse_request.pos_algorithm = td.otp_algorithm; - GNUNET_free (td.otp_description); - } - if (create_token) - { - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &oc->parse_request.claim_token, - sizeof (oc->parse_request.claim_token)); - } - /* Compute h_post_data (for idempotency check) */ - { - char *req_body_enc; - - /* Dump normalized JSON to string. */ - if (NULL == (req_body_enc - = json_dumps (oc->hc->request_body, - JSON_ENCODE_ANY - | JSON_COMPACT - | JSON_SORT_KEYS))) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "request body normalization for hashing"); - return; - } - GNUNET_CRYPTO_hash (req_body_enc, - strlen (req_body_enc), - &oc->parse_request.h_post_data.hash); - GNUNET_free (req_body_enc); - } - - /* parse the inventory_products (optionally given) */ - if (NULL != ip) - { - unsigned int ipl = (unsigned int) json_array_size (ip); - - if ( (json_array_size (ip) != (size_t) ipl) || - (ipl > MAX_PRODUCTS) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_products (too many)"); - return; - } - GNUNET_array_grow (oc->parse_request.inventory_products, - oc->parse_request.inventory_products_length, - (unsigned int) json_array_size (ip)); - for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) - { - struct InventoryProduct *ipr = &oc->parse_request.inventory_products[i]; - const char *error_name; - unsigned int error_line; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("product_id", - &ipr->product_id), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("quantity", - &ipr->quantity), - &ipr->quantity_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_quantity", - &ipr->unit_quantity), - &ipr->unit_quantity_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("product_money_pot", - &ipr->product_money_pot), - NULL), - GNUNET_JSON_spec_end () - }; - - ret = GNUNET_JSON_parse (json_array_get (ip, - i), - ispec, - &error_name, - &error_line); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Product parsing failed at #%u: %s:%u\n", - i, - error_name, - error_line); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inventory_products"); - return; - } - if (ipr->quantity_missing && ipr->unit_quantity_missing) - { - ipr->quantity = 1; - ipr->quantity_missing = false; - } - } - } - - /* parse the lock_uuids (optionally given) */ - if (NULL != uuid) - { - GNUNET_array_grow (oc->parse_request.uuids, - oc->parse_request.uuids_length, - json_array_size (uuid)); - for (unsigned int i = 0; i<oc->parse_request.uuids_length; i++) - { - json_t *ui = json_array_get (uuid, - i); - - if (! json_is_string (ui)) - { - GNUNET_break_op (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "UUID parsing failed at #%u\n", - i); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "lock_uuids"); - return; - } - TMH_uuid_from_string (json_string_value (ui), - &oc->parse_request.uuids[i]); - } - } - oc->phase++; -} - - -/* ***************** Main handler **************** */ - - -MHD_RESULT -TMH_private_post_orders ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct OrderContext *oc = hc->ctx; - - if (NULL == oc) - { - oc = GNUNET_new (struct OrderContext); - hc->ctx = oc; - hc->cc = &clean_order; - oc->connection = connection; - oc->hc = hc; - } - while (1) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Processing order in phase %d\n", - oc->phase); - switch (oc->phase) - { - case ORDER_PHASE_PARSE_REQUEST: - phase_parse_request (oc); - break; - case ORDER_PHASE_PARSE_ORDER: - phase_parse_order (oc); - break; - case ORDER_PHASE_PARSE_CHOICES: - phase_parse_choices (oc); - break; - case ORDER_PHASE_MERGE_INVENTORY: - phase_merge_inventory (oc); - break; - case ORDER_PHASE_ADD_PAYMENT_DETAILS: - phase_add_payment_details (oc); - break; - case ORDER_PHASE_SET_EXCHANGES: - if (phase_set_exchanges (oc)) - return MHD_YES; - break; - case ORDER_PHASE_SELECT_WIRE_METHOD: - phase_select_wire_method (oc); - break; - case ORDER_PHASE_SET_MAX_FEE: - phase_set_max_fee (oc); - break; - case ORDER_PHASE_SERIALIZE_ORDER: - phase_serialize_order (oc); - break; - case ORDER_PHASE_CHECK_CONTRACT: - phase_check_contract (oc); - break; - case ORDER_PHASE_SALT_FORGETTABLE: - phase_salt_forgettable (oc); - break; - case ORDER_PHASE_EXECUTE_ORDER: - phase_execute_order (oc); - break; - case ORDER_PHASE_FINISHED_MHD_YES: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Finished processing order (1)\n"); - return MHD_YES; - case ORDER_PHASE_FINISHED_MHD_NO: - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Finished processing order (0)\n"); - return MHD_NO; - } - } -} - - -/* end of taler-merchant-httpd_private-post-orders.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-orders.h b/src/backend/taler-merchant-httpd_private-post-orders.h @@ -1,50 +0,0 @@ -/* - This file is part of TALER - (C) 2014, 2015, 2019 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/> -*/ -/** - * @file taler-merchant-httpd_private-post-orders.h - * @brief headers for POST /orders handler - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_ORDERS_H - -#include "taler-merchant-httpd.h" - - -/** - * Force resuming all suspended orders on shutdown. - */ -void -TMH_force_orders_resume (void); - - -/** - * Generate an order. We add the fields 'exchanges', 'merchant_pub', and - * 'H_wire' to the order gotten from the frontend, as well as possibly other - * fields if the frontend did not provide them. Returns the order_id. - * - * @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_orders (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-otp-devices.c b/src/backend/taler-merchant-httpd_private-post-otp-devices.c @@ -1,199 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-post-otp-devices.c - * @brief implementing POST /otp-devices request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-otp-devices.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Check if the two otp-devices are identical. - * - * @param t1 device to compare - * @param t2 other device to compare - * @return true if they are 'equal', false if not or of payto_uris is not an array - */ -static bool -otp_devices_equal (const struct TALER_MERCHANTDB_OtpDeviceDetails *t1, - const struct TALER_MERCHANTDB_OtpDeviceDetails *t2) -{ - return ( (0 == strcmp (t1->otp_description, - t2->otp_description)) && - (0 == strcmp (t1->otp_key, - t2->otp_key) ) && - (t1->otp_ctr == t2->otp_ctr) && - (t1->otp_algorithm == t2->otp_algorithm) ); -} - - -MHD_RESULT -TMH_private_post_otp_devices (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_OtpDeviceDetails tp = { 0 }; - const char *device_id; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("otp_device_id", - &device_id), - GNUNET_JSON_spec_string ("otp_device_description", - (const char **) &tp.otp_description), - TALER_JSON_spec_otp_type ("otp_algorithm", - &tp.otp_algorithm), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("otp_ctr", - &tp.otp_ctr), - NULL), - TALER_JSON_spec_otp_key ("otp_key", - (const char **) &tp.otp_key), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - { - 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; - } - } - - /* finally, interact with DB until no serialization error */ - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - /* Test if a OTP device of this id is known */ - struct TALER_MERCHANTDB_OtpDeviceDetails etp; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "/post otp-devices")) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->select_otp (TMH_db->cls, - mi->settings.id, - device_id, - &etp); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - /* restart transaction */ - goto retry; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Good, we can proceed! */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* idempotency check: is etp == tp? */ - { - bool eq; - - eq = otp_devices_equal (&tp, - &etp); - GNUNET_free (etp.otp_description); - GNUNET_free (etp.otp_key); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0) - : TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS, - device_id); - } - } /* end switch (qs) */ - - qs = TMH_db->insert_otp (TMH_db->cls, - mi->settings.id, - device_id, - &tp); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - break; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } -retry: - GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - } /* for RETRIES loop */ - GNUNET_JSON_parse_free (spec); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_private-post-otp-devices.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-otp-devices.h b/src/backend/taler-merchant-httpd_private-post-otp-devices.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-post-otp-devices.h - * @brief implementing POST /otp-devices request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_OTP_DEVICES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_OTP_DEVICES_H - -#include "taler-merchant-httpd.h" - - -/** - * Generate an OTP device. - * - * @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_otp_devices (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 @@ -1,91 +0,0 @@ -/* - 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 "taler/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; - 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_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; - } - } - - qs = TMH_db->insert_money_pot (TMH_db->cls, - hc->instance->settings.id, - pot_name, - description, - &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 @@ -1,40 +0,0 @@ -/* - 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-httpd_private-post-products-ID-lock.c b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.c @@ -1,207 +0,0 @@ -/* - This file is part of TALER - (C) 2020, 2021 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-httpd_private-post-products-ID-lock.c - * @brief implementing POST /products/$ID/lock request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-products-ID-lock.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_post_products_ID_lock ( - const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - const char *product_id = hc->infix; - enum GNUNET_DB_QueryStatus qs; - const char *uuids; - struct GNUNET_Uuid uuid; - uint64_t quantity; - bool quantity_missing; - const char *unit_quantity = NULL; - bool unit_quantity_missing = true; - struct GNUNET_TIME_Relative duration; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("lock_uuid", - &uuids), - GNUNET_JSON_spec_relative_time ("duration", - &duration), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("quantity", - &quantity), - &quantity_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_quantity", - &unit_quantity), - &unit_quantity_missing), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - GNUNET_assert (NULL != product_id); - { - enum GNUNET_GenericReturnValue res; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - TMH_uuid_from_string (uuids, - &uuid); - TMH_db->expire_locks (TMH_db->cls); - { - struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; - size_t num_categories; - uint64_t *categories; - uint64_t normalized_quantity = 0; - uint32_t normalized_quantity_frac = 0; - - if (quantity_missing && unit_quantity_missing) - { - quantity = 1; - quantity_missing = false; - } - else if ( (! quantity_missing) && - (quantity > (uint64_t) INT64_MAX) ) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "quantity"); - } - - qs = TMH_db->lookup_product (TMH_db->cls, - mi->settings.id, - product_id, - &pd, - &num_categories, - &categories); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TALER_MERCHANTDB_product_details_free (&pd); - GNUNET_free (categories); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_product"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - TALER_MERCHANTDB_product_details_free (&pd); - GNUNET_free (categories); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "lookup_product"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break_op (0); - TALER_MERCHANTDB_product_details_free (&pd); - GNUNET_free (categories); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN, - product_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - GNUNET_free (categories); - { - const char *eparam; - if (GNUNET_OK != - TALER_MERCHANT_vk_process_quantity_inputs ( - TALER_MERCHANT_VK_QUANTITY, - pd.allow_fractional_quantity, - quantity_missing, - (int64_t) quantity, - unit_quantity_missing, - unit_quantity, - &normalized_quantity, - &normalized_quantity_frac, - &eparam)) - { - TALER_MERCHANTDB_product_details_free (&pd); - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - eparam); - } - } - quantity = normalized_quantity; - qs = TMH_db->lock_product (TMH_db->cls, - mi->settings.id, - product_id, - &uuid, - quantity, - normalized_quantity_frac, - GNUNET_TIME_relative_to_timestamp (duration)); - TALER_MERCHANTDB_product_details_free (&pd); - } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "Serialization error for single-statment request"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_GONE, - TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS, - product_id); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - GNUNET_assert (0); - return MHD_NO; -} - - -/* end of taler-merchant-httpd_private-patch-products-ID-lock.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-products-ID-lock.h b/src/backend/taler-merchant-httpd_private-post-products-ID-lock.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-post-products-ID-lock.h - * @brief implementing POST /products/$ID/lock request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_ID_LOCK_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_ID_LOCK_H -#include "taler-merchant-httpd.h" - - -/** - * Lock an existing product. - * - * @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_products_ID_lock (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c @@ -1,435 +0,0 @@ -/* - This file is part of TALER - (C) 2020-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-httpd_private-post-products.c - * @brief implementing POST /products request handling - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-products.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - -MHD_RESULT -TMH_private_post_products (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_ProductDetails pd = { 0 }; - const json_t *categories = NULL; - const char *product_id; - int64_t total_stock; - const char *unit_total_stock = NULL; - bool unit_total_stock_missing; - bool total_stock_missing; - bool unit_price_missing; - bool unit_allow_fraction; - bool unit_allow_fraction_missing; - uint32_t unit_precision_level; - bool unit_precision_missing; - struct TALER_Amount price; - bool price_missing; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("product_id", - &product_id), - /* new in protocol v20, thus optional for backwards-compatibility */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("product_name", - (const char **) &pd.product_name), - NULL), - GNUNET_JSON_spec_string ("description", - (const char **) &pd.description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("description_i18n", - &pd.description_i18n), - NULL), - GNUNET_JSON_spec_string ("unit", - (const char **) &pd.unit), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("price", - &price), - &price_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("image", - (const char **) &pd.image), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("taxes", - &pd.taxes), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("categories", - &categories), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_total_stock", - &unit_total_stock), - &unit_total_stock_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_int64 ("total_stock", - &total_stock), - &total_stock_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("unit_allow_fraction", - &unit_allow_fraction), - &unit_allow_fraction_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("unit_precision_level", - &unit_precision_level), - &unit_precision_missing), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any_array ("unit_price", - &pd.price_array_length, - &pd.price_array), - &unit_price_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("address", - &pd.address), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("next_restock", - &pd.next_restock), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("minimum_age", - &pd.minimum_age), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("money_pot_id", - &pd.money_pot_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("product_group_id", - &pd.product_group_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("price_is_net", - &pd.price_is_net), - NULL), - GNUNET_JSON_spec_end () - }; - size_t num_cats = 0; - uint64_t *cats = NULL; - bool conflict; - bool no_instance; - ssize_t no_cat; - bool no_group; - bool no_pot; - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT ret; - - GNUNET_assert (NULL != mi); - { - 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; - } - /* For pre-v20 clients, we use the description given as the - product name; remove once we make product_name mandatory. */ - if (NULL == pd.product_name) - pd.product_name = pd.description; - - if (! unit_price_missing) - { - if (! price_missing) - { - if (0 != TALER_amount_cmp (&price, - &pd.price_array[0])) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "price,unit_price mismatch"); - goto cleanup; - } - } - if (GNUNET_OK != - TMH_validate_unit_price_array (pd.price_array, - pd.price_array_length)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_price"); - goto cleanup; - } - } - else - { - if (price_missing) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "price and unit_price missing"); - goto cleanup; - } - pd.price_array = GNUNET_new_array (1, - struct TALER_Amount); - pd.price_array[0] = price; - pd.price_array_length = 1; - } - } - if (! unit_precision_missing) - { - if (unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_precision_level"); - goto cleanup; - } - } - { - bool default_allow_fractional; - uint32_t default_precision_level; - - if (GNUNET_OK != - TMH_unit_defaults_for_instance (mi, - pd.unit, - &default_allow_fractional, - &default_precision_level)) - { - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "unit defaults"); - goto cleanup; - } - if (unit_allow_fraction_missing) - unit_allow_fraction = default_allow_fractional; - if (unit_precision_missing) - unit_precision_level = default_precision_level; - } - if (! unit_allow_fraction) - unit_precision_level = 0; - pd.fractional_precision_level = unit_precision_level; - { - const char *eparam; - - if (GNUNET_OK != - TALER_MERCHANT_vk_process_quantity_inputs ( - TALER_MERCHANT_VK_STOCK, - unit_allow_fraction, - total_stock_missing, - total_stock, - unit_total_stock_missing, - unit_total_stock, - &pd.total_stock, - &pd.total_stock_frac, - &eparam)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - eparam); - goto cleanup; - } - pd.allow_fractional_quantity = unit_allow_fraction; - } - num_cats = json_array_size (categories); - cats = GNUNET_new_array (num_cats, - uint64_t); - { - size_t idx; - json_t *val; - - json_array_foreach (categories, idx, val) - { - if (! json_is_integer (val)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "categories"); - goto cleanup; - } - cats[idx] = json_integer_value (val); - } - } - - if (NULL == pd.address) - pd.address = json_object (); - if (NULL == pd.description_i18n) - pd.description_i18n = json_object (); - if (NULL == pd.taxes) - pd.taxes = json_array (); - - /* check taxes is well-formed */ - if (! TALER_MERCHANT_taxes_array_valid (pd.taxes)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "taxes"); - goto cleanup; - } - - if (! TMH_location_object_valid (pd.address)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "address"); - goto cleanup; - } - - if (! TALER_JSON_check_i18n (pd.description_i18n)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); - goto cleanup; - } - - if (NULL == pd.image) - pd.image = (char *) ""; - if (! TALER_MERCHANT_image_data_url_valid (pd.image)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "image"); - goto cleanup; - } - - qs = TMH_db->insert_product (TMH_db->cls, - mi->settings.id, - product_id, - &pd, - num_cats, - cats, - &no_instance, - &conflict, - &no_cat, - &no_group, - &no_pot); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - NULL); - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - if (no_instance) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } - if (no_group) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_PRODUCT_GROUP_UNKNOWN, - NULL); - goto cleanup; - } - if (no_pot) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_MONEY_POT_UNKNOWN, - NULL); - goto cleanup; - } - if (conflict) - { - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS, - product_id); - goto cleanup; - } - if (-1 != no_cat) - { - char nocats[24]; - - GNUNET_break_op (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_snprintf (nocats, - sizeof (nocats), - "%llu", - (unsigned long long) no_cat); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, - nocats); - goto cleanup; - } - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -cleanup: - GNUNET_JSON_parse_free (spec); - GNUNET_free (pd.price_array); - GNUNET_free (cats); - return ret; -} - - -/* end of taler-merchant-httpd_private-post-products.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-products.h b/src/backend/taler-merchant-httpd_private-post-products.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2020 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-httpd_private-post-products.h - * @brief implementing POST /products request handling - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_PRODUCTS_H -#include "taler-merchant-httpd.h" - - -/** - * Generate a product entry in our inventory. - * - * @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_products (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-reports.c b/src/backend/taler-merchant-httpd_private-post-reports.c @@ -1,147 +0,0 @@ -/* - 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-reports.c - * @brief implementation of POST /private/reports - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-reports.h" -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> - -MHD_RESULT -TMH_private_post_reports (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - const char *description; - const char *program_section; - const char *mime_type; - const char *data_source; - const char *target_address; - struct GNUNET_TIME_Relative frequency; - struct GNUNET_TIME_Relative frequency_shift - = GNUNET_TIME_UNIT_ZERO; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("description", - &description), - GNUNET_JSON_spec_string ("program_section", - &program_section), - GNUNET_JSON_spec_string ("mime_type", - &mime_type), - GNUNET_JSON_spec_string ("data_source", - &data_source), - GNUNET_JSON_spec_string ("target_address", - &target_address), - GNUNET_JSON_spec_relative_time ("report_frequency", - &frequency), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("report_frequency_shift", - &frequency_shift), - NULL), - GNUNET_JSON_spec_end () - }; - uint64_t report_id; - - (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; - } - } - { - char *section; - - /* Check program_section exists in config! */ - GNUNET_asprintf (&section, - "report-generator-%s", - program_section); - if (GNUNET_YES != - GNUNET_CONFIGURATION_have_value (TMH_cfg, - section, - "BINARY")) - { - GNUNET_free (section); - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_MERCHANT_GENERIC_REPORT_GENERATOR_UNCONFIGURED, - program_section); - } - GNUNET_free (section); - } - if ('/' != data_source[0]) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "data_source"); - - } - qs = TMH_db->insert_report (TMH_db->cls, - hc->instance->settings.id, - program_section, - description, - mime_type, - data_source, - target_address, - frequency, - frequency_shift, - &report_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, - "insert_report"); - } - - /* FIXME-Optimization: do trigger inside of transaction above... */ - { - struct GNUNET_DB_EventHeaderP ev = { - .size = htons (sizeof (ev)), - .type = htons (TALER_DBEVENT_MERCHANT_REPORT_UPDATE) - }; - - TMH_db->event_notify (TMH_db->cls, - &ev, - NULL, - 0); - } - - return TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_uint64 ("report_serial_id", - report_id)); -} diff --git a/src/backend/taler-merchant-httpd_private-post-reports.h b/src/backend/taler-merchant-httpd_private-post-reports.h @@ -1,41 +0,0 @@ -/* - 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-reports.h - * @brief HTTP serving layer for creating reports - * @author Christian Grothoff - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_REPORTS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_REPORTS_H - -#include "taler-merchant-httpd.h" - -/** - * Handle POST /private/reports 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_reports (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-templates.c b/src/backend/taler-merchant-httpd_private-post-templates.c @@ -1,252 +0,0 @@ -/* - This file is part of TALER - (C) 2022-2024 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-httpd_private-post-templates.c - * @brief implementing POST /templates request handling - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-templates.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * Check if the two templates are identical. - * - * @param t1 template to compare - * @param t2 other template to compare - * @return true if they are 'equal', false if not or of payto_uris is not an array - */ -static bool -templates_equal (const struct TALER_MERCHANTDB_TemplateDetails *t1, - const struct TALER_MERCHANTDB_TemplateDetails *t2) -{ - return ( (0 == strcmp (t1->template_description, - t2->template_description)) && - ( ( (NULL == t1->otp_id) && - (NULL == t2->otp_id) ) || - ( (NULL != t1->otp_id) && - (NULL != t2->otp_id) && - (0 == strcmp (t1->otp_id, - t2->otp_id))) ) && - ( ( (NULL == t1->editable_defaults) && - (NULL == t2->editable_defaults) ) || - ( (NULL != t1->editable_defaults) && - (NULL != t2->editable_defaults) && - (1 == json_equal (t1->editable_defaults, - t2->editable_defaults))) ) && - (1 == json_equal (t1->template_contract, - t2->template_contract)) ); -} - - -MHD_RESULT -TMH_private_post_templates (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; - const char *template_id; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("template_id", - &template_id), - GNUNET_JSON_spec_string ("template_description", - (const char **) &tp.template_description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("otp_id", - (const char **) &tp.otp_id), - NULL), - GNUNET_JSON_spec_json ("template_contract", - &tp.template_contract), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("editable_defaults", - &tp.editable_defaults), - NULL), - GNUNET_JSON_spec_end () - }; - uint64_t otp_serial = 0; - - GNUNET_assert (NULL != mi); - { - 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; - } - } - if (! TALER_MERCHANT_template_contract_valid (tp.template_contract)) - { - GNUNET_break_op (0); - json_dumpf (tp.template_contract, - stderr, - JSON_INDENT (2)); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "template_contract"); - } - - if (NULL != tp.editable_defaults) - { - const char *key; - json_t *val; - - json_object_foreach (tp.editable_defaults, key, val) - { - if (NULL != - json_object_get (tp.template_contract, - key)) - { - char *msg; - MHD_RESULT ret; - - GNUNET_break_op (0); - GNUNET_asprintf (&msg, - "editable_defaults::%s conflicts with template_contract", - key); - GNUNET_JSON_parse_free (spec); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - msg); - GNUNET_free (msg); - return ret; - } - } - } - - if (NULL != tp.otp_id) - { - qs = TMH_db->select_otp_serial (TMH_db->cls, - mi->settings.id, - tp.otp_id, - &otp_serial); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "select_otp_serial"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - } - - qs = TMH_db->insert_template (TMH_db->cls, - mi->settings.id, - template_id, - otp_serial, - &tp); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - break; - } - - { - /* Test if a template of this id is known */ - struct TALER_MERCHANTDB_TemplateDetails etp; - - qs = TMH_db->lookup_template (TMH_db->cls, - mi->settings.id, - template_id, - &etp); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "logic error"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - /* idempotency check: is etp == tp? */ - { - bool eq; - - eq = templates_equal (&tp, - &etp); - TALER_MERCHANTDB_template_details_free (&etp); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0) - : TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS, - template_id); - } - } -} - - -/* end of taler-merchant-httpd_private-post-templates.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-templates.h b/src/backend/taler-merchant-httpd_private-post-templates.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-post-templates.h - * @brief implementing POST /templates request handling - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_TEMPLATES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_TEMPLATES_H -#include "taler-merchant-httpd.h" - - -/** - * Generate a template entry. - * - * @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_templates (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-token-families.c b/src/backend/taler-merchant-httpd_private-post-token-families.c @@ -1,384 +0,0 @@ -/* - This file is part of TALER - (C) 2023, 2024 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-httpd_private-post-token-families.c - * @brief implementing POST /tokenfamilies request handling - * @author Christian Blättler - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-token-families.h" -#include "taler-merchant-httpd_helper.h" -#include <gnunet/gnunet_time_lib.h> -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Check if the two token families are identical. - * - * @param tf1 token family to compare - * @param tf2 other token family to compare - * @return true if they are 'equal', false if not - */ -static bool -token_families_equal (const struct TALER_MERCHANTDB_TokenFamilyDetails *tf1, - const struct TALER_MERCHANTDB_TokenFamilyDetails *tf2) -{ - /* Note: we're not comparing 'cipher', as that is selected - in the database to some default value and we currently - do not allow the SPA to change it. As a result, it should - always be "NULL" in tf1 and the DB-default in tf2. */ - return ( (0 == strcmp (tf1->slug, - tf2->slug)) && - (0 == strcmp (tf1->name, - tf2->name)) && - (0 == strcmp (tf1->description, - tf2->description)) && - (1 == json_equal (tf1->description_i18n, - tf2->description_i18n)) && - ( (tf1->extra_data == tf2->extra_data) || - (1 == json_equal (tf1->extra_data, - tf2->extra_data)) ) && - (GNUNET_TIME_timestamp_cmp (tf1->valid_after, - ==, - tf2->valid_after)) && - (GNUNET_TIME_timestamp_cmp (tf1->valid_before, - ==, - tf2->valid_before)) && - (GNUNET_TIME_relative_cmp (tf1->duration, - ==, - tf2->duration)) && - (GNUNET_TIME_relative_cmp (tf1->validity_granularity, - ==, - tf2->validity_granularity)) && - (GNUNET_TIME_relative_cmp (tf1->start_offset, - ==, - tf2->start_offset)) && - (tf1->kind == tf2->kind) ); -} - - -MHD_RESULT -TMH_private_post_token_families (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 }; - const char *kind = NULL; - bool no_valid_after = false; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("slug", - (const char **) &details.slug), - GNUNET_JSON_spec_string ("name", - (const char **) &details.name), - GNUNET_JSON_spec_string ("description", - (const char **) &details.description), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("description_i18n", - &details.description_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("extra_data", - &details.extra_data), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("valid_after", - &details.valid_after), - &no_valid_after), - GNUNET_JSON_spec_timestamp ("valid_before", - &details.valid_before), - GNUNET_JSON_spec_relative_time ("duration", - &details.duration), - GNUNET_JSON_spec_relative_time ("validity_granularity", - &details.validity_granularity), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("start_offset", - &details.start_offset), - NULL), - GNUNET_JSON_spec_string ("kind", - &kind), - GNUNET_JSON_spec_end () - }; - struct GNUNET_TIME_Timestamp now - = GNUNET_TIME_timestamp_get (); - - GNUNET_assert (NULL != mi); - { - 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; - } - } - if (no_valid_after) - details.valid_after = now; - - /* Ensure that valid_after is before valid_before */ - if (GNUNET_TIME_timestamp_cmp (details.valid_after, - >=, - details.valid_before)) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "valid_after >= valid_before"); - } - - /* Ensure that duration exceeds rounding plus start_offset */ - if (GNUNET_TIME_relative_cmp (details.duration, - <, - GNUNET_TIME_relative_add (details. - validity_granularity, - details.start_offset)) - ) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "duration below validity_granularity plus start_offset"); - } - - if (0 == - strcmp (kind, - "discount")) - details.kind = TALER_MERCHANTDB_TFK_Discount; - else if (0 == - strcmp (kind, - "subscription")) - details.kind = TALER_MERCHANTDB_TFK_Subscription; - else - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "kind"); - } - - if (NULL == details.description_i18n) - details.description_i18n = json_object (); - - if (! TALER_JSON_check_i18n (details.description_i18n)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "description_i18n"); - } - - if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, - !=, - details.validity_granularity) && - GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply ( - GNUNET_TIME_UNIT_DAYS, - 90), - !=, - details.validity_granularity) && - GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, - !=, - details.validity_granularity) && - GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS, - !=, - details.validity_granularity) && - GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, - !=, - details.validity_granularity) && - GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, - !=, - details.validity_granularity) && - GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, - !=, - details.validity_granularity) - ) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Received invalid validity_granularity value: %s\n", - GNUNET_STRINGS_relative_time_to_string (details. - validity_granularity, - false)); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "validity_granularity"); - } - - /* finally, interact with DB until no serialization error */ - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - /* Test if a token family of this id is known */ - struct TALER_MERCHANTDB_TokenFamilyDetails existing; - - TMH_db->preflight (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "/post tokenfamilies")) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->insert_token_family (TMH_db->cls, - mi->settings.id, - details.slug, - &details); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "insert_token_family returned %d\n", - (int) qs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_token_family"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - qs = TMH_db->lookup_token_family (TMH_db->cls, - mi->settings.id, - details.slug, - &existing); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "lookup_token_family returned %d\n", - (int) qs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup_token_family"); - case GNUNET_DB_STATUS_SOFT_ERROR: - TMH_db->rollback (TMH_db->cls); - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "lookup_token_family failed after insert_token_family failed"); - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - { - bool eq; - - eq = token_families_equal (&details, - &existing); - TALER_MERCHANTDB_token_family_details_free (&existing); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_reply_static ( - connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0) - : TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT, - details.slug); - } - } - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - qs = TMH_db->commit (TMH_db->cls); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "insert_token_family"); - case GNUNET_DB_STATUS_SOFT_ERROR: - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - break; - } - if (GNUNET_DB_STATUS_SOFT_ERROR == qs) - TMH_db->rollback (TMH_db->cls); - else - break; - } /* for(i... MAX_RETRIES) */ - - GNUNET_JSON_parse_free (spec); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_private-post-token-families.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-token-families.h b/src/backend/taler-merchant-httpd_private-post-token-families.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2023 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-httpd_private-post-token-families.h - * @brief implementing POST /tokenfamilies request handling - * @author Christian Blättler - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_TOKEN_FAMILIES_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_TOKEN_FAMILIES_H -#include "taler-merchant-httpd.h" - - -/** - * Create a new token family. - * - * @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_token_families (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-transfers.c b/src/backend/taler-merchant-httpd_private-post-transfers.c @@ -1,144 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2023, 2025, 2026 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-httpd_private-post-transfers.c - * @brief implement API for registering wire transfers - * @author Marcello Stanisci - * @author Christian Grothoff - */ -#include "taler/platform.h" -#include <jansson.h> -#include <taler/taler_signatures.h> -#include <taler/taler_json_lib.h> -#include <taler/taler_dbevents.h> -#include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_helper.h" -#include "taler-merchant-httpd_private-post-transfers.h" - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 5 - - -MHD_RESULT -TMH_private_post_transfers (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TALER_FullPayto payto_uri; - const char *exchange_url; - struct TALER_WireTransferIdentifierRawP wtid; - struct TALER_Amount amount; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("credit_amount", - &amount), - GNUNET_JSON_spec_fixed_auto ("wtid", - &wtid), - TALER_JSON_spec_full_payto_uri ("payto_uri", - &payto_uri), - TALER_JSON_spec_web_url ("exchange_url", - &exchange_url), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - enum GNUNET_DB_QueryStatus qs; - bool no_instance; - bool no_account; - bool conflict; - - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - if (GNUNET_OK != res) - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "New inbound wire transfer over %s to %s from %s\n", - TALER_amount2s (&amount), - payto_uri.full_payto, - exchange_url); - - /* Check if transfer data is in database, if not, add it. */ - qs = TMH_db->insert_transfer (TMH_db->cls, - hc->instance->settings.id, - exchange_url, - &wtid, - &amount, - payto_uri, - 0 /* no bank serial known! */, - &no_instance, - &no_account, - &conflict); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_transfer"); - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_transfer"); - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_assert (0); /* should be impossible */ - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - if (no_instance) - { - /* should be only possible if instance was concurrently deleted, - that's so theoretical we rather log as error... */ - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->instance->settings.id); - } - if (no_account) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_ACCOUNT_UNKNOWN, - payto_uri.full_payto); - } - if (conflict) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION, - NULL); - } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_private-post-transfers.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-transfers.h b/src/backend/taler-merchant-httpd_private-post-transfers.h @@ -1,44 +0,0 @@ -/* - This file is part of TALER - (C) 2014-2023 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/> -*/ -/** - * @file taler-merchant-httpd_private-post-transfers.h - * @brief headers for /track/transfer handler - * @author Christian Grothoff - * @author Marcello Stanisci - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_TRANSFERS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_TRANSFERS_H -#include <microhttpd.h> -#include "taler-merchant-httpd.h" - - -/** - * Manages a POST /private/transfers call. It calls the GET /transfers/$WTID - * offered by the exchange in order to obtain the set of transfers - * (of coins) associated with a given wire transfer. - * - * @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_transfers (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - - -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-units.c b/src/backend/taler-merchant-httpd_private-post-units.c @@ -1,219 +0,0 @@ -/* - 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-httpd_private-post-units.c - * @brief implement POST /private/units - * @author Bohdan Potuzhnyi - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-units.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -MHD_RESULT -TMH_private_post_units (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_UnitDetails nud = { 0 }; - bool allow_fraction_missing = true; - bool unit_precision_missing = true; - bool unit_active_missing = true; - enum GNUNET_GenericReturnValue res; - enum GNUNET_DB_QueryStatus qs; - MHD_RESULT ret; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("unit", - (const char **) &nud.unit), - GNUNET_JSON_spec_string ("unit_name_long", - (const char **) &nud.unit_name_long), - GNUNET_JSON_spec_string ("unit_name_short", - (const char **) &nud.unit_name_short), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("unit_name_long_i18n", - &nud.unit_name_long_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_json ("unit_name_short_i18n", - &nud.unit_name_short_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("unit_allow_fraction", - &nud.unit_allow_fraction), - &allow_fraction_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("unit_precision_level", - &nud.unit_precision_level), - &unit_precision_missing), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("unit_active", - &nud.unit_active), - &unit_active_missing), - GNUNET_JSON_spec_end () - }; - - - GNUNET_assert (NULL != mi); - res = TALER_MHD_parse_json_data (connection, - hc->request_body, - spec); - (void) rh; - - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - return (GNUNET_NO == res) - ? MHD_YES - : MHD_NO; - } - - if (allow_fraction_missing) - { - nud.unit_allow_fraction = false; - nud.unit_precision_level = 0; - } - else - { - if (! nud.unit_allow_fraction) - { - nud.unit_precision_level = 0; - unit_precision_missing = false; - } - else if (unit_precision_missing) - { - nud.unit_precision_level = 0; - } - } - if (nud.unit_precision_level > TMH_MAX_FRACTIONAL_PRECISION_LEVEL) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_precision_level"); - goto cleanup; - } - if (unit_active_missing) - nud.unit_active = true; - - if (NULL == nud.unit_name_long_i18n) - nud.unit_name_long_i18n = json_object (); - if (NULL == nud.unit_name_short_i18n) - nud.unit_name_short_i18n = json_object (); - - if (! TALER_JSON_check_i18n (nud.unit_name_long_i18n)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_name_long_i18n"); - goto cleanup; - } - if (! TALER_JSON_check_i18n (nud.unit_name_short_i18n)) - { - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "unit_name_short_i18n"); - goto cleanup; - } - - nud.unit_builtin = false; - - { - bool no_instance = false; - bool conflict = false; - uint64_t unit_serial = 0; - - qs = TMH_db->insert_unit (TMH_db->cls, - mi->settings.id, - &nud, - &no_instance, - &conflict, - &unit_serial); - - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_STORE_FAILED, - NULL); - goto cleanup; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "insert_unit"); - goto cleanup; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - } - - if (no_instance) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - mi->settings.id); - goto cleanup; - } - if (conflict) - { - ret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_UNIT_BUILTIN, - nud.unit); - goto cleanup; - } - - ret = TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); - } - -cleanup: - if (NULL != nud.unit_name_long_i18n) - { - json_decref (nud.unit_name_long_i18n); - nud.unit_name_long_i18n = NULL; - } - if (NULL != nud.unit_name_short_i18n) - { - json_decref (nud.unit_name_short_i18n); - nud.unit_name_short_i18n = NULL; - } - GNUNET_JSON_parse_free (spec); - return ret; -} - - -/* end of taler-merchant-httpd_private-post-units.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-units.h b/src/backend/taler-merchant-httpd_private-post-units.h @@ -1,33 +0,0 @@ -/* - 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-httpd_private-post-units.h - * @brief implement POST /private/units - * @author Bohdan Potuzhnyi - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_UNITS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_UNITS_H - -#include "taler-merchant-httpd.h" - - -MHD_RESULT -TMH_private_post_units (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -/* end of taler-merchant-httpd_private-post-units.h */ -#endif diff --git a/src/backend/taler-merchant-httpd_private-post-webhooks.c b/src/backend/taler-merchant-httpd_private-post-webhooks.c @@ -1,215 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-post-webhooks.c - * @brief implementing POST /webhooks request handling - * @author Priscilla HUANG - */ -#include "taler/platform.h" -#include "taler-merchant-httpd_private-post-webhooks.h" -#include "taler-merchant-httpd_helper.h" -#include <taler/taler_json_lib.h> - - -/** - * How often do we retry the simple INSERT database transaction? - */ -#define MAX_RETRIES 3 - - -/** - * Check if the two webhooks are identical. - * - * @param w1 webhook to compare - * @param w2 other webhook to compare - * @return true if they are 'equal', false if not or of payto_uris is not an array - */ -static bool -webhooks_equal (const struct TALER_MERCHANTDB_WebhookDetails *w1, - const struct TALER_MERCHANTDB_WebhookDetails *w2) -{ - return ( (0 == strcmp (w1->event_type, - w2->event_type)) && - (0 == strcmp (w1->url, - w2->url)) && - (0 == strcmp (w1->http_method, - w2->http_method)) && - ( ( (NULL == w1->header_template) && - (NULL == w2->header_template) ) || - ( (NULL != w1->header_template) && - (NULL != w2->header_template) && - (0 == strcmp (w1->header_template, - w2->header_template)) ) ) && - ( ( (NULL == w1->body_template) && - (NULL == w2->body_template) ) || - ( (NULL != w1->body_template) && - (NULL != w2->body_template) && - (0 == strcmp (w1->body_template, - w2->body_template)) ) ) ); -} - - -MHD_RESULT -TMH_private_post_webhooks (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) -{ - struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_WebhookDetails wb = { 0 }; - const char *webhook_id; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("webhook_id", - &webhook_id), - GNUNET_JSON_spec_string ("event_type", - (const char **) &wb.event_type), - TALER_JSON_spec_web_url ("url", - (const char **) &wb.url), - GNUNET_JSON_spec_string ("http_method", - (const char **) &wb.http_method), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("header_template", - (const char **) &wb.header_template), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("body_template", - (const char **) &wb.body_template), - NULL), - GNUNET_JSON_spec_end () - }; - - GNUNET_assert (NULL != mi); - { - 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; - } - } - - - /* finally, interact with DB until no serialization error */ - for (unsigned int i = 0; i<MAX_RETRIES; i++) - { - /* Test if a webhook of this id is known */ - struct TALER_MERCHANTDB_WebhookDetails ewb; - - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "/post webhooks")) - { - GNUNET_break (0); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_START_FAILED, - NULL); - } - qs = TMH_db->lookup_webhook (TMH_db->cls, - mi->settings.id, - webhook_id, - &ewb); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - /* Clean up and fail hard */ - GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL); - case GNUNET_DB_STATUS_SOFT_ERROR: - /* restart transaction */ - goto retry; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Good, we can proceed! */ - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* idempotency check: is ewb == wb? */ - { - bool eq; - - eq = webhooks_equal (&wb, - &ewb); - TALER_MERCHANTDB_webhook_details_free (&ewb); - TMH_db->rollback (TMH_db->cls); - GNUNET_JSON_parse_free (spec); - return eq - ? TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0) - : TALER_MHD_reply_with_error (connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS, - webhook_id); - } - } /* end switch (qs) */ - - qs = TMH_db->insert_webhook (TMH_db->cls, - mi->settings.id, - webhook_id, - &wb); - if (GNUNET_DB_STATUS_HARD_ERROR == qs) - { - TMH_db->rollback (TMH_db->cls); - break; - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - qs = TMH_db->commit (TMH_db->cls); - if (GNUNET_DB_STATUS_SOFT_ERROR != qs) - break; - } -retry: - GNUNET_assert (GNUNET_DB_STATUS_SOFT_ERROR == qs); - TMH_db->rollback (TMH_db->cls); - } /* for RETRIES loop */ - GNUNET_JSON_parse_free (spec); - if (qs < 0) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - } - return TALER_MHD_reply_static (connection, - MHD_HTTP_NO_CONTENT, - NULL, - NULL, - 0); -} - - -/* end of taler-merchant-httpd_private-post-webhooks.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-webhooks.h b/src/backend/taler-merchant-httpd_private-post-webhooks.h @@ -1,43 +0,0 @@ -/* - This file is part of TALER - (C) 2022 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-httpd_private-post-webhooks.h - * @brief implementing POST /webhooks request handling - * @author Priscilla HUANG - */ -#ifndef TALER_MERCHANT_HTTPD_PRIVATE_POST_WEBHOOKS_H -#define TALER_MERCHANT_HTTPD_PRIVATE_POST_WEBHOOKS_H -#include "taler-merchant-httpd.h" - - -/** - * Generate a webhook entry. - * - * @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_webhooks (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); - -#endif