merchant

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

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:
Msrc/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c | 29+++++++++++++++++++++++++++++
Msrc/include/taler/taler_merchant_testing_lib.h | 34++++++++++++++++++++++++++++++++++
Msrc/include/taler/taler_merchant_util.h | 3++-
Msrc/testing/test_merchant_api.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/testing_api_cmd_patch_template.c | 2++
Msrc/testing/testing_api_cmd_post_templates.c | 2++
Msrc/testing/testing_api_cmd_post_using_templates.c | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/template_parse.c | 3+++
Msrc/util/test_contract.c | 39+++++++++++++++++++++++++++++++++++++++
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; }