Skip to content

Honour discount conditions in BuyXGetY - #2624

Merged
glennjacobs merged 2 commits into
lunarphp:1.xfrom
kha333n:fix/buyxgety-honour-discount-conditions
Aug 25, 2026
Merged

Honour discount conditions in BuyXGetY#2624
glennjacobs merged 2 commits into
lunarphp:1.xfrom
kha333n:fix/buyxgety-honour-discount-conditions

Conversation

@kha333n

@kha333n kha333n commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

A merchant sets up "spend £50, get one free" and it fires on a £20 cart. A
discount restricted to named customers applies to everyone.

What happens now

  • The minimum spend on a BuyXGetY discount has no effect — the reward is given
    at any cart value.
  • A BuyXGetY discount restricted to specific customers applies to every shopper.
  • max_uses_per_user is likewise not consulted.
  • The quantity condition (min_qty) is the only thing actually gating it.

What should happen

  • The discount applies only when the cart meets the advertised minimum spend.
  • Customer restrictions and per-user limits are respected, as they are for
    AmountOff.

Why

AbstractDiscountType::checkDiscountConditions() enforces minimum spend,
customer restrictions and max_uses_per_user. AmountOff::apply() guards on it
first thing:

if (! $this->checkDiscountConditions($cart)) {
    return $cart;
}

BuyXGetY::apply() has no such call — the method is never referenced in the
class.

Worth noting what is not affected, since it narrows the blast radius: dates,
max_uses, the coupon and the channel are all enforced by the query in
DiscountManager::getDiscounts() (active(), usable(), and the coupon
when() clause), so those keep working today. Only the three conditions checked
solely inside checkDiscountConditions() are being skipped.

The fix

Add the same guard AmountOff already uses, at the top of BuyXGetY::apply().
Nothing else in the class changes, and no condition logic is duplicated — it
routes through the shared method so the two discount types stay consistent.

One interaction worth flagging: this means BuyXGetY now also inherits the
guest-cart behaviour of max_uses_per_user that #2470 is addressing. That seems
right — it is the same rule AmountOff already follows — but if #2470 lands
first, both types will pick up its fix together.

Tests

tests/core/Unit/DiscountTypes/BuyXGetYConditionsTest.php:

  • is not applied when the cart is below the minimum spend — £50 minimum on a
    £20 cart. Fails on 1.x with Failed asserting that 1000 matches expected 0.
  • is not applied when the cart customer is not on the discount — same
    failure on 1.x.
  • is applied when the cart meets the minimum spend — passes before and after,
    so the guard is not simply switching the discount off.

AbstractDiscountType::checkDiscountConditions() enforces a discount's minimum
spend, customer restrictions and per-user usage limit. AmountOff::apply() guards
on it before doing anything; BuyXGetY::apply() never calls it, so a BuyXGetY
discount applies whenever its quantity condition is met, whatever those fields
say.

Dates, max_uses, the coupon and the channel are already enforced by the query in
DiscountManager::getDiscounts(), so those continue to hold. What is only checked
in checkDiscountConditions() is ignored: a "spend £50, get one free" promotion
fires on a £20 cart, and a discount restricted to named customers applies to
everyone.

Adds the same guard AmountOff uses.

Tests cover a cart below the advertised minimum spend and a discount restricted
to a customer the cart does not belong to; both fail on 1.x with
"Failed asserting that 1000 matches expected 0". A third asserts the discount
still applies when the minimum is met, so the guard is not simply switching it
off.

Core suite: 556 passing before, 559 after.
@glennjacobs
glennjacobs merged commit b08664f into lunarphp:1.x Aug 25, 2026
14 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Roadmap Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants