[#1184] Coupons handling: rest_amount and overspending

Migrated from Redmine #1184 | Author: Thomas Luzat
Status: New | Priority: High, I’m very impatient | Created: 2024-03-01


I noticed some problems with coupons. The worst one is that codes can be applied multiple times and customers can baiscally order for free.

When accessing /admin/promotions/coupons?filter[code]=CODE using the REST API, I get a result such as:


{
    "data": [
        {
            "id": 4672,
            "promotion": {
                "id": 3587,
                "name": "XXXX",
                "description": "",
                "file_id": null,
                "picture_preview": null,
                "picture_large": null,
                "is_active": true,
                "position": 5996,
                "price": 0.0,
                "currency": "EUR",
                "tax": null,
                "promotion_type": "discount",
                "discount_type": "fixed_amount",
                "discount": 209.3,
                "duration_type": "month",
                "duration": 1,
                "client_type": "all",
                "allow_usage_count": 1,
                "is_unlimited": false,
                "affect_services": true,
                "affect_products": true,
                "affect_paid_attributes": true,
                "affect_memberships": false,
                "affected_packages": false,
                "service_restrictions": [],
                "booking_restrictions": [],
                "product_restrictions": [],
                "paid_attribute_restrictions": [],
                "memberships_restrictions": [],
                "package_restrictions": [],
                "print_template_id": 1,
                "bonus_is_enabled": false,
                "bonus_amount": 0
            },
            "start_date": null,
            "expired_date": "2027-02-25",
            "is_used": true,
            "can_be_used": true,
            "can_be_used_count": -1,
            "code": "XXXX",
            "client_id": null,
            "client": null,
            "user_id": "1",
            "user": {
                …
            },
            "status": "used",
            "used_count": "2",
            "used_amount": "337.3000",
            "rest_amount": null,
            "allowed_to_use_full_amount": true
        }
    ],
    "metadata": {
        …
    }
}

There are some minor and some major problems (3. is the worst one):

  1. used_amount is either null or a string. Why not always a float (like promotion.discount), either 0.0 or some value? Similar for rest_amount, used_count, user_id. Other values are integers, such as can_be_used_count …
  2. rest_amount is null for coupons? It would be nice to have it go from 209.3 down to 0.0, somewhat similar to gift cards.
  3. I was able to apply this code to two orders using /admin/invoices/…/apply-promo-code and the price was in sum reduced by 337.30 € (as indicated by used_amount), even though it should have been limited to 209.30 €. Expected: Discount should be used to reduce order 1 fully (e.g. by 190 €) and partially for order 2 (e.g. by 19.30 € which are still left, 200+19.30 = 209.30). According to the SimplyBook interface, fixed discount coupons should be usable across multiple orders like gift cards if there is value left.
  4. Values are completely broken now: can_be_used is still true, can_be_used_count is negative, used_count > allow_usage_count. Not sure what allowed_to_use_full_amount indicates.

What I expect:

  1. used_amount and rest_amount should always be valid floats except for percentage discounts, where they should be zero. integers should preferably also be represented as integers, not strings.
  2. The discount should only apply at most rest_amount if it has already been partially applied. Also, can_be_used should be false when rest_amount reaches 0.0.

A valid rest_amount and can_be_used would allow to easier provide a better custom interface for booking. Currently I use something like:


if (can_be_used && (used_amount === null || promotion_type.discount > parseFloat(used_amount))) { show(rest_amount) }

I would prefer:


if (can_be_used) { show(rest_amount) }

or similar, but the worst problem is obviously that the code is applied multiple times with too much of a value.

There seems to be some broken logic for the shown configuration in apply-promo-code and in the coupons endpoint. It might even be that I can still use the coupon shown above.

Redmine Admin wrote:

hi, thank you for feedback

can be applied multiple times and customers can baiscally order for free
Currently fixed amount coupon can be used for full amount by each customer who has it.

  1. Was it the same customer or 2 different clients?

Thomas Luzat wrote:

Redmine Admin wrote:

hi, thank you for feedback

can be applied multiple times and customers can baiscally order for free
Currently fixed amount coupon can be used for full amount by each customer who has it.

  1. Was it the same customer or 2 different clients?

I have indeed not tested with the same client, but most likely just using a different mail address or sharing their code would already allow customers to work around this without any good way to notice.

I guess gift cards would be a workaround? We could create those instead, even though that means that we need to create a new gift card template and instance for every amount we’re handing out.