Skip to content

Conversation

@cmraible
Copy link
Collaborator

@cmraible cmraible commented Jan 29, 2026

ref https://linear.app/ghost/issue/NY-891/before-ga-double-check-that-only-members-createdupdated-through-portal

Summary

  • Added unit tests for member-bread-service.add() to verify context is passed to linkStripeCustomer
  • Fixed bug where context was not included in sharedOptions when calling linkStripeCustomer

Problem

When an admin creates a member via the Admin API with a stripe_customer_id, the paid welcome email was incorrectly being sent. This happened because:

  1. member-bread-service.add() calls linkStripeCustomer() with sharedOptions
  2. sharedOptions only included transacting, not context
  3. linkStripeCustomer()linkSubscription() uses context to determine the source
  4. Without context, _resolveContextSource({}) defaulted to 'member'
  5. Since 'member' is in WELCOME_EMAIL_SOURCES, the paid welcome email was queued

Fix

Updated sharedOptions in member-bread-service.js to include context:

const sharedOptions = {
    ...(options.transacting && {transacting: options.transacting}),
    ...(options.context && {context: options.context})
};

Now the source correctly resolves to 'admin' for admin-created members, and no welcome email is sent.


Side effect in members API tests

This change also corrects a bug in the staff notifications behavior, which was being hidden by the bug described above. Previously in the members API tests we were asserting that a "Paid subscription started" staff notification was sent. This was the case because, due to the bug described and fixed in this PR, the member in these tests was being incorrectly created with source = 'member'.

Now these members are correctly created with source = 'admin', and we correctly do not send the "Paid subscription started" notification anymore.


Testing

Signup as a paid member via portal

  • Signup via portal for a paid subscription
  • Member should receive welcome email
  • Staff user should receive "Paid subscription started" email

Add a paid member via Admin API

  • Create a new Stripe customer in your stripe dashboard
  • Add payment method using the 4242 4242 4242 4242 test card
  • Add an active subscription to the customer
  • Grab the stripe customer ID
  • Grab your staff access token, or an integration's Admin API token
  • Create a new member by POSTing to /ghost/api/admin/members, using your access token to authenticate and passing the stripe_customer_id
  • Member should be created, with status = paid
  • Member should not receive welcome email
  • Staff user should receive "New free member" email. Note: this is a pre-existing bug. This shouldn't really happen, but it's not new behavior introduced by this PR
  • Staff user should receive "Paid subscription started" email

Add a paid member via members CSV import

  • Create a new Stripe customer in your stripe dashboard
  • Add payment method
  • Add an active subscription to the customer
  • Grab the stripe customer id
  • Create a members CSV import with a unique (i.e. not already in use by existing member) email address, and include the stripe_customer_id
  • Import the CSV file in the members screen
  • Member should be created, with status = paid
  • Member should not receive welcome email
  • Staff user should not receive "Paid subscription started" email
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Walkthrough

This pull request modifies the option propagation mechanism in MemberBreadService to conditionally spread both transacting and context parameters to downstream service calls (such as linkStripeCustomer), rather than conditionally including only transacting. The production code change affects 11 lines in the members bread service. Corresponding test updates add new unit tests for MemberBreadService.add to verify context and transacting propagation, update an existing webhook test to verify attribution fields, and add assertions in CSV importer tests to confirm the importer context flag is passed through correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix in the changeset: preventing paid welcome emails from being incorrectly sent when admins create members via the API with a Stripe customer ID.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the problem, root cause, fix, and testing instructions with clear context.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-welcome-emails-sent-to-paid-members-from-api

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.40.5)
ghost/core/test/e2e-api/members/webhooks.test.js

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.

@cmraible cmraible changed the title 🐛 Fixed paid welcome emails incorrectly sent to admin-created members Jan 29, 2026
When members are created via Admin API with stripe_customer_id:
- Session auth (context.user) → source='admin' → no staff notification
- API key auth (context.api_key) → source='api' → staff notification sent

This is correct because:
- Staff notifications filter: ['api', 'member'] only
- Admins manually linking Stripe customers already know about the subscription
cmraible added a commit that referenced this pull request Jan 29, 2026
Documents all staff notification email types, their triggers, and the
source filtering logic that determines when notifications are sent.

refs #26086
Documents all staff notification email types, their triggers, and the
source filtering logic that determines when notifications are sent.

refs #26086
@cmraible cmraible force-pushed the fix-welcome-emails-sent-to-paid-members-from-api branch from ef12707 to 2391d40 Compare January 29, 2026 22:48
@cmraible
Copy link
Collaborator Author

I did some digging and found this issue from the original implementation: https://github.com/TryGhost/Product/issues/1864

The expected behavior for triggering email alerts for members created via different sources -

Importer = our import tooling ❌
Admin = manually in the UI ❌
API = external / zapier ✅
Member = member triggered ✅

So I think this actually is correct, and the members webhook tests were only passing because of this bug

ref TryGhost/Product#1864

These tests verify the intended behavior documented in the referenced issue:
- source='admin' (session auth) → NO staff notification emails
- source='api' (API key auth) → Staff notification emails ARE sent
- source='member' (checkout webhook) → Staff notification emails ARE sent
- source='import' → NO staff notification emails

Also added `sentEmailCount` assertion to the mock manager for verifying
that zero emails were sent.
Comment on lines -382 to -386
mockManager.assert.sentEmail({
subject: /Paid subscription started: Cancel me at the end of the billing cycle/,
to: 'jbloggs@example.com'
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This assertion should not have been here in the first place, but it was required in the test due to the underlying bug we found.

The member created above in this test now correctly has source = 'admin', and Ghost correctly should not send a "Paid subscription started" notification email in this case. There isn't really a behavioral change here, because there isn't any UI in Admin that allows creating paid members with stripe_customer_id.

Comment on lines -548 to -552
mockManager.assert.sentEmail({
subject: /Paid subscription started: Cancel me now/,
to: 'jbloggs@example.com'
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ditto here

@cmraible cmraible marked this pull request as ready for review January 30, 2026 01:22
@cmraible cmraible requested a review from troyciesco January 30, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants