Skip to content

🐛 Fixed member edited webhook missing tiers#27236

Merged
9larsons merged 14 commits intomainfrom
fix-member-edited-webhook-missing-subscriptions
Apr 8, 2026
Merged

🐛 Fixed member edited webhook missing tiers#27236
9larsons merged 14 commits intomainfrom
fix-member-edited-webhook-missing-subscriptions

Conversation

@9larsons
Copy link
Copy Markdown
Contributor

@9larsons 9larsons commented Apr 8, 2026

ref https://linear.app/ghost/issue/NY-1216/

Summary

The member.edited webhook payload was missing tiers because the webhook serializer used the raw Bookshelf model from the event, which only had labels loaded. The Admin API response was correct (it does a fresh read() with all relations), but the webhook was not.

The webhook serializer now loads products, labels, and newsletters on the member model before serializing, so tiers is included in the payload.

Also fixed the stripe-mocker to return the type field on price objects, matching real Stripe API behavior (needed for the comp flow to work in tests).

Root cause

The member.edited event fires with the Bookshelf model from memberRepository.update(), which only has labels in its withRelated. The serializeMember function only sets tiers when json.products exists on the model — and it didn't, because products was never loaded.

Payload: before → after

Before (comped member):

{
  "member": {
    "current": {
      "id": "...",
      "uuid": "...",
      "email": "comped-test@example.com",
      "name": "Comped Test Member",
      "status": "comped",
      "comped": true,
      "subscribed": true,
      "subscriptions": [],
      "labels": [],
      "avatar_image": null,
      "email_count": 0,
      "email_opened_count": 0,
      "email_open_rate": null,
      "last_seen_at": null,
      "created_at": "...",
      "updated_at": "..."
    },
    "previous": {
      "status": "free",
      "updated_at": "..."
    }
  }
}

After (comped member):

{
  "member": {
    "current": {
      "id": "...",
      "uuid": "...",
      "email": "comped-test@example.com",
      "name": "Comped Test Member",
      "status": "comped",
      "comped": true,
      "subscribed": true,
      "subscriptions": [],
      "tiers": [
        {
          "id": "...",
          "name": "Default Product",
          "slug": "default-product",
          "active": true,
          "type": "paid",
          "currency": "usd",
          "monthly_price": 500,
          "yearly_price": 5000
        }
      ],
      "labels": [],
      "newsletters": [...],
      "avatar_image": null,
      "email_count": 0,
      "email_opened_count": 0,
      "email_open_rate": null,
      "last_seen_at": null,
      "created_at": "...",
      "updated_at": "..."
    },
    "previous": {
      "status": "free",
      "updated_at": "..."
    }
  }
}

Changes:

  • Added: tiers — the Ghost products/tiers the member has access to (source of truth for member access)
  • Added: newsletters — the newsletters the member is subscribed to
  • Unchanged: subscriptions remains [] (Stripe relations not loaded; documenting/fixing this separately)

Test plan

  • New e2e test: creates a free member, comps them via Admin API, verifies the webhook includes tiers with correct shape (id, name, slug)
  • Existing member webhook tests (added, deleted, edited) pass with updated snapshots

9larsons added 2 commits April 8, 2026 11:44
The member.edited webhook payload is missing subscriptions when a
comp subscription is added via the Admin API. The API response
correctly includes both tiers and subscriptions, but the webhook
only includes tiers.

Also fixed stripe-mocker to return type field on price objects,
matching real Stripe API behavior.
The webhook serializer was using the raw model from the member.edited
event, which only had labels loaded. The API response was correct
because it does a fresh read() with all relations, but the webhook
payload was missing subscriptions, tiers, and other relation data.

Now loads the full set of member relations before serializing the
webhook payload.

closes https://linear.app/ghost/issue/ENG-000
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 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

Walkthrough

Added a MEMBER_WITH_RELATED field list and made member webhook serialization await model.load(MEMBER_WITH_RELATED) for members. For members, result.subscriptions is transformed into an array of minimal subscription reference objects containing id, status, and a reduced tier payload (including id and stripe_product_id) derived from related Stripe product/price associations. Added an E2E member.edited test that uses Stripe mocking, registers a webhook fixture, creates and updates a member with comped: true, and asserts non-empty tiers and minimized subscriptions in both the Admin response and webhook payload. Updated the Stripe mock to set decoded.object = 'price' and derive decoded.type as 'recurring' when decoded.recurring is present, otherwise 'one_time', when creating prices.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'tiers' but the PR objectives clarify the core issue is missing 'subscriptions' in the webhook payload; the actual fix ensures both are included. Update the title to accurately reflect the primary fix: 'Fixed member edited webhook missing subscriptions and tiers' or similar, as both are central to resolving the issue.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description is directly related to the changeset, explaining the root cause, implementation, and expected payload changes for the member.edited webhook fix.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-member-edited-webhook-missing-subscriptions

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.

❤️ Share

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

9larsons added 2 commits April 8, 2026 11:49
Only expose subscription id, stripe_price_id, and status in the
member webhook payload rather than full Stripe customer/payment
details. Tiers are still included from the products relation.
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/test/e2e-webhooks/members.test.js`:
- Around line 168-169: The test redundantly re-initializes webhookMockReceiver
here; remove the two lines that call mockManager.mockWebhookRequests() and await
webhookMockReceiver.mock(webhookURL) so the test uses the webhookMockReceiver
set up in beforeEach; ensure any required mocking is handled in the existing
beforeEach setup (referencing webhookMockReceiver and
mockManager.mockWebhookRequests()) and that this test only uses
webhookMockReceiver without reassigning or re-mocking.
🪄 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: bee51def-2a70-47a4-ab41-c9c38ad94616

📥 Commits

Reviewing files that changed from the base of the PR and between b0a1d51 and 15a9551.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-webhooks/__snapshots__/members.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • ghost/core/core/server/services/webhooks/serialize.js
  • ghost/core/test/e2e-webhooks/members.test.js
  • ghost/core/test/utils/stripe-mocker.js

Comment thread ghost/core/test/e2e-webhooks/members.test.js Outdated
9larsons added 5 commits April 8, 2026 12:01
Subscription objects now contain the Ghost tier id and stripe_product_id
rather than the stripe_price_id, making it easier for webhook consumers
to identify which tier a subscription belongs to.
The webhook was missing tiers because the products relation wasn't
loaded on the model at event time. Now loads products, labels, and
newsletters — no Stripe subscription data is exposed.
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/webhooks/serialize.js`:
- Around line 54-63: The minimal subscription payload created in
result.subscriptions (inside stripeSubscriptions.map) omits the price
identifier; update the mapping to include stripe_price_id (the price the member
is actually subscribed to) alongside the tier info so consumers can distinguish
monthly vs yearly plans. Locate the map callback that constructs the object with
id, status and tier (uses stripePrice/stripeProduct via
sub.related('stripePrice').related('stripeProduct')) and add a stripe_price_id
field (sourced from the related stripePrice model, e.g.
sub.related('stripePrice').get('stripe_price_id')) into the returned
subscription shape while keeping the existing tier.{id,stripe_product_id}
values.
🪄 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: 9c37a6ba-46cb-4520-89c3-e4463378bc0e

📥 Commits

Reviewing files that changed from the base of the PR and between b2cdae5 and e40e562.

📒 Files selected for processing (2)
  • ghost/core/core/server/services/webhooks/serialize.js
  • ghost/core/test/e2e-webhooks/members.test.js

Comment thread ghost/core/core/server/services/webhooks/serialize.js Outdated
9larsons added 2 commits April 8, 2026 13:04
Without Stripe relations loaded, subscriptions was always an empty
array which falsely implies the member has no subscriptions. Removed
it entirely — tiers is the source of truth for member access.
@9larsons 9larsons changed the title 🐛 Fixed member edited webhook missing subscriptions 🐛 Fixed member edited webhook missing tiers Apr 8, 2026
@9larsons 9larsons requested a review from EvanHahn April 8, 2026 19:15
Copy link
Copy Markdown
Contributor

@EvanHahn EvanHahn left a comment

Choose a reason for hiding this comment

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

Several non-blocking comments. The subscriptions removal is my biggest question.

Comment thread ghost/core/test/e2e-webhooks/members.test.js Outdated
Comment thread ghost/core/test/e2e-webhooks/members.test.js Outdated
Comment thread ghost/core/test/utils/stripe-mocker.js Outdated
Comment thread ghost/core/core/server/services/webhooks/serialize.js Outdated
Comment thread ghost/core/core/server/services/webhooks/serialize.js Outdated
9larsons added 2 commits April 8, 2026 15:29
- Restored subscriptions field in webhook payload (breaking change concern)
- Used switch statement for docName checks in serialize.js
- Used .example domain in test webhook URL (RFC 2606)
- Added tier shape assertions (id, name, slug)
- Combined nested conditional in stripe-mocker price handling
@9larsons 9larsons enabled auto-merge (squash) April 8, 2026 21:29
The tiers array now appears in member webhook payloads, so the
click-tracking test needs matchers for the dynamic fields (ids,
timestamps) in the tier objects.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

@9larsons 9larsons merged commit 6ddc07a into main Apr 8, 2026
39 checks passed
@9larsons 9larsons deleted the fix-member-edited-webhook-missing-subscriptions branch April 8, 2026 22:55
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