feat(membership): guard auto_renew against missing gateway renewal credential#1230
Conversation
…edential
When a recurring auto-renewing membership saves on a paid gateway, run a
new `wu_membership_has_renewal_credential` filter. If a listener returns
`false` — i.e. it knows the gateway side has no renewable artifact (no
PayPal billing-agreement, no Stripe customer/subscription, no saved
vault) — the membership:
- has `auto_renew` forced to false on save,
- stores a `wu_renewal_credential_missing` meta timestamp,
- emits a WARNING to `membership-{id}` log,
- fires `wu_membership_renewal_credential_missing` once.
The default filter value is `null` (unknown), so existing gateways that
do not opt in are unaffected. Free / manual / empty gateway memberships
are also skipped. Once the flag is set, subsequent saves keep auto_renew
off without re-logging or re-firing the action, even if the filter is
later removed.
This protects customers whose checkout completed successfully but whose
gateway integration mis-classified a recurring purchase as a one-shot
sale (e.g. PPCP `intent=CAPTURE` on a WooCommerce subscription product),
where the renewal a month later would otherwise cascade-cancel the
membership with no recoverable trace.
Refs end-to-end repro on 2026-05-19 with PPCP 4.0.4 + WooCommerce
Subscriptions 7.7.0 + Ultimate Multisite WooCommerce 2.0.12 producing
WC order completed, WC subscription active, UM membership active, but
WC subscription `_ppcp_paypal_intent=CAPTURE` and no PayPal-side
subscription/billing-agreement to charge at renewal.
Tests cover: no listener, listener returns true/false, idempotent
re-save when flag already set, non-recurring skip, exempt gateways
(empty/free/manual), filter receives membership instance.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 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: |
SummaryAdds a defensive guard so a recurring auto-renewing membership cannot be marked WhyEnd-to-end reproduction on 2026-05-19 with PayPal Payments for WooCommerce
HowA new
Verification
Test coverage
Files
Out of scope
aidevops.sh v3.15.68 plugin for OpenCode v1.15.5 with claude-sonnet-4-6 spent 57m and 106 tokens on this as a headless worker. Merged via PR #1230 to main. |
1 similar comment
SummaryAdds a defensive guard so a recurring auto-renewing membership cannot be marked WhyEnd-to-end reproduction on 2026-05-19 with PayPal Payments for WooCommerce
HowA new
Verification
Test coverage
Files
Out of scope
aidevops.sh v3.15.68 plugin for OpenCode v1.15.5 with claude-sonnet-4-6 spent 57m and 106 tokens on this as a headless worker. Merged via PR #1230 to main. |
|
Performance Test Results Performance test results for d641e34 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
Adds a defensive guard so a recurring auto-renewing membership cannot be marked
auto_renew=1when the underlying gateway has no renewable credential on file.Why
End-to-end reproduction on 2026-05-19 with PayPal Payments for WooCommerce
(PPCP) 4.0.4 + WooCommerce Subscriptions 7.7.0 + Ultimate Multisite WooCommerce
2.0.12 produced a checkout that looked completely successful:
auto_renew=1, gateway=woocommerce…but on the PayPal side the order was captured with
intent=CAPTURE(one-shotsale). The WC subscription has no
_ppcp_*/_paypal_subscription_id/_ppcp_billing_agreement_id/ vault meta. A month later, the WC subscriptionrenewal has nothing to charge -> fails -> UM membership cascade-cancels with no
recoverable trace for the customer or admin.
This PR introduces a hookable contract so gateways and addons can declare
"this membership has no renewable artifact on my side", and the model degrades
gracefully before the silent cancellation cascade has a chance to fire.
How
A new
wu_membership_has_renewal_credentialfilter (defaultnull= unknown)runs at the end of
Membership::save()whenever the membership is recurring,auto-renewing, and on a paid gateway (i.e. not
''/free/manual).If a listener returns explicit
false, the model:auto_renewoff,wu_renewal_credential_missingmeta timestamp,membership-{id},wu_membership_renewal_credential_missingexactly once (idempotent on re-save).Default behaviour is unchanged: gateways that do not register an opinion let
auto_renew stand. Free / manual / empty gateway memberships are skipped.
The companion verifier for PPCP one-shot recurring purchases lives in
ultimate-multisite-woocommerce(separate PR).Verification
vendor/bin/phpunit --filter '/Membership_Test::test_(save_(preserves|downgrades|keeps|skips)|filter_receives)/'-- 9 tests, 22 assertions, OK.vendor/bin/phpcs inc/models/class-membership.php tests/WP_Ultimo/Models/Membership_Test.php-- no new violations introduced (pre-existing Yoda + doc-comment violations remain unchanged on both files; present onmain).vendor/bin/phpstan analyse inc/models/class-membership.php tests/WP_Ultimo/Models/Membership_Test.php-- no errors.Test coverage
auto_renewpreserved, no flagtrueauto_renewpreserved, no flagfalseauto_renew=false, flag set, action fires onceauto_renewstays off, action does NOT re-fire(?bool $verified, Membership $m)Files
inc/models/class-membership.php--maybe_downgrade_auto_renew_without_credential()+use Psr\Log\LogLevel;+ save() integration.tests/WP_Ultimo/Models/Membership_Test.php-- 8 new test methods (1 with a 3-row data provider).Out of scope
intent=SUBSCRIPTIONfor recurring carts (PPCP-side bug, follow-up issue).aidevops.sh v3.15.68 plugin for OpenCode v1.15.5 with claude-sonnet-4-6 spent 57m and 106 tokens on this as a headless worker.