merchant

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

commit 758715b9afe583d3c3d40915962bfcc7940182cb
parent cdd9de84bcc57bab62ab23fbef2929484afd16c9
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue,  4 Aug 2026 19:40:17 +0200

implement #11538: allow amount overrides in paivana templates (protocol v34)

Diffstat:
Msrc/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c | 31+++++++++++++++++++++++++++++--
Msrc/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c | 339++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/testing/test_merchant_templates.sh | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/template_parse.c | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 663 insertions(+), 4 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c @@ -30,6 +30,31 @@ /** + * Determine the currency the client must pay in, if the template + * requires a particular one without fixing the amount. + * + * @param template_contract contract of the template + * @return currency to require, NULL if the template does not say + */ +static const char * +get_required_currency (const json_t *template_contract) +{ + const json_t *currency; + + currency = json_object_get (template_contract, + "currency"); + if (NULL == currency) + return NULL; + if (! json_is_string (currency)) + { + GNUNET_break (0); + return NULL; + } + return json_string_value (currency); +} + + +/** * Context for building inventory template payloads. */ struct InventoryPayloadContext @@ -495,7 +520,8 @@ handle_get_templates_inventory ( tp->editable_defaults)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("required_currency", - NULL /* FIXME: add support */)), + get_required_currency ( + tp->template_contract))), GNUNET_JSON_pack_object_steal ("template_contract", template_contract)); inventory_payload_cleanup (&ipc); @@ -557,7 +583,8 @@ TMH_get_templates_ID ( tp.editable_defaults)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("required_currency", - NULL /* FIXME: add support */)), + get_required_currency ( + tp.template_contract))), GNUNET_JSON_pack_object_incref ("template_contract", tp.template_contract)); TALER_MERCHANTDB_template_details_free (&tp); diff --git a/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c @@ -37,6 +37,23 @@ /** + * Amount the client chose for one of the choices of a paivana template. + */ +struct PaivanaChoiceAmount +{ + /** + * Index into the @e choices array of the template contract. + */ + uint32_t choice_index; + + /** + * Amount to use for that choice, excluding any tip. + */ + struct TALER_Amount amount; +}; + + +/** * Item selected from inventory_selection. */ struct InventoryTemplateItemContext @@ -235,6 +252,17 @@ struct UseContext */ const char *paivana_id; + /** + * Amounts the client picked for those choices of the + * template that allow the amount to be edited. + */ + struct PaivanaChoiceAmount *choice_amounts; + + /** + * Length of the @e choice_amounts array. + */ + unsigned int choice_amounts_len; + } paivana; } parse_request; @@ -320,6 +348,9 @@ cleanup_use_context (void *cls) uc->parse_request.inventory.items) cleanup_inventory_items (uc->parse_request.inventory.items_len, uc->parse_request.inventory.items); + GNUNET_array_grow (uc->parse_request.paivana.choice_amounts, + uc->parse_request.paivana.choice_amounts_len, + 0); TALER_MERCHANT_template_contract_free (&uc->template_contract); GNUNET_free (uc->compute_price.totals); uc->compute_price.totals_len = 0; @@ -439,8 +470,8 @@ parse_using_templates_inventory_request ( { struct InventoryTemplateItemContext item = { 0 }; struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("product_id", - &item.product_id), + TALER_JSON_spec_slug ("product_id", + &item.product_id), GNUNET_JSON_spec_string ("quantity", &item.unit_quantity), GNUNET_JSON_spec_end () @@ -481,11 +512,16 @@ static enum GNUNET_GenericReturnValue parse_using_templates_paivana_request ( struct UseContext *uc) { + const json_t *choice_amounts = NULL; 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_mark_optional ( + GNUNET_JSON_spec_array_const ("choice_amounts", + &choice_amounts), + NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; @@ -503,6 +539,69 @@ parse_using_templates_paivana_request ( res); return GNUNET_SYSERR; } + if (NULL != choice_amounts) + { + if (! uc->parse_request.no_amount) + { + /* 'amount' is the shorthand for a template with a single + editable choice; using both is ambiguous. */ + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + "amount and choice_amounts are mutually exclusive"); + return GNUNET_SYSERR; + } + for (size_t i = 0; i < json_array_size (choice_amounts); i++) + { + struct PaivanaChoiceAmount ca; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_uint32 ("choice_index", + &ca.choice_index), + TALER_JSON_spec_amount_any ("amount", + &ca.amount), + GNUNET_JSON_spec_end () + }; + const char *err_name; + unsigned int err_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (choice_amounts, + i), + ispec, + &err_name, + &err_line)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice_amounts"); + return GNUNET_SYSERR; + } + for (unsigned int j = 0; + j < uc->parse_request.paivana.choice_amounts_len; + j++) + { + if (uc->parse_request.paivana.choice_amounts[j].choice_index != + ca.choice_index) + continue; + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice_amounts::choice_index is not unique"); + return GNUNET_SYSERR; + } + /* The currency does not need to be checked against our + configuration here: it must match the currency of the + choice in the template, which the merchant picked. */ + GNUNET_array_append (uc->parse_request.paivana.choice_amounts, + uc->parse_request.paivana.choice_amounts_len, + ca); + } + } if (1 != sscanf (uc->parse_request.paivana.paivana_id, "%llu-", @@ -790,6 +889,83 @@ handle_phase_db_fetch (struct UseContext *uc) /* *************** Helpers for USE_PHASE_VERIFY ***************** */ /** + * Check that @a amount is within the limits (if any) the template + * imposes on amounts the client had an influence on. Amounts the + * merchant hard-coded in the template are not subject to these limits. + * Replies with an error if the check fails. + * + * @param[in,out] uc use context + * @param amount amount to check, excluding any tip + * @param detail hint to return to the client on failure + * @return #GNUNET_OK if @a amount is acceptable + */ +static enum GNUNET_GenericReturnValue +check_amount_limits (struct UseContext *uc, + const struct TALER_Amount *amount, + const char *detail) +{ + const struct TALER_MERCHANT_TemplateContract *tc = &uc->template_contract; + const struct TALER_Amount *limit; + char *msg; + + if (tc->no_min_amount && + tc->no_max_amount) + return GNUNET_OK; + /* Parsing the template guarantees both limits to be in the same + currency, so it suffices to check @a amount against either one. + We must check against a limit (and not merely against the + currency of the template) as it is the limits that @a amount is + compared to below, and comparing amounts of different currencies + fails an assertion. */ + limit = tc->no_min_amount + ? &tc->max_amount + : &tc->min_amount; + if (GNUNET_YES != + TALER_amount_cmp_currency (amount, + limit)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + limit->currency); + return GNUNET_SYSERR; + } + if ( (! tc->no_min_amount) && + (0 > TALER_amount_cmp (amount, + &tc->min_amount)) ) + { + GNUNET_break_op (0); + GNUNET_asprintf (&msg, + "%s is below the min_amount of %s", + detail, + TALER_amount2s (&tc->min_amount)); + } + else if ( (! tc->no_max_amount) && + (0 < TALER_amount_cmp (amount, + &tc->max_amount)) ) + { + GNUNET_break_op (0); + GNUNET_asprintf (&msg, + "%s is above the max_amount of %s", + detail, + TALER_amount2s (&tc->max_amount)); + } + else + { + return GNUNET_OK; + } + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + msg); + GNUNET_free (msg); + return GNUNET_SYSERR; +} + + +/** * 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 @@ -1009,6 +1185,61 @@ verify_using_templates_fixed ( /** + * Turn a plain @e amount given by the client into an entry in the + * @e choice_amounts array. This shorthand is what the generic + * template flow (``editable_defaults`` and ``taler://pay-template`` + * URIs) can express, and thus only works if the template has exactly + * one choice with an editable amount. + * + * @param[in,out] uc use context + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +resolve_paivana_amount_shorthand ( + struct UseContext *uc) +{ + const struct TALER_MERCHANT_TemplateContractPaivana *tcp + = &uc->template_contract.details.paivana; + struct PaivanaChoiceAmount ca = { + .amount = uc->parse_request.amount + }; + unsigned int editable = 0; + + for (unsigned int i = 0; i < tcp->choices_len; i++) + { + if (! tcp->choices[i].editable_amount) + continue; + ca.choice_index = i; + editable++; + } + if (0 == editable) + { + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + "template does not allow the amount to be edited"); + return GNUNET_SYSERR; + } + if (1 != editable) + { + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + "template has more than one editable choice, use choice_amounts"); + return GNUNET_SYSERR; + } + GNUNET_array_append (uc->parse_request.paivana.choice_amounts, + uc->parse_request.paivana.choice_amounts_len, + ca); + return GNUNET_OK; +} + + +/** * Verify request data for paivana templates. * * @param[in,out] uc use context @@ -1018,6 +1249,60 @@ static enum GNUNET_GenericReturnValue verify_using_templates_paivana ( struct UseContext *uc) { + const struct TALER_MERCHANT_TemplateContractPaivana *tcp + = &uc->template_contract.details.paivana; + + if ( (! uc->parse_request.no_amount) && + (GNUNET_OK != + resolve_paivana_amount_shorthand (uc)) ) + return GNUNET_SYSERR; + for (unsigned int i = 0; + i < uc->parse_request.paivana.choice_amounts_len; + i++) + { + const struct PaivanaChoiceAmount *ca + = &uc->parse_request.paivana.choice_amounts[i]; + const struct TALER_MERCHANT_OrderChoice *choice; + + if (ca->choice_index >= tcp->choices_len) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "choice_amounts::choice_index out of range"); + return GNUNET_SYSERR; + } + choice = &tcp->choices[ca->choice_index]; + if (! choice->editable_amount) + { + GNUNET_break_op (0); + use_reply_with_error ( + uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT, + "selected choice has a fixed amount"); + return GNUNET_SYSERR; + } + /* The currency is baked into the choice (say via 'max_fee'), + so the client may only change the value, not the currency. */ + if (GNUNET_YES != + TALER_amount_cmp_currency (&ca->amount, + &choice->amount)) + { + GNUNET_break_op (0); + use_reply_with_error (uc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + choice->amount.currency); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + check_amount_limits (uc, + &ca->amount, + "amount of the selected choice")) + return GNUNET_SYSERR; + } if (NULL != uc->template_contract.details.paivana.website_regex) { regex_t ex; @@ -1123,6 +1408,33 @@ handle_phase_verify ( /** + * Find the amount the client picked for the choice at @a choice_index + * of a paivana template. + * + * @param uc use context + * @param choice_index index into the choices of the template contract + * @return NULL if the client did not pick an amount for this choice, + * in which case the amount from the template applies + */ +static const struct TALER_Amount * +find_paivana_choice_amount (const struct UseContext *uc, + unsigned int choice_index) +{ + for (unsigned int i = 0; + i < uc->parse_request.paivana.choice_amounts_len; + i++) + { + const struct PaivanaChoiceAmount *ca + = &uc->parse_request.paivana.choice_amounts[i]; + + if (choice_index == ca->choice_index) + return &ca->amount; + } + return NULL; +} + + +/** * Compute the line total for a product based on quantity. * * @param unit_price price per unit @@ -1387,6 +1699,12 @@ handle_phase_compute_price (struct UseContext *uc) GNUNET_assert (uc->template_contract.no_amount); *uc->compute_price.totals = uc->parse_request.amount; + /* Only an amount the client chose is subject to the limits. */ + if (GNUNET_OK != + check_amount_limits (uc, + uc->compute_price.totals, + "amount")) + return; } uc->phase++; return; @@ -1406,7 +1724,12 @@ handle_phase_compute_price (struct UseContext *uc) /* Make deep copy, we're going to MODIFY it! */ struct TALER_MERCHANT_OrderChoice choice = tcp->choices[i]; + const struct TALER_Amount *ca; + ca = find_paivana_choice_amount (uc, + i); + if (NULL != ca) + choice.amount = *ca; choice.no_tip = uc->parse_request.no_tip; if (! uc->parse_request.no_tip) { @@ -1490,6 +1813,18 @@ handle_phase_compute_price (struct UseContext *uc) NULL); return; } + /* The client picked the products and quantities, so the + resulting total is subject to the limits. */ + for (unsigned int i = 0; + i < uc->compute_price.totals_len; + i++) + { + if (GNUNET_OK != + check_amount_limits (uc, + &uc->compute_price.totals[i], + "total of the selected products")) + return; + } uc->phase++; } diff --git a/src/testing/test_merchant_templates.sh b/src/testing/test_merchant_templates.sh @@ -235,5 +235,204 @@ then exit_fail "Order status should be 'paid'. got: $ORDER_STATUS" fi +# +# PAIVANA TEMPLATES WITH A CLIENT-EDITABLE AMOUNT +# + +echo -n "Rejecting amount limits without a currency ..." +STATUS=$(curl 'http://localhost:9966/private/templates' \ + -d '{"template_id":"paivana-bad","template_description":"Limits but no currency","template_contract":{"template_type":"paivana","summary":"The summary","min_amount":"TESTKUDOS:0.5","choices":[{"amount":"TESTKUDOS:1"}]}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "400" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 400 for min_amount without currency. got: $STATUS" +fi +echo " OK" + +echo -n "Rejecting amount limits in different currencies ..." +STATUS=$(curl 'http://localhost:9966/private/templates' \ + -d '{"template_id":"paivana-bad","template_description":"Limits in different currencies","template_contract":{"template_type":"paivana","summary":"The summary","min_amount":"TESTKUDOS:1","max_amount":"EUR:2","choices":[{"amount":"TESTKUDOS:1"}]}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "400" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 400 for min_amount and max_amount in different currencies. got: $STATUS" +fi +echo " OK" + +echo -n "Rejecting min_amount above max_amount ..." +STATUS=$(curl 'http://localhost:9966/private/templates' \ + -d '{"template_id":"paivana-bad","template_description":"Limits inverted","template_contract":{"template_type":"paivana","summary":"The summary","currency":"TESTKUDOS","min_amount":"TESTKUDOS:5","max_amount":"TESTKUDOS:1","choices":[{"amount":"TESTKUDOS:1"}]}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "400" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 400 for min_amount > max_amount. got: $STATUS" +fi +echo " OK" + +echo -n "Rejecting an amount for a template with a fixed amount ..." +STATUS=$(curl 'http://localhost:9966/templates/'"$TID" \ + -d '{"template_type":"paivana","amount":"TESTKUDOS:2","website":"https://example.com/","paivana_id":"4322-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs="}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "409" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 409, amount is not editable. got: $STATUS" +fi +echo " OK" + +echo -n "Creating Paivana template with an editable amount..." +ETID="paivana-editable" +STATUS=$(curl 'http://localhost:9966/private/templates' \ + -d '{"template_id":"paivana-editable","template_description":"A Paivana template with an editable amount","template_contract":{"template_type":"paivana","summary":"The summary","currency":"TESTKUDOS","min_amount":"TESTKUDOS:0.5","max_amount":"TESTKUDOS:10","choices":[{"amount":"TESTKUDOS:2","editable_amount":true}]},"editable_defaults":{"amount":"TESTKUDOS:2"}}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "204" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 204, template created. got: $STATUS" +fi +echo " OK" + +echo -n "Checking editable template data ..." +STATUS=$(curl http://localhost:9966/templates/"$ETID" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +EDITABLE=$(jq -r .template_contract.choices[0].editable_amount < "$LAST_RESPONSE") +if [ "$EDITABLE" != "true" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected editable_amount true. Got: $EDITABLE" +fi + +REQUIRED_CURRENCY=$(jq -r .required_currency < "$LAST_RESPONSE") +if [ "$REQUIRED_CURRENCY" != "TESTKUDOS" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected required_currency TESTKUDOS. Got: $REQUIRED_CURRENCY" +fi + +DEFAULT_AMOUNT=$(jq -r .editable_defaults.amount < "$LAST_RESPONSE") +if [ "$DEFAULT_AMOUNT" != "TESTKUDOS:2" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected editable_defaults.amount TESTKUDOS:2. Got: $DEFAULT_AMOUNT" +fi +echo " OK" + +echo -n "Rejecting an amount below min_amount ..." +STATUS=$(curl 'http://localhost:9966/templates/'"$ETID" \ + -d '{"template_type":"paivana","amount":"TESTKUDOS:0.25","website":"https://example.com/","paivana_id":"4323-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs="}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "409" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 409, amount below min_amount. got: $STATUS" +fi +echo " OK" + +echo -n "Rejecting an amount above max_amount ..." +STATUS=$(curl 'http://localhost:9966/templates/'"$ETID" \ + -d '{"template_type":"paivana","amount":"TESTKUDOS:50","website":"https://example.com/","paivana_id":"4324-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs="}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "409" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 409, amount above max_amount. got: $STATUS" +fi +echo " OK" + +echo -n "Rejecting an out-of-range choice_index ..." +STATUS=$(curl 'http://localhost:9966/templates/'"$ETID" \ + -d '{"template_type":"paivana","choice_amounts":[{"choice_index":7,"amount":"TESTKUDOS:2"}],"website":"https://example.com/","paivana_id":"4325-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs="}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "400" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 400, choice_index out of range. got: $STATUS" +fi +echo " OK" + +echo -n "Rejecting amount together with choice_amounts ..." +STATUS=$(curl 'http://localhost:9966/templates/'"$ETID" \ + -d '{"template_type":"paivana","amount":"TESTKUDOS:2","choice_amounts":[{"choice_index":0,"amount":"TESTKUDOS:2"}],"website":"https://example.com/","paivana_id":"4326-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs="}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "409" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 409, amount and choice_amounts are exclusive. got: $STATUS" +fi +echo " OK" + +echo -n "Creating order with a client-specified amount ..." +EPAIVANA_ID="4327-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs=" +STATUS=$(curl 'http://localhost:9966/templates/'"$ETID" \ + -d '{"template_type":"paivana","amount":"TESTKUDOS:5","tip":"TESTKUDOS:0.5","website":"https://example.com/","paivana_id":"'"${EPAIVANA_ID}"'"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, order created. got: $STATUS" +fi + +EORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") +STATUS=$(curl "http://localhost:9966/private/orders/${EORDER_ID}?session_id=${EPAIVANA_ID}" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, getting order info. got: $STATUS" +fi + +PRICE=$(jq -r .total_amount < "$LAST_RESPONSE") +if [ "$PRICE" != "TESTKUDOS:5.5" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected TESTKUDOS:5.5 (amount plus tip) but got: $PRICE" +fi +echo " OK" + +echo -n "Creating order with an explicit choice_amounts entry ..." +CPAIVANA_ID="4328-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs=" +STATUS=$(curl 'http://localhost:9966/templates/'"$ETID" \ + -d '{"template_type":"paivana","choice_amounts":[{"choice_index":0,"amount":"TESTKUDOS:3"}],"website":"https://example.com/","paivana_id":"'"${CPAIVANA_ID}"'"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, order created. got: $STATUS" +fi + +CORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") +STATUS=$(curl "http://localhost:9966/private/orders/${CORDER_ID}?session_id=${CPAIVANA_ID}" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, getting order info. got: $STATUS" +fi + +PRICE=$(jq -r .total_amount < "$LAST_RESPONSE") +if [ "$PRICE" != "TESTKUDOS:3" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected TESTKUDOS:3 but got: $PRICE" +fi +echo " OK" + echo "TEST PASSED" exit 0 diff --git a/src/util/template_parse.c b/src/util/template_parse.c @@ -247,6 +247,90 @@ parse_template_paivana (const json_t *template_contract, } +/** + * Check that the (optional) @e min_amount and @e max_amount of @a tc are + * usable: they must be denominated in the same currency, which must be + * the currency of the template (which then must be given, as otherwise we + * could not tell in which currency to enforce the limits), and they must + * not exclude each other. + * + * @param tc template contract to check + * @param[out] error_name error description + * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure + */ +static enum GNUNET_GenericReturnValue +check_amount_limits (const struct TALER_MERCHANT_TemplateContract *tc, + const char **error_name) +{ + if (tc->no_min_amount && + tc->no_max_amount) + return GNUNET_OK; + if ( (! tc->no_min_amount) && + (! tc->no_max_amount) ) + { + /* Must be checked before the two are compared, as comparing + amounts of different currencies fails an assertion. Note that + we deliberately do not rely on both having been checked against + the currency of the template here. */ + if (GNUNET_YES != + TALER_amount_cmp_currency (&tc->min_amount, + &tc->max_amount)) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "min_amount and max_amount must use the same currency\n"); + if (NULL != error_name) + *error_name = "max_amount"; + return GNUNET_SYSERR; + } + if (0 < TALER_amount_cmp (&tc->min_amount, + &tc->max_amount)) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "min_amount exceeds max_amount\n"); + if (NULL != error_name) + *error_name = "min_amount"; + return GNUNET_SYSERR; + } + } + if (NULL == tc->currency) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Template with min_amount or max_amount must specify a currency\n"); + if (NULL != error_name) + *error_name = "currency"; + return GNUNET_SYSERR; + } + if ( (! tc->no_min_amount) && + (0 != strcasecmp (tc->currency, + tc->min_amount.currency)) ) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "min_amount must use the currency of the template (%s)\n", + tc->currency); + if (NULL != error_name) + *error_name = "min_amount"; + return GNUNET_SYSERR; + } + if ( (! tc->no_max_amount) && + (0 != strcasecmp (tc->currency, + tc->max_amount.currency)) ) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "max_amount must use the currency of the template (%s)\n", + tc->currency); + if (NULL != error_name) + *error_name = "max_amount"; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + enum GNUNET_GenericReturnValue TALER_MERCHANT_template_contract_parse ( const json_t *template_contract, @@ -272,6 +356,14 @@ TALER_MERCHANT_template_contract_parse ( &out->amount), &out->no_amount), GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("min_amount", + &out->min_amount), + &out->no_min_amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("max_amount", + &out->max_amount), + &out->no_max_amount), + GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint32 ("minimum_age", &out->minimum_age), NULL), @@ -298,6 +390,8 @@ TALER_MERCHANT_template_contract_parse ( return GNUNET_SYSERR; } out->max_pickup_duration = GNUNET_TIME_UNIT_FOREVER_REL; + out->no_min_amount = true; + out->no_max_amount = true; if (GNUNET_OK != GNUNET_JSON_parse ((json_t *) template_contract, spec, @@ -311,6 +405,10 @@ TALER_MERCHANT_template_contract_parse ( *error_name = en; return GNUNET_SYSERR; } + if (GNUNET_OK != + check_amount_limits (out, + error_name)) + return GNUNET_SYSERR; out->type = TALER_MERCHANT_template_type_from_string (template_type_str); if (TALER_MERCHANT_TEMPLATE_TYPE_INVALID == out->type)