🐛 Fixed orphaned Stripe prices when importing members by tier#28955
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Walkthrough
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ 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 |
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
left a comment
There was a problem hiding this comment.
Thanks for this! Updated some of the tests to use asserts.rejects.
|
| 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 Appreciate the review and merge! Noted on assert.rejects for next time 🙌 |

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_tierand 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.