Skip to content

Comments

fix: prevent past_due status cascading to newly paid subscriptions#782

Open
lutz-grex wants to merge 1 commit intouseautumn:devfrom
lutz-grex:fix/past-due-status-cascading
Open

fix: prevent past_due status cascading to newly paid subscriptions#782
lutz-grex wants to merge 1 commit intouseautumn:devfrom
lutz-grex:fix/past-due-status-cascading

Conversation

@lutz-grex
Copy link

@lutz-grex lutz-grex commented Feb 20, 2026

Summary

When a customer has an unpaid invoice, all products on the same Stripe subscription were marked as past_due — including newly purchased products with fully paid invoices. This blocked users from accessing the app even with a valid, paid subscription.

Root cause: new products were merged onto unhealthy (past_due) Stripe subscriptions via getTargetSubscriptionCusProduct, then syncCustomerProductStatus applied the subscription-level past_due status to all products uniformly.

Fix:

  • Reject unhealthy subscriptions (past_due, incomplete, unpaid, paused) as merge targets in fetchStripeSubscriptionForBilling, forcing creation of a new Stripe subscription for the new product
  • Clean up expired product items from the old subscription after transitioning to prevent future charges for the expired product

Related Issues

None identified yet — this was reported by a user

Type of Change

  • [ x] Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Other (please describe):

Checklist

  • [x ] I have read the CONTRIBUTING.md
  • [x ] My code follows the code style of this project
  • I have added tests where applicable
  • [ x] I have tested my changes locally
  • I have linked relevant issues
  • I have added screenshots for UI changes (if applicable)

Screenshots (if applicable)

Additional Context

Changed files

  • classifyStripeSubscriptionUtils.ts — Added isStripeSubscriptionUnhealthy() helper that detects payment-problematic statuses (past_due, incomplete, incomplete_expired, unpaid, paused)
  • fetchStripeSubscriptionForBilling.ts — Returns undefined when the target subscription is unhealthy, forcing the billing system to create a new Stripe subscription instead of merging onto the problematic one. Includes an info-level log for production traceability.
  • cleanupOldSubscriptionItems.ts (new) — After transitioning to a new subscription, removes the expired product's items from the old subscription. Cancels the old subscription entirely if no items remain. Uses proration_behavior: "none" to avoid generating credits for already-unpaid items.
  • executeStripeBillingPlan.ts — Calls cleanupOldSubscriptionItems() after the subscription action completes.

Edge cases handled

  • Downgrades (end_of_cycle): Old product is marked as canceling (not Expired), so cleanup is skipped — items stay on the old subscription until end of cycle.
  • Same-subscription transitions: Guard clause skips cleanup when old and new subscription IDs match — the existing item diff handles it.
  • Cancel actions: Early if (!newStripeSubscription) return exit prevents cleanup from running when no new subscription was created.
  • Already-canceled old subscriptions: isStripeSubscriptionCanceled guard prevents redundant Stripe API calls.

Summary by cubic

Prevented past_due status from cascading to newly paid products by avoiding merges onto unhealthy Stripe subscriptions and cleaning up old subscription items after moving. Users with a new paid product are no longer blocked by an unpaid invoice on another product.

  • Bug Fixes
    • Skip unhealthy Stripe subscriptions (past_due, incomplete, incomplete_expired, unpaid, paused) as merge targets in fetchStripeSubscriptionForBilling; create a new subscription instead.
    • After transitioning, remove the expired product’s items from the old subscription or cancel it if empty, with no proration.

Written for commit ab8d8ae. Summary will update on new commits.

Greptile Summary

Prevents newly purchased products from being blocked when customers have unpaid invoices on existing subscriptions. Previously, new products merged onto unhealthy (past_due) Stripe subscriptions inherited the payment-problematic status, blocking legitimate access.

Key Changes:

Bug fixes

  • Reject unhealthy subscriptions (past_due, incomplete, incomplete_expired, unpaid, paused) as merge targets in fetchStripeSubscriptionForBilling, forcing creation of new Stripe subscriptions for new products
  • Clean up expired product items from old subscriptions after transitioning to prevent future charges for already-expired products

Improvements

  • Added isStripeSubscriptionUnhealthy() utility to centralize detection of payment-problematic subscription statuses
  • Comprehensive edge case handling: skips cleanup for downgrades (end-of-cycle), same-subscription transitions, cancel actions, and already-canceled subscriptions

Confidence Score: 4/5

  • This PR is safe to merge with thorough testing of the cleanup flow
  • The fix correctly addresses the root cause by preventing unhealthy subscription merges. The cleanup logic has comprehensive guard clauses for edge cases. However, the cleanup functionality introduces new Stripe API calls and subscription modifications that should be tested with real payment scenarios, particularly around the proration_behavior and item removal logic
  • Pay close attention to cleanupOldSubscriptionItems.ts - ensure the item removal logic handles all edge cases correctly in production

Important Files Changed

Filename Overview
server/src/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils.ts Added isStripeSubscriptionUnhealthy() helper to detect payment-problematic subscription statuses
server/src/internal/billing/v2/providers/stripe/setup/fetchStripeSubscriptionForBilling.ts Rejects unhealthy subscriptions as merge targets, forcing creation of new subscriptions for new products
server/src/internal/billing/v2/providers/stripe/execute/cleanupOldSubscriptionItems.ts New cleanup function to remove expired product items from old subscriptions; edge case handling looks solid with one potential issue
server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts Integrates cleanup call after subscription action completes

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Customer purchases new product] --> B{Existing subscription?}
    B -->|No| C[Create new subscription]
    B -->|Yes| D{Check subscription health}
    D -->|Healthy: active/trialing| E[Merge onto existing subscription]
    D -->|Unhealthy: past_due/incomplete/unpaid/paused| F[Force new subscription creation]
    F --> G[Execute billing plan on new subscription]
    G --> H{Old product status?}
    H -->|Expired| I[cleanupOldSubscriptionItems]
    H -->|Canceling| J[Skip cleanup - handled at period end]
    I --> K{Remove items from old subscription}
    K --> L{Any items remaining?}
    L -->|No| M[Cancel old subscription entirely]
    L -->|Yes| N[Remove only expired product items]
    E --> O[Update existing subscription]
    C --> P[Product active on new subscription]
    M --> P
    N --> P
    O --> P
Loading

Last reviewed commit: ab8d8ae

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

When a customer has an unpaid invoice, all products on the same Stripe
subscription were marked as past_due — including newly purchased products
with fully paid invoices. This blocked users from accessing the app even
with a valid, paid subscription.

Root cause: new products were merged onto unhealthy (past_due) Stripe
subscriptions via getTargetSubscriptionCusProduct, then
syncCustomerProductStatus applied the subscription-level past_due status
to all products uniformly.

Fix:
- Reject unhealthy subscriptions (past_due, incomplete, unpaid, paused)
  as merge targets in fetchStripeSubscriptionForBilling, forcing creation
  of a new Stripe subscription for the new product
- Clean up expired product items from the old subscription after
  transitioning to prevent future charges for the expired product
@vercel
Copy link

vercel bot commented Feb 20, 2026

@lutz-grex is attempting to deploy a commit to the Autumn Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

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.

1 participant