fix: prevent free memberships from expiring by treating them as lifetime#957
fix: prevent free memberships from expiring by treating them as lifetime#957superdav42 merged 1 commit intomainfrom
Conversation
When signing up for a free product, Cart::get_billing_start_date() correctly returns null (no billing needed). However, Checkout::maybe_create_membership() passed that null directly into gmdate(), which silently treated it as the current timestamp — setting date_expiration to today at 23:59:59. This caused free memberships to be picked up by the expiration cron and marked as expired within days of signup. The fix checks whether the billing start date is null before calling gmdate(). When null (free/non-recurring product), date_expiration is set to null, which Membership::is_lifetime() correctly identifies as a lifetime membership. The cron's expired-check query already excludes null expiration dates. Adds test: test_maybe_create_membership_free_product_has_null_expiration
📝 WalkthroughWalkthroughThe changes fix a bug where free memberships were incorrectly expiring immediately due to improper null handling of billing start dates. The Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@inc/checkout/class-checkout.php`:
- Around line 1324-1326: The ternary for assigning
$membership_data['date_expiration'] uses a truthy check on $billing_start_date
which treats 0/'0' as no date; change the condition to an explicit null check
(e.g. compare $billing_start_date !== null) so that when $billing_start_date is
zero it still calls gmdate('Y-m-d 23:59:59', $billing_start_date) and only
assigns null when $billing_start_date is actually null; update the expression
where $membership_data['date_expiration'] is set (the line that calls gmdate
with $billing_start_date) accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 701bc47f-6542-444a-8469-95d116094c0e
📒 Files selected for processing (2)
inc/checkout/class-checkout.phptests/WP_Ultimo/Checkout/Checkout_Test.php
| $membership_data['date_expiration'] = $billing_start_date | ||
| ? gmdate('Y-m-d 23:59:59', $billing_start_date) | ||
| : null; |
There was a problem hiding this comment.
Use an explicit null check for date_expiration assignment.
Using a truthy check can incorrectly treat 0/'0' as “no date” and set a lifetime membership. Compare against null explicitly.
Suggested fix
- $membership_data['date_expiration'] = $billing_start_date
- ? gmdate('Y-m-d 23:59:59', $billing_start_date)
+ $membership_data['date_expiration'] = null !== $billing_start_date
+ ? gmdate('Y-m-d 23:59:59', (int) $billing_start_date)
: null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $membership_data['date_expiration'] = $billing_start_date | |
| ? gmdate('Y-m-d 23:59:59', $billing_start_date) | |
| : null; | |
| $membership_data['date_expiration'] = null !== $billing_start_date | |
| ? gmdate('Y-m-d 23:59:59', (int) $billing_start_date) | |
| : null; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@inc/checkout/class-checkout.php` around lines 1324 - 1326, The ternary for
assigning $membership_data['date_expiration'] uses a truthy check on
$billing_start_date which treats 0/'0' as no date; change the condition to an
explicit null check (e.g. compare $billing_start_date !== null) so that when
$billing_start_date is zero it still calls gmdate('Y-m-d 23:59:59',
$billing_start_date) and only assigns null when $billing_start_date is actually
null; update the expression where $membership_data['date_expiration'] is set
(the line that calls gmdate with $billing_start_date) accordingly.
|
Performance Test Results Performance test results for 592d5db are in 🛎️! Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown. URL:
|
Summary
Checkout::maybe_create_membership()passednull(fromCart::get_billing_start_date()) intogmdate(), which silently used the current timestamp — settingdate_expirationto today at 23:59:59gmdate(); when null (free product), setdate_expirationtonullso the membership is correctly identified as lifetimeDetails
The bug trace
Cart::get_billing_start_date()correctly returnsnullfor free, non-recurring productsCheckout::maybe_create_membership()rangmdate('Y-m-d 23:59:59', null)— PHP treatsnullas current timedate_expiration = "today 23:59:59"Membership::is_lifetime()returnsfalse(expiration is non-empty)membership_expired_check()picks it up after 3-day grace period → membership marked EXPIREDThe fix
When
date_expirationisnull:Membership::is_lifetime()→truenulldates from the expired-check query (date_expiration__not_in => [null, '0000-00-00 00:00:00'])Files changed
inc/checkout/class-checkout.php— fix inmaybe_create_membership()tests/WP_Ultimo/Checkout/Checkout_Test.php— new testtest_maybe_create_membership_free_product_has_null_expirationTesting
nullexpiration andis_lifetime() === trueNote on existing free memberships
Existing free memberships that have already been marked as expired will need to be manually reactivated. The admin can do this from the membership edit screen.
aidevops.sh v3.13.1 plugin for OpenCode v1.3.17 with gemma4:e4b spent 7d and 43,427 tokens on this as a headless worker.
Summary by CodeRabbit
Bug Fixes
Tests