Uploaded image for project: 'Blesta Core'
  1. Blesta Core
  2. CORE-5630

Expired recurring coupons with ignore limitations enabled fail to be applied

    Details

    • Type: Bug
    • Status: Closed
    • Priority: Blocker
    • Resolution: Fixed
    • Affects Version/s: 5.13.0
    • Fix Version/s: 5.13.1
    • Component/s: Staff Interface
    • Labels:
      None

      Description

      Recurring coupons are not applying to renewing services when the "Limitations do not apply to renewing services" option is enabled (limit_recurring = 0), even though they should continue to apply indefinitely regardless of expiration dates or usage limits.

      Steps to Reproduce:

      1. Create a recurring coupon with start/end dates (e.g., 2025-12-31 to 2026-01-08)
      2. Set "Limitations do not apply to renewing services" (limit_recurring = 0)
      3. Assign the coupon to a service
      4. Wait until after the coupon's end date passes
      5. Attempt to renew the service

      Expected Result:
      The coupon should continue to apply to the service renewal because limitations (including expiration) are disabled for renewing services.

      Actual Result:
      The coupon does not apply after the end date has passed, even though limitations are disabled.

      Root Cause:
      Three interconnected bugs were introduced in version 5.13:

      1. CORE-5499 (commit 56c7390da, Oct 31 2025) added the limit_recurring option but didn't properly handle NULL date values when checking expiration
      2. Commit c01688ef8 (Nov 5 2025) changed AbstractCoupon to "always check expiration regardless of limits" which broke the ignore limitations feature
      3. AbstractCoupon::expired() had inverted logic that treated NULL dates as expired instead of unlimited

      Technical Details:

      • Coupons::getRecurring() incorrectly evaluated NULL dates: toTime(null) returns false, causing $date > false to always be true
      • AbstractCoupon::expired() returned true when dates were NULL (should return false - no expiration)
      • AbstractCoupon::applies() always checked expiration even when limit_recurring = 0 (should skip expiration check)

      Summary of the 3 updates across the 2 files in the current PR and how they relate:

      1. coupons.php (Coupons::getRecurring())

      • Method: getRecurring() - This method is ONLY called for recurring coupons
      • Scenario: Recurring coupon with limit_recurring = '1' (limitations DO apply) AND NULL dates
      • Bug: toTime(null) returns false, so $date > false is always true = coupon rejected
      • Fix: Check if dates are not NULL before comparing
      • Example: Recurring coupon with no end date set, but "Limitations do apply to renewing services" enabled

      2. AbstractCoupon.php - expired() method (lines 370-371)

      • Method: expired() - Called by the pricing system for all coupons
      • Scenario: ANY coupon (recurring or not) with NULL dates
      • Bug: Returned true (expired) if dates were NULL = coupon rejected
      • Fix: NULL dates now correctly mean "no expiration"
      • Example: Any coupon without start/end dates being treated as expired

      3. AbstractCoupon.php - applies() method (line 168)

      • Method: applies() - Main method that determines if coupon applies
      • Scenario: Recurring coupon with limit_recurring = '0' (limitations DON'T apply) AND expired dates
      • Bug: Always checked expiration even when limits shouldn't apply = customer's expired coupon rejected
      • Fix: Skip expiration check for recurring coupons when limit_recurring = '0'
      • Example: Customer reported expired coupon (2025-12-31) with "Limitations do not apply" enabled, coupon was skipped

        Activity

        There are no comments yet on this issue.

          People

          • Assignee:
            Unassigned
            Reporter:
            admin Paul Phillips
          • Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: