Skip to content

Folded gift signups into Monthly / Annual on the paid subscription breakdown chart#27624

Merged
mike182uk merged 6 commits intomainfrom
growth-pie-gift-members
Apr 30, 2026
Merged

Folded gift signups into Monthly / Annual on the paid subscription breakdown chart#27624
mike182uk merged 6 commits intomainfrom
growth-pie-gift-members

Conversation

@mike182uk
Copy link
Copy Markdown
Member

@mike182uk mike182uk commented Apr 29, 2026

ref https://linear.app/ghost/issue/BER-3491

  • the "Paid subscription breakdown" pie chart was showing "Gift" as a separate slice alongside Monthly/Annual/Complimentary, which mixed billing-period semantics with member-status semantics
  • gifts have an inherent cadence (month/year) stored on the gifts table, so they belong in the corresponding billing-period bucket rather than as their own slice
  • surfaced gifts through the existing subscription-stats endpoint by adding fetchAllGiftDeltas (gift redemptions as signups, consumed/expired/refunded gifts as cancellations) so the chart's existing per-cadence aggregation picks them up with no special-casing in the frontend
  • preferred extending subscription-stats over a new endpoint to keep the change small; the side effect is that gifts now also count toward the "Paid subscriptions" daily new/cancelled chart, which is consistent with treating gifts as paid acquisitions

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6aa8df8-43f0-40fe-bfe8-13a44d94528e

📥 Commits

Reviewing files that changed from the base of the PR and between d0535c3 and 3af6ee1.

📒 Files selected for processing (1)
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js

Walkthrough

Frontend: removed gift-specific logic and the labs.giftSubscriptions flag from the new-subscribers-cadence component; gift signups are now included in existing cadence signups and the UI no longer renders a separate "Gift" slice. Backend: SubscriptionStatsService added fetchAllGiftDeltas(), aggregateCounts and aggregateDeltas; getSubscriptionHistory and fetchSubscriptionCounts now include redeemed-gift deltas/counts, concatenate paid and gift results, aggregate rows by (date, tier, cadence), and sort by date before applying rollback logic. Unit tests updated to cover gift lifecycle and merged deltas.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: gifts are now folded into Monthly/Annual cadences in the paid subscription breakdown chart, directly reflecting the core objective.
Description check ✅ Passed The description clearly explains the rationale for the change, implementation approach, and side effects. It is fully related to the changeset and provides helpful context.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch growth-pie-gift-members

Review rate limit: 2/5 reviews remaining, refill in 24 minutes and 1 second.

Comment @coderabbitai help to get the list of available commands and usage tips.

@mike182uk mike182uk changed the title Folded gift signups into Monthly / Annual on the paid subscription br… Folded gift signups into Monthly / Annual on the paid subscription breakdown chart Apr 29, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/stats/subscription-stats-service.js`:
- Around line 15-17: The code concatenates paidDeltas and giftDeltas into
subscriptionDeltaEntries which breaks chronological order before
getSubscriptionHistory consumes the stream; before calling
getSubscriptionHistory (and similarly where else paid/gift arrays are merged
around lines 158-185) merge or sort the combined array by the delta timestamp
(ascending) so deltas are applied in time order — e.g., replace simple concat
with a merge/sort of fetchAllSubscriptionDeltas() and fetchAllGiftDeltas()
results (use the delta date field used by getSubscriptionHistory, and add a
secondary sort key for same-day tie-breaking if needed).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6d6ddb4-c86a-4b12-b96c-ee361167ee05

📥 Commits

Reviewing files that changed from the base of the PR and between d153b28 and 24966d1.

📒 Files selected for processing (4)
  • apps/stats/src/views/Stats/Growth/components/new-subscribers-cadence.tsx
  • apps/stats/test/unit/components/growth/new-subscribers-cadence.test.tsx
  • ghost/core/core/server/services/stats/subscription-stats-service.js
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js

Comment thread ghost/core/core/server/services/stats/subscription-stats-service.js Outdated
.select(knex.raw('COUNT(id) as count'))
.groupByRaw('DATE(redeemed_at), tier_id, cadence'),
knex('gifts')
.whereIn('status', ['consumed', 'expired', 'refunded'])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A gift can be expired or refunded without ever been redeemed - in this case, they shouldn't be counted as cancellations?

Adding .whereNotNull('redeemed_at') to the cancellations query should do the trick

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, fixed 👍

@mike182uk mike182uk force-pushed the growth-pie-gift-members branch from 24966d1 to 93f94eb Compare April 29, 2026 20:04
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/stats/subscription-stats-service.js`:
- Around line 20-22: After merging paidDeltas and giftDeltas into
subscriptionDeltaEntries, aggregate entries with the same date, tier and cadence
into a single entry (summing numeric delta fields like
countDelta/revenueDelta/etc.) before sorting and before the rollback loop;
update the code that creates subscriptionDeltaEntries (and the analogous block
at lines 187-190) to group by the composite key
(moment(date).format('YYYY-MM-DD') or equivalent normalized date, tier, cadence)
and replace duplicates with a single aggregated entry so the subsequent rollback
history loop emits one snapshot per date/tier/cadence.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 915e1762-2a0b-48bc-85b5-8dc19c0677a4

📥 Commits

Reviewing files that changed from the base of the PR and between 24966d1 and 93f94eb.

📒 Files selected for processing (4)
  • apps/stats/src/views/Stats/Growth/components/new-subscribers-cadence.tsx
  • apps/stats/test/unit/components/growth/new-subscribers-cadence.test.tsx
  • ghost/core/core/server/services/stats/subscription-stats-service.js
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/stats/test/unit/components/growth/new-subscribers-cadence.test.tsx
  • apps/stats/src/views/Stats/Growth/components/new-subscribers-cadence.tsx
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js

Comment thread ghost/core/core/server/services/stats/subscription-stats-service.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/stats/subscription-stats-service.js`:
- Around line 15-22: The current baseline totals used to seed the rollback
(values from fetchSubscriptionCounts() -> counts / meta.totals) don't include
active redeemed gifts, causing undercounted histories; update the logic so the
starting totals include active gifts too—either by extending
fetchSubscriptionCounts() to union in active gift rows (e.g., gifts WHERE
status='redeemed') mapped to the same schema, or by calling a new helper (e.g.,
fetchActiveGiftTotals()) and merging its results into counts/meta.totals before
using subscriptionDeltaEntries (from aggregateDeltas and fetchAllGiftDeltas) to
roll back; ensure the merged totals use the same tier/cadence keys as the
subscription counts so the rollback loop produces correct snapshots.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 87d6e37e-28dc-4f5d-b35e-8c3bd48567cd

📥 Commits

Reviewing files that changed from the base of the PR and between 93f94eb and 3bcce5e.

📒 Files selected for processing (2)
  • ghost/core/core/server/services/stats/subscription-stats-service.js
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js

Comment thread ghost/core/core/server/services/stats/subscription-stats-service.js
…eakdown chart

ref https://linear.app/ghost/issue/BER-3491

- the "Paid subscription breakdown" pie chart was showing "Gift" as a separate slice alongside Monthly/Annual/Complimentary, which mixed billing-period semantics with member-status semantics
- gifts have an inherent cadence (month/year) stored on the gifts table, so they belong in the corresponding billing-period bucket rather than as their own slice
- surfaced gifts through the existing subscription-stats endpoint by adding `fetchAllGiftDeltas` (gift redemptions as signups, consumed/expired/refunded gifts as cancellations) so the chart's existing per-cadence aggregation picks them up with no special-casing in the frontend
- preferred extending subscription-stats over a new endpoint to keep the change small; the side effect is that gifts now also count toward the "Paid subscriptions" daily new/cancelled chart, which is consistent with treating gifts as paid acquisitions
@mike182uk mike182uk force-pushed the growth-pie-gift-members branch from 3bcce5e to d0535c3 Compare April 30, 2026 07:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ghost/core/test/unit/server/services/stats/subscriptions.test.js (1)

396-427: ⚡ Quick win

Cover the remaining redeemed gift cancellation branches.

The new suite exercises consumed and pre-redemption refunded, but it still misses redeemed→expired and redeemed→refunded. A regression in either timestamp/filter path would still pass here.

Possible extension
+            await db('gifts').insert([
+                {
+                    id: 'g-expired',
+                    tier_id: 'basic',
+                    cadence: 'year',
+                    status: 'expired',
+                    redeemed_at: '1970-01-01T00:00:00.000Z',
+                    expired_at: '1970-01-03T00:00:00.000Z'
+                },
+                {
+                    id: 'g-refunded-after-redemption',
+                    tier_id: 'basic',
+                    cadence: 'year',
+                    status: 'refunded',
+                    redeemed_at: '1970-01-01T00:00:00.000Z',
+                    refunded_at: '1970-01-04T00:00:00.000Z'
+                }
+            ]);
+
+            assert.equal(sumWhere('basic', 'year', '1970-01-03', 'cancellations'), 1);
+            assert.equal(sumWhere('basic', 'year', '1970-01-04', 'cancellations'), 1);

Also applies to: 477-497

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/server/services/stats/subscriptions.test.js` around
lines 396 - 427, Add tests covering redeemed→expired and redeemed→refunded gift
branches: when inserting test gifts via db('gifts').insert (the same area that
currently creates 'g-month' and 'g-year'), add entries for a redeemed gift that
later becomes expired (set redeemed_at and expired_at timestamps) and a redeemed
gift that later becomes refunded (set redeemed_at and refunded_at timestamps).
Then extend the assertions that use sumWhere (helper alongside createTiers,
createEvent, insertEvents and the call to
SubscriptionStatsService.getSubscriptionHistory) to check that signups include
those redeemed rows on their redeemed_at dates and that cancellations (or the
appropriate cancellation-type field) appear on the expired_at and refunded_at
dates respectively; repeat the same coverage for the similar test block around
lines 477-497.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ghost/core/test/unit/server/services/stats/subscriptions.test.js`:
- Around line 396-427: Add tests covering redeemed→expired and redeemed→refunded
gift branches: when inserting test gifts via db('gifts').insert (the same area
that currently creates 'g-month' and 'g-year'), add entries for a redeemed gift
that later becomes expired (set redeemed_at and expired_at timestamps) and a
redeemed gift that later becomes refunded (set redeemed_at and refunded_at
timestamps). Then extend the assertions that use sumWhere (helper alongside
createTiers, createEvent, insertEvents and the call to
SubscriptionStatsService.getSubscriptionHistory) to check that signups include
those redeemed rows on their redeemed_at dates and that cancellations (or the
appropriate cancellation-type field) appear on the expired_at and refunded_at
dates respectively; repeat the same coverage for the similar test block around
lines 477-497.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c2530157-32c0-47f7-907a-25b63ec044ee

📥 Commits

Reviewing files that changed from the base of the PR and between 3bcce5e and d0535c3.

📒 Files selected for processing (4)
  • apps/stats/src/views/Stats/Growth/components/new-subscribers-cadence.tsx
  • apps/stats/test/unit/components/growth/new-subscribers-cadence.test.tsx
  • ghost/core/core/server/services/stats/subscription-stats-service.js
  • ghost/core/test/unit/server/services/stats/subscriptions.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/stats/src/views/Stats/Growth/components/new-subscribers-cadence.tsx
  • ghost/core/core/server/services/stats/subscription-stats-service.js

@mike182uk mike182uk merged commit 1f185d3 into main Apr 30, 2026
43 checks passed
@mike182uk mike182uk deleted the growth-pie-gift-members branch April 30, 2026 11:38
mike182uk added a commit that referenced this pull request May 7, 2026
ref https://linear.app/ghost/issue/BER-3615
ref #27624
ref #27703

- the "Paid subscriptions" bar chart and "Paid subscription breakdown"
pie chart were counting gift redemptions as signups and gift end-of-life
events as cancellations, which mixed gift activity into charts that are
meant to track paid Stripe activity only
- gifts now flow through their own member status (`gift`) and surface in
the Paid members KPI tooltip instead, so duplicating them in the per-
cadence/per-tier breakdowns double-counted activity and made the bar and
pie charts disagree
- reverted the gift-aware additions to subscription-stats-service so
`/stats/subscriptions` returns paid Stripe deltas and counts only;
`gift`→`paid` upgrades still appear via the regular paid subscription
event flow once the trial converts and MRR begins
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants