commit 78f6a9b0d23a97d75b48581e64c23016432588d1
parent ac4c7c1baeb1c3fb0cd5b94f7440b5817a5203cb
Author: Florian Dold <dold@taler.net>
Date: Fri, 28 Aug 2026 14:49:36 +0200
templates: apply template pay duration to orders
Issue: https://bugs.taler.net/n/11757
Diffstat:
9 files changed, 322 insertions(+), 1 deletion(-)
diff --git a/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c
@@ -2165,6 +2165,8 @@ create_using_templates_paivana (struct UseContext *uc)
static void
handle_phase_create_order (struct UseContext *uc)
{
+ json_t *order;
+
GNUNET_assert (NULL == uc->ihc.request_body);
switch (uc->template_type)
{
@@ -2180,6 +2182,33 @@ handle_phase_create_order (struct UseContext *uc)
case TALER_MERCHANT_TEMPLATE_TYPE_INVALID:
GNUNET_assert (0);
}
+ order = json_object_get (uc->ihc.request_body,
+ "order");
+ GNUNET_assert (json_is_object (order));
+ /* A zero duration means that the template did not specify one. Leave the
+ deadline absent in that case so the regular order handler applies the
+ instance default. Older backends accepted FOREVER; also use the instance
+ default for such legacy templates instead of generating a forbidden
+ "never" deadline. */
+ if (GNUNET_TIME_relative_is_forever (
+ uc->template_contract.pay_duration))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Template uses invalid infinite pay duration; "
+ "using instance default\n");
+ }
+ else if (! GNUNET_TIME_relative_is_zero (
+ uc->template_contract.pay_duration))
+ {
+ GNUNET_assert (
+ 0 ==
+ json_object_set_new (
+ order,
+ "pay_deadline",
+ GNUNET_JSON_from_timestamp (
+ GNUNET_TIME_relative_to_timestamp (
+ uc->template_contract.pay_duration))));
+ }
uc->phase++;
}
diff --git a/src/include/taler/taler_merchant_testing_lib.h b/src/include/taler/taler_merchant_testing_lib.h
@@ -2039,6 +2039,40 @@ TALER_TESTING_cmd_merchant_post_using_templates (
struct GNUNET_TIME_Timestamp pay_deadline,
unsigned int http_status);
+
+/**
+ * Define a "POST /using-templates" command and check the resulting pay
+ * deadline against an explicitly expected duration. This is useful for
+ * checking the instance-default fallback when the template omits its pay
+ * duration.
+ *
+ * @param label command label
+ * @param template_ref label of command that created the template to use
+ * @param otp_ref label of command that created OTP device, or NULL
+ * @param merchant_url base URL of the merchant
+ * @param using_template_id template ID to use
+ * @param summary summary supplied by the customer
+ * @param amount amount supplied by the customer
+ * @param refund_deadline refund deadline to use for the contract
+ * @param pay_deadline pay deadline to use for the contract
+ * @param expected_pay_duration expected duration until the pay deadline
+ * @param http_status expected HTTP response code
+ * @return the command
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_post_using_templates_with_expected_pay_duration (
+ const char *label,
+ const char *template_ref,
+ const char *otp_ref,
+ const char *merchant_url,
+ const char *using_template_id,
+ const char *summary,
+ const char *amount,
+ struct GNUNET_TIME_Timestamp refund_deadline,
+ struct GNUNET_TIME_Timestamp pay_deadline,
+ struct GNUNET_TIME_Relative expected_pay_duration,
+ unsigned int http_status);
+
/**
* Define a "POST /using-templates" CMD with a raw JSON request body.
*
diff --git a/src/include/taler/taler_merchant_util.h b/src/include/taler/taler_merchant_util.h
@@ -1054,7 +1054,8 @@ struct TALER_MERCHANT_TemplateContract
/**
* How long does the customer have to pay for the order.
- * 0 if not specified (use instance default).
+ * 0 if not specified (use instance default). FOREVER is invalid for new
+ * templates; it can occur only when reading legacy data.
*/
struct GNUNET_TIME_Relative pay_duration;
diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c
@@ -1820,6 +1820,17 @@ run (void *cls,
"template-1",
MHD_HTTP_OK,
"post-templates-t1"),
+ TALER_TESTING_cmd_merchant_post_templates2 (
+ "post-templates-pay-forever",
+ merchant_url,
+ "template-pay-forever",
+ "template with an invalid infinite pay duration",
+ NULL,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_uint64 ("minimum_age", 0),
+ GNUNET_JSON_pack_time_rel ("pay_duration",
+ GNUNET_TIME_UNIT_FOREVER_REL)),
+ MHD_HTTP_BAD_REQUEST),
TALER_TESTING_cmd_merchant_post_templates ("post-templates-t2",
merchant_url,
"template-2",
@@ -1841,6 +1852,23 @@ run (void *cls,
"template-2",
MHD_HTTP_OK,
"patch-templates-t2"),
+ TALER_TESTING_cmd_merchant_patch_template (
+ "patch-templates-t2-pay-forever",
+ merchant_url,
+ "template-2",
+ "template with an invalid infinite pay duration",
+ NULL,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_uint64 ("minimum_age", 0),
+ GNUNET_JSON_pack_time_rel ("pay_duration",
+ GNUNET_TIME_UNIT_FOREVER_REL)),
+ MHD_HTTP_BAD_REQUEST),
+ TALER_TESTING_cmd_merchant_get_template (
+ "get-template-t2-after-pay-forever",
+ merchant_url,
+ "template-2",
+ MHD_HTTP_OK,
+ "patch-templates-t2"),
TALER_TESTING_cmd_merchant_get_template ("get-template-nx",
merchant_url,
"template-nx",
@@ -1910,6 +1938,60 @@ run (void *cls,
GNUNET_JSON_pack_string ("amount",
"EUR:4")),
MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_templates2 (
+ "post-templates-pay-default-omitted",
+ merchant_url,
+ "template-pay-default-omitted",
+ "template without a pay duration",
+ NULL,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_uint64 ("minimum_age", 0),
+ GNUNET_JSON_pack_string ("summary",
+ "use the instance pay delay"),
+ GNUNET_JSON_pack_string ("amount",
+ "EUR:1")),
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_using_templates_with_expected_pay_duration (
+ "using-templates-pay-default-omitted",
+ "post-templates-pay-default-omitted",
+ NULL,
+ merchant_url,
+ "pay-default-omitted",
+ NULL,
+ NULL,
+ GNUNET_TIME_UNIT_ZERO_TS,
+ GNUNET_TIME_UNIT_FOREVER_TS,
+ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
+ 2),
+ MHD_HTTP_OK),
+ TALER_TESTING_cmd_merchant_post_templates2 (
+ "post-templates-pay-default-zero",
+ merchant_url,
+ "template-pay-default-zero",
+ "template with a zero pay duration",
+ NULL,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_uint64 ("minimum_age", 0),
+ GNUNET_JSON_pack_time_rel ("pay_duration",
+ GNUNET_TIME_UNIT_ZERO),
+ GNUNET_JSON_pack_string ("summary",
+ "use the instance pay delay"),
+ GNUNET_JSON_pack_string ("amount",
+ "EUR:1")),
+ MHD_HTTP_NO_CONTENT),
+ TALER_TESTING_cmd_merchant_post_using_templates_with_expected_pay_duration (
+ "using-templates-pay-default-zero",
+ "post-templates-pay-default-zero",
+ NULL,
+ merchant_url,
+ "pay-default-zero",
+ NULL,
+ NULL,
+ GNUNET_TIME_UNIT_ZERO_TS,
+ GNUNET_TIME_UNIT_FOREVER_TS,
+ GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
+ 2),
+ MHD_HTTP_OK),
TALER_TESTING_cmd_merchant_post_using_templates (
"using-templates-t1",
"post-templates-t1",
diff --git a/src/testing/testing_api_cmd_patch_template.c b/src/testing/testing_api_cmd_patch_template.c
@@ -108,6 +108,8 @@ patch_template_cb (struct PatchTemplateState *pis,
{
case MHD_HTTP_NO_CONTENT:
break;
+ case MHD_HTTP_BAD_REQUEST:
+ break;
case MHD_HTTP_UNAUTHORIZED:
break;
case MHD_HTTP_FORBIDDEN:
diff --git a/src/testing/testing_api_cmd_post_templates.c b/src/testing/testing_api_cmd_post_templates.c
@@ -105,6 +105,8 @@ post_templates_cb (struct PostTemplatesState *tis,
{
case MHD_HTTP_NO_CONTENT:
break;
+ case MHD_HTTP_BAD_REQUEST:
+ break;
case MHD_HTTP_UNAUTHORIZED:
break;
case MHD_HTTP_FORBIDDEN:
diff --git a/src/testing/testing_api_cmd_post_using_templates.c b/src/testing/testing_api_cmd_post_using_templates.c
@@ -132,6 +132,31 @@ struct PostUsingTemplatesState
struct TALER_ClaimTokenP claim_token;
/**
+ * Pay deadline returned when the template was instantiated.
+ */
+ struct GNUNET_TIME_Timestamp pay_deadline;
+
+ /**
+ * Time when the template instantiation request was started.
+ */
+ struct GNUNET_TIME_Absolute request_start;
+
+ /**
+ * Pay duration configured in the template.
+ */
+ struct GNUNET_TIME_Relative pay_duration;
+
+ /**
+ * Whether @e pay_duration is finite and non-zero and should be checked.
+ */
+ bool check_pay_duration;
+
+ /**
+ * Whether @e pay_duration was explicitly supplied as the expected duration.
+ */
+ bool have_expected_pay_duration;
+
+ /**
* Should the command also CLAIM the order?
*/
bool with_claim;
@@ -178,9 +203,12 @@ using_claim_cb (struct PostUsingTemplatesState *tis,
{
const char *error_name;
unsigned int error_line;
+ struct GNUNET_TIME_Timestamp pay_deadline;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("merchant_pub",
&tis->merchant_pub),
+ GNUNET_JSON_spec_timestamp ("pay_deadline",
+ &pay_deadline),
GNUNET_JSON_spec_end ()
};
@@ -228,6 +256,14 @@ using_claim_cb (struct PostUsingTemplatesState *tis,
free (log);
TALER_TESTING_FAIL (tis->is);
}
+ if (GNUNET_TIME_timestamp_cmp (pay_deadline,
+ !=,
+ tis->pay_deadline))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Pay deadline in claimed contract does not match template instantiation response\n");
+ TALER_TESTING_FAIL (tis->is);
+ }
TALER_TESTING_interpreter_next (tis->is);
}
@@ -264,6 +300,33 @@ post_using_templates_cb (struct PostUsingTemplatesState *tis,
switch (por->hr.http_status)
{
case MHD_HTTP_OK:
+ tis->pay_deadline = por->details.ok.pay_deadline;
+ if (tis->check_pay_duration)
+ {
+ struct GNUNET_TIME_Absolute earliest;
+ struct GNUNET_TIME_Absolute latest;
+
+ earliest = GNUNET_TIME_absolute_subtract (
+ GNUNET_TIME_absolute_add (tis->request_start,
+ tis->pay_duration),
+ GNUNET_TIME_UNIT_SECONDS);
+ latest = GNUNET_TIME_absolute_add (
+ GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (),
+ tis->pay_duration),
+ GNUNET_TIME_UNIT_SECONDS);
+ if ( (GNUNET_TIME_absolute_cmp (tis->pay_deadline.abs_time,
+ <,
+ earliest)) ||
+ (GNUNET_TIME_absolute_cmp (tis->pay_deadline.abs_time,
+ >,
+ latest)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Template pay deadline does not match configured pay duration\n");
+ TALER_TESTING_interpreter_fail (tis->is);
+ return;
+ }
+ }
if (NULL != por->details.ok.token)
tis->claim_token = *por->details.ok.token;
tis->order_id = GNUNET_strdup (por->details.ok.order_id);
@@ -381,6 +444,7 @@ post_using_templates_run (void *cls,
struct PostUsingTemplatesState *tis = cls;
const struct TALER_TESTING_Command *ref;
const char *template_id;
+ const json_t *template_contract;
tis->is = is;
ref = TALER_TESTING_interpreter_lookup_command (is,
@@ -389,6 +453,35 @@ post_using_templates_run (void *cls,
TALER_TESTING_get_trait_template_id (ref,
&template_id))
TALER_TESTING_FAIL (is);
+ if (GNUNET_OK !=
+ TALER_TESTING_get_trait_template_contract (ref,
+ &template_contract))
+ TALER_TESTING_FAIL (is);
+ if (tis->have_expected_pay_duration)
+ {
+ tis->check_pay_duration
+ = (! GNUNET_TIME_relative_is_zero (tis->pay_duration)) &&
+ (! GNUNET_TIME_relative_is_forever (tis->pay_duration));
+ }
+ else if (NULL !=
+ json_object_get (template_contract,
+ "pay_duration"))
+ {
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_relative_time ("pay_duration",
+ &tis->pay_duration),
+ GNUNET_JSON_spec_end ()
+ };
+
+ GNUNET_assert (GNUNET_OK ==
+ GNUNET_JSON_parse ((json_t *) template_contract,
+ spec,
+ NULL,
+ NULL));
+ tis->check_pay_duration
+ = (! GNUNET_TIME_relative_is_zero (tis->pay_duration)) &&
+ (! GNUNET_TIME_relative_is_forever (tis->pay_duration));
+ }
if (NULL != tis->otp_ref)
{
ref = TALER_TESTING_interpreter_lookup_command (is,
@@ -432,6 +525,7 @@ post_using_templates_run (void *cls,
{
enum TALER_ErrorCode ec;
+ tis->request_start = GNUNET_TIME_absolute_get ();
ec = TALER_MERCHANT_post_templates_start (tis->iph,
&post_using_templates_cb,
tis);
@@ -654,6 +748,41 @@ TALER_TESTING_cmd_merchant_post_using_templates (
struct TALER_TESTING_Command
+TALER_TESTING_cmd_merchant_post_using_templates_with_expected_pay_duration (
+ const char *label,
+ const char *template_ref,
+ const char *otp_ref,
+ const char *merchant_url,
+ const char *using_template_id,
+ const char *summary,
+ const char *amount,
+ struct GNUNET_TIME_Timestamp refund_deadline,
+ struct GNUNET_TIME_Timestamp pay_deadline,
+ struct GNUNET_TIME_Relative expected_pay_duration,
+ unsigned int http_status)
+{
+ struct TALER_TESTING_Command cmd;
+ struct PostUsingTemplatesState *tis;
+
+ cmd = TALER_TESTING_cmd_merchant_post_using_templates (
+ label,
+ template_ref,
+ otp_ref,
+ merchant_url,
+ using_template_id,
+ summary,
+ amount,
+ refund_deadline,
+ pay_deadline,
+ http_status);
+ tis = cmd.cls;
+ tis->pay_duration = expected_pay_duration;
+ tis->have_expected_pay_duration = true;
+ return cmd;
+}
+
+
+struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_post_using_templates2 (
const char *label,
const char *template_ref,
diff --git a/src/util/template_parse.c b/src/util/template_parse.c
@@ -482,6 +482,9 @@ TALER_MERCHANT_template_contract_valid (const json_t *template_contract)
TALER_MERCHANT_template_contract_parse (template_contract,
&tmp,
NULL));
+ if (ret &&
+ GNUNET_TIME_relative_is_forever (tmp.pay_duration))
+ ret = false;
TALER_MERCHANT_template_contract_free (&tmp);
return ret;
}
diff --git a/src/util/test_contract.c b/src/util/test_contract.c
@@ -243,5 +243,44 @@ main (int argc,
json_decref (v1);
}
+ { // Template pay duration validation
+ json_t *finite;
+ json_t *zero;
+ json_t *omitted;
+ json_t *forever;
+ struct TALER_MERCHANT_TemplateContract parsed = { 0 };
+
+ finite = json_pack ("{s:{s:I}}",
+ "pay_duration",
+ "d_us",
+ (json_int_t) GNUNET_TIME_UNIT_MINUTES.rel_value_us);
+ zero = json_pack ("{s:{s:I}}",
+ "pay_duration",
+ "d_us",
+ (json_int_t) 0);
+ omitted = json_object ();
+ forever = json_pack ("{s:{s:s}}",
+ "pay_duration",
+ "d_us",
+ "forever");
+ GNUNET_assert (TALER_MERCHANT_template_contract_valid (finite));
+ GNUNET_assert (TALER_MERCHANT_template_contract_valid (zero));
+ GNUNET_assert (TALER_MERCHANT_template_contract_valid (omitted));
+ GNUNET_assert (! TALER_MERCHANT_template_contract_valid (forever));
+ /* Legacy templates must remain parseable so they can fall back to the
+ instance default when instantiated. */
+ GNUNET_assert (
+ GNUNET_OK ==
+ TALER_MERCHANT_template_contract_parse (forever,
+ &parsed,
+ NULL));
+ GNUNET_assert (GNUNET_TIME_relative_is_forever (parsed.pay_duration));
+ TALER_MERCHANT_template_contract_free (&parsed);
+ json_decref (finite);
+ json_decref (zero);
+ json_decref (omitted);
+ json_decref (forever);
+ }
+
return 0;
}