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:
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 (®,
+ "^[A-Za-z0-9][A-Za-z0-9_.@-]+$",
+ REG_EXTENDED));
+ }
+
+ if (0 != regexec (®,
+ 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",
- ×tamp),
- 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",
+ ×tamp),
+ 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",
+ ×tamp),
+ 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,
+ ×tamp,
+ &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,
+ ×tamp,
+ &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 (§ion,
+ "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 (®,
- "^[A-Za-z0-9][A-Za-z0-9_.@-]+$",
- REG_EXTENDED));
- }
-
- if (0 != regexec (®,
- 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",
- ×tamp),
- 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,
- ×tamp,
- &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,
- ×tamp,
- &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 (§ion,
- "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