Conversation
WalkthroughUpdates add date utilities (getLastDayOfMonth, isLastDayOfMonth, getAnchoredBillingDate, getLastDiscountedPayment) and revise getDiscountWindow to handle repeating Stripe discounts and month-end anchoring. The function now documents and uses subscription fields current_period_end, plan.interval, and plan_interval. For Stripe discounts with a discount_end, the code returns null when the discount has expired or ends before the next billing date; otherwise it returns a window anchored to billing dates. Tests were extended with sinon fake timers and cases for expiry and anchoring. Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
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
`@ghost/core/core/server/services/members/members-api/utils/get-discount-window.js`:
- Around line 27-38: The current getLastDiscountedPayment computes monthOffset
in single-month steps which fails for yearly plans — update
getLastDiscountedPayment to advance in billing periods based on the plan cadence
(subscription.plan.interval), not always by one month: derive periodLengthMonths
= (interval === 'year' ? 12 : 1) (or map other intervals similarly), compute
totalMonthsDiff between discountEnd and nextBillingDate, compute periodOffset =
Math.floor(totalMonthsDiff / periodLengthMonths), then call
getAnchoredBillingDate(nextBillingDate, periodOffset * periodLengthMonths) and
fall back to one period less if that date is after discountEnd; apply the same
cadence-aware logic to the other affected blocks referenced (around the code at
lines ~49-50 and ~58-75) so all computations use periodLengthMonths/periodOffset
rather than fixed one-month increments.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 918f92cd-f74c-4453-9d5d-ff3b979e5002
📒 Files selected for processing (3)
ghost/core/core/server/services/members/members-api/utils/get-discount-window.jsghost/core/test/unit/server/services/members/members-api/services/next-payment-calculator.test.jsghost/core/test/unit/server/services/members/members-api/utils/get-discount-window.test.js
ghost/core/core/server/services/members/members-api/utils/get-discount-window.js
Show resolved
Hide resolved
🤖 Velo CI Failure AnalysisClassification: 🔴 HARD FAIL
|
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
33d0fd1 to
dbd371b
Compare
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
`@ghost/core/core/server/services/members/members-api/utils/get-discount-window.js`:
- Around line 58-75: The legacy repeating-offer branch in get-discount-window.js
must be changed to compute the discount end as the last discounted payment
rather than start_date + duration_in_months; locate the legacy backport branch
(the code that uses subscription.discount_start and duration_in_months) and
replace its end calculation to call the same helper logic used above
(getLastDiscountedPayment) with the offer duration and dates so it returns the
last discounted billing date, keeping start as subscription.discount_start; also
add a regression test exercising a repeating legacy offer (e.g., start Jan 1,
duration_in_months 3) to assert discount.end equals the last discounted renewal
(Mar 1) and ensure NextPaymentCalculator behavior is covered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6f592cd4-117e-43d5-bdf1-a2e4d172fbf3
📒 Files selected for processing (6)
apps/portal/package.jsonapps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.jsapps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.jsghost/core/core/server/services/members/members-api/utils/get-discount-window.jsghost/core/test/unit/server/services/members/members-api/services/next-payment-calculator.test.jsghost/core/test/unit/server/services/members/members-api/utils/get-discount-window.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/portal/package.json
ghost/core/core/server/services/members/members-api/utils/get-discount-window.js
Show resolved
Hide resolved
🤖 Velo CI Failure AnalysisClassification: 🔴 HARD FAIL
|
closes https://linear.app/ghost/issue/BER-3413/account-page-date-mismatch-between-discount-end-billing-period - In Stripe, discount dates are anchored around the redemption date, not the billing date. However, we're interested in whether the next payment(s) are discounted. This aligns the next payment object discount end date with the last payment with a discount. - Example: - Bob's billing cycle is from Mar 3 to April 3 - Bob redeems a retention offer on Mar 1, that is repeating 3 months - Stripe will set discount start to Mar 1 and discount end to Jun 1 - Bob's next 3 payments will be discounted: Mar 3, Apr 3 and May 3, and we want to display "Ends May 3" (not "Ends Jun 1")
- not needed anymore, as fix has moved to the backend
Changelog for v2.65.5 -> 2.65.6:
55945e4 to
e6eb2cd
Compare
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
`@ghost/core/core/server/services/members/members-api/utils/get-discount-window.js`:
- Around line 27-38: getLastDiscountedPayment is inferring a month-end anchor
from nextBillingDate/current_period_end which breaks when a subscription is
shortened by February; update the function to accept and use the real billing
anchor (pass through the original billing anchor value instead of deriving it
from nextBillingDate) when calling getAnchoredBillingDate so month-length
normalization doesn’t convert a non-month-end anchor into month-end, and add a
regression test that covers the February-shortened case (e.g., start_date
2025-01-28, current_period_end 2025-02-28, discount_end 2025-04-01 expecting
last discounted billing date 2025-03-28) to ensure the anchored calculations
preserve the original anchor.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ae902c04-be51-4736-ae75-26a59c0a114c
📒 Files selected for processing (6)
apps/portal/package.jsonapps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.jsapps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.jsghost/core/core/server/services/members/members-api/utils/get-discount-window.jsghost/core/test/unit/server/services/members/members-api/services/next-payment-calculator.test.jsghost/core/test/unit/server/services/members/members-api/utils/get-discount-window.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/portal/package.json
- apps/portal/test/unit/components/pages/AccountHomePage/paid-account-actions.test.js
- apps/portal/src/components/pages/AccountHomePage/components/paid-account-actions.js
| function getLastDiscountedPayment(nextBillingDate, discountEnd) { | ||
| const monthOffset = | ||
| ((discountEnd.getUTCFullYear() - nextBillingDate.getUTCFullYear()) * 12) + | ||
| (discountEnd.getUTCMonth() - nextBillingDate.getUTCMonth()); | ||
|
|
||
| let lastDiscountedBillingDate = getAnchoredBillingDate(nextBillingDate, monthOffset); | ||
|
|
||
| if (lastDiscountedBillingDate > discountEnd) { | ||
| lastDiscountedBillingDate = getAnchoredBillingDate(nextBillingDate, monthOffset - 1); | ||
| } | ||
|
|
||
| return lastDiscountedBillingDate; |
There was a problem hiding this comment.
current_period_end is not a stable billing anchor here.
Once a non-month-end subscription is shortened by February, this helper starts treating it as a month-end plan. For example, with start_date = 2025-01-28, current_period_end = 2025-02-28, and discount_end = 2025-04-01, this returns 2025-03-31, even though the last discounted renewal should be 2025-03-28. Please carry the real billing anchor into this calculation instead of inferring month-end semantics from current_period_end, and add a regression for the February-shortened case.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@ghost/core/core/server/services/members/members-api/utils/get-discount-window.js`
around lines 27 - 38, getLastDiscountedPayment is inferring a month-end anchor
from nextBillingDate/current_period_end which breaks when a subscription is
shortened by February; update the function to accept and use the real billing
anchor (pass through the original billing anchor value instead of deriving it
from nextBillingDate) when calling getAnchoredBillingDate so month-length
normalization doesn’t convert a non-month-end anchor into month-end, and add a
regression test that covers the February-shortened case (e.g., start_date
2025-01-28, current_period_end 2025-02-28, discount_end 2025-04-01 expecting
last discounted billing date 2025-03-28) to ensure the anchored calculations
preserve the original anchor.
closes https://linear.app/ghost/issue/BER-3413/account-page-date-mismatch-between-discount-end-billing-period - In Stripe, discount dates are anchored around the redemption date, not the billing date. However, we're interested in whether the next payment(s) are discounted. This aligns the next payment object discount end date with the last payment with a discount. - Example: - Bob's billing cycle is from Mar 3 to April 3 - Bob redeems a retention offer on Mar 1, that is repeating 3 months - Stripe will set discount start to Mar 1 and discount end to Jun 1 - Bob's next 3 payments will be discounted: Mar 3, Apr 3 and May 3, and we want to display "Ends May 3" (not "Ends Jun 1")
closes https://linear.app/ghost/issue/BER-3413/account-page-date-mismatch-between-discount-end-billing-period