feat(wc-subscriptions): recover switch proration when no amount paid#4745
Open
miguelpeixe wants to merge 16 commits into
Open
feat(wc-subscriptions): recover switch proration when no amount paid#4745miguelpeixe wants to merge 16 commits into
miguelpeixe wants to merge 16 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses incorrect proration behavior when switching WooCommerce Subscriptions plans in cases where WCS computes a non-positive “total paid for current period” (e.g., migrated subscriptions, fully-discounted/comped orders, or broken switch chains). It introduces a fallback baseline based on the subscription line item’s recurring total so downgrades are not misclassified as upgrades and don’t incur an erroneous prorated sign-up fee.
Changes:
- Hook
wcs_switch_total_paid_for_current_periodand, when WCS returns a non-positive amount, fall back to the subscription line item’s recurring total. - Add unit tests covering zero baseline recovery, pass-through for positive values, genuinely free subscriptions, and unexpected item types.
- Extend the WooCommerce test mock
WC_Order_Item_Productto supportget_total().
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tests/unit-tests/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php | Adds unit tests for the new “recover total paid” switch proration fallback behavior. |
| tests/mocks/wc-mocks.php | Adds WC_Order_Item_Product::get_total() to support the new logic and tests. |
| includes/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php | Adds the wcs_switch_total_paid_for_current_period filter handler that restores a proration baseline when WCS reports zero/non-positive paid amount. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4171f68 to
2cd7405
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All Submissions:
Changes proposed in this Pull Request:
When a reader switches subscription plans, WooCommerce Subscriptions prorates the change from the amount paid for the current billing period — which it derives by summing the matching line item across the subscription's related orders. For two populations that sum is
$0, which makes WCS treat the old subscription as$0/day, misclassify a downgrade as an upgrade, and charge the full prorated price of the new plan as a sign-up fee instead of crediting the remaining term.This PR corrects the switch behavior for those two cases via WCS filters:
1. Migrated subscriptions. Subscriptions migrated into WooCommerce from another platform (Piano, Stripe) have no Woo order history, so WCS sees
$0. Newspack already opts these into switching viaallow_migrated_subscription_switch(). Thewcs_switch_total_paid_for_current_periodfilter falls back to the subscription line item's recurring total — one billing period's recurring charge, dimensionally what WCS divides by the billing-cycle length. Active free trials are skipped (a migrated sub still in a trial has paid nothing). This applies on all migrated subscriptions.A companion
wcs_switch_proration_days_in_old_cyclefilter clamps the denominator to one billing cycle for migrated subscriptions: without it, WCS's(next_payment - subscription_start) / DAY_IN_SECONDScalculation spans the entire time since the original platform sign-up (often years), which would makeold_price_per_dayartificially low and still misclassify a downgrade as an upgrade even after the amount-paid recovery above. Non-migrated subscriptions are left to WCS's default behavior.2. Paid-trial subscriptions (opt-in). Some publishers express stepped pricing as a one-time sign-up fee plus a free trial — the sign-up fee is functionally a first-period discount, not a real one-time fee. Switching between two such products at WCS's defaults is broken: the matching-trials path forces
new_price_per_dayto0and routes the switch throughextend_prepaid_term, so the reader is upgraded to a more expensive plan for free (or for just the sign-up-fee delta, depending on store settings).When the publisher opts in by defining
NEWSPACK_WC_SUBS_SWITCH_INCLUDE_SIGNUP_FEEinwp-config.php, the integration:wcs_switch_total_paid_for_current_period(so the oldprice_per_dayreflects the discount the reader actually paid).wcs_switch_sign_up_feetomax(new_recurring − unconsumed_credit, 0)— the model is "switch ends the first-period discount on both sides; charge the new plan at its regular recurring price for the first cycle, crediting any unconsumed portion of what the reader paid". This is the only WCS hook reachable on the matching-trials downgrade path;wcs_switch_proration_extra_to_payis never called because WCS routes throughextend_prepaid_terminstead ofcalculate_upgrade_cost.Off by default; only opted-in publishers are affected. The opt-in is also exposed as the
newspack_wc_subs_switch_include_signup_feefilter for finer-grained control (e.g. per-subscription or per-product) — callbacks receive the subscription and line item alongside the enabled flag. The override only intervenes during an active trial when the existing line item carries a paid sign-up fee (the stepped-pricing signature); real free trials, comps, and out-of-trial switches all pass through to WCS's default behavior. Works regardless of WC's store-wideapportion_sign_up_feesetting.Scope is intentionally narrow. Every other zero-paid case — 100%-discount purchases, comps, ordinary unpaid states — is left to WCS's default switching behavior on purpose: we do not carry discounts or comps across switches. Subscriptions with normal order history are untouched (WCS already returns a positive value and the filter no-ops).
Closes NPPM-2837.
How to test the changes in this Pull Request:
$5/month + $3 sign-up fee + 1 month trial, Pro at$10/month + $6 sign-up fee + 1 month trial._piano_subscription_idor_stripe_subscription_id). Backdate the subscription start so it predates the next payment by more than one billing cycle (this surfaces thedays_in_old_cycleclamp). Switch it to a cheaper plan and expand the transaction details. Verify the switch is correctly classified as a downgrade — the term should extend / credit the remaining period rather than charging a full prorated sign-up fee.define( 'NEWSPACK_WC_SUBS_SWITCH_INCLUDE_SIGNUP_FEE', true );towp-config.php. Purchase Regular (reader pays the$3sign-up fee, enters the trial). Immediately switch to Pro and expand the transaction details. Verify the charge equalsnew_recurring − unconsumed_credit— for$10recurring with$3unconsumed, that is$7(plus transaction fee, if any).trial_endby 15 days using WP-CLI). Switch to Pro. Verify the charge scales with consumed time — for half-consumed, the charge is roughly$10 − $1.50 = $8.50.define()(or set itfalse) and repeat step 5. Confirm the switch behaves like release does today — the integration does not intervene and WCS's default switching cost applies.Other information: