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:
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)