Skip to content

🐛 Fixed orphaned Stripe prices when importing members by tier#28955

Merged
9larsons merged 2 commits into
TryGhost:mainfrom
Aubaid12:fix/22115-archive-orphaned-stripe-price
Jul 2, 2026
Merged

🐛 Fixed orphaned Stripe prices when importing members by tier#28955
9larsons merged 2 commits into
TryGhost:mainfrom
Aubaid12:fix/22115-archive-orphaned-stripe-price

Conversation

@Aubaid12

Copy link
Copy Markdown
Contributor

fixes #22115

Why

@9larsons reproduced this on main and tagged it bug + help wanted. Importing members by tier could leave orphaned, unused prices piling up on a Stripe product.

What it does

When you import a member with an import_tier and a Stripe customer, forceStripeSubscriptionToProduct() creates a new price on the tier's product, then updates the customer's subscription to use it. That new price is only recorded for archival (it gets cleaned up after the import) once the update succeeds. So if the update throws, for example because Stripe rejects it or the subscription is schedule-managed, the price is never archived and stays orphaned on the product, building up on every import.

The subscription update is now wrapped so that on failure the just-created price is archived before the error propagates. The original error always surfaces, even if archiving itself fails (that's logged as a warning). This is what @9larsons suggested on the issue: "archive the just-created price if the update throws."

Why Ghost users/developers need it

Site owners and integrations that import paid members by tier end up with a growing list of unused prices on their Stripe products, which is confusing to manage. This keeps the price list clean.

Tests

5 unit cases covering the full create → update → archive path: a successful update does not archive; a failed update archives the new price; the original error still surfaces if archiving also fails; an existing-price update failure does not archive (nothing was created); and a createPrice failure does not update or archive. All 17 tests in the file pass.

  • I've read and followed the Contributor Guide
  • I've written an automated test to prove my change works

fixes TryGhost#22115

When importing members with an import_tier, the Stripe utils create a new price on
the tier's product and then update the customer's subscription to use it. If that
update threw (Stripe rejecting it, or a schedule-managed subscription), the
just-created price was never recorded for archival and was left orphaned on the
product, accumulating across imports. As suggested on the issue, the price is now
archived if the subscription update fails, and the original error is surfaced
regardless of whether archiving succeeds.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 172fc7ce-9bb5-4e59-b815-d12ffa8b8ab9

📥 Commits

Reviewing files that changed from the base of the PR and between 2e60214 and 0643c00.

📒 Files selected for processing (1)
  • ghost/core/test/unit/server/services/members/importer/members-csv-importer-stripe-utils.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/unit/server/services/members/importer/members-csv-importer-stripe-utils.test.js

Walkthrough

forceStripeSubscriptionToProduct now wraps updateSubscriptionItemPrice in a try/catch. If the subscription update fails after creating a new Stripe price, it archives that price with updatePrice(active: false), logs a warning if archival fails, and re-throws the original error. The unit tests now cover success, update failure with cleanup, cleanup failure, no-new-price paths, and createPrice failure.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: preventing orphaned Stripe prices during member imports by tier.
Description check ✅ Passed The description is directly about the same Stripe price cleanup fix and its regression tests.
Linked Issues check ✅ Passed The change addresses #22115 by archiving newly created Stripe prices when subscription updates fail, while preserving the original error.
Out of Scope Changes check ✅ Passed The code and tests are focused on the reported import-by-tier Stripe price orphaning bug with no obvious unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

ref TryGhost#28955

- review feedback on the PR: the four new rejection tests captured errors with manual try/catch blocks while every other rejection test in this file uses await assert.rejects, so they now use assert.rejects with an identity validator
- the new happy-path test duplicated the existing 'creates a new price' test for a single assertion, so that assertion (updatePrice never called on success) is folded into the existing test instead
@9larsons
9larsons enabled auto-merge (squash) July 2, 2026 16:15

@9larsons 9larsons left a comment

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.

Thanks for this! Updated some of the tests to use asserts.rejects.

@nx-cloud

nx-cloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 0643c00

Command Status Duration Result
nx run ghost:test:ci:integration ✅ Succeeded 2m 42s View ↗
nx run ghost:test:integration ✅ Succeeded 2m 41s View ↗
nx run ghost:test:legacy ✅ Succeeded 3m 5s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded <1s View ↗
nx run ghost:test:e2e ✅ Succeeded 2m 24s View ↗
nx run-many -t test:unit -p ghost ✅ Succeeded 29s View ↗
nx run-many -t lint -p ghost ✅ Succeeded 37s View ↗
nx run @tryghost/admin:build ✅ Succeeded 7s View ↗
Additional runs (2) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-02 16:25:37 UTC

@9larsons
9larsons merged commit 614249a into TryGhost:main Jul 2, 2026
40 checks passed
@Aubaid12

Aubaid12 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

@9larsons Appreciate the review and merge! Noted on assert.rejects for next time 🙌

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.

Import by tier can create lots of prices in Stripe if prices don't match

2 participants