Skip to content

improvement(billing): treat past_due state correctly#3750

Merged
icecrasher321 merged 4 commits intostagingfrom
fix/status-check
Mar 25, 2026
Merged

improvement(billing): treat past_due state correctly#3750
icecrasher321 merged 4 commits intostagingfrom
fix/status-check

Conversation

@icecrasher321
Copy link
Collaborator

Summary

Past_Due should be treated as someone still on the subscription but blocked. Currently, code path downgrades on status change to this.

Type of Change

  • Bug fix
  • Other: UX Improvement

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Mar 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 25, 2026 5:19am

Request Review

@cursor
Copy link

cursor bot commented Mar 25, 2026

PR Summary

Medium Risk
Updates subscription status semantics and billing-gate checks across multiple APIs and UI surfaces, which can change who is allowed to manage plans/seats/limits and access paid features. Risk is moderate due to broad impact on billing enforcement, but changes are mostly centralized into shared helpers with added tests.

Overview
Refactors billing status handling to treat Stripe past_due as a paid entitlement while requiring active (and not billingBlocked) for usable access.

Adds centralized helpers/constants in billing/subscriptions/utils (plus tests) and a new billing/core/access module to compute effective billing blocks (including org-owner blocks). Updates many API routes and UI gating to use these helpers (plan switching, seats updates, portal lookup, audit logs auth, credits purchase/issuance, usage limit edits, log retention cleanup, navigation gating), replacing scattered status === 'active' checks and introducing a requiresMax/disabled-state behavior in settings navigation.

Written by Cursor Bugbot for commit 6289abf. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR fixes a billing state machine bug where past_due subscriptions were being treated the same as cancelled/free subscriptions, triggering erroneous downgrades. The fix introduces a two-tier status model:

  • ENTITLED statuses (active, past_due): The subscription row still represents a paid plan — used for DB filters that guard against duplicate subscription creation, org cleanup, and plan-preservation logic.
  • USABLE statuses (active only): The user has full feature access — used for guards on seat changes, credit purchasing, usage limit editing, inbox access, and threshold billing.

Key changes:

  • New apps/sim/lib/billing/subscriptions/utils.ts exports (hasPaidSubscriptionStatus, hasUsableSubscriptionStatus, hasUsableSubscriptionAccess, ENTITLED_SUBSCRIPTION_STATUSES, USABLE_SUBSCRIPTION_STATUSES) establish a single source of truth for status semantics.
  • getEffectiveBillingStatus / isOrganizationBillingBlocked extracted from route-level code into apps/sim/lib/billing/core/access.ts and reused across ~10 call sites that previously had ad-hoc inline implementations.
  • Client-side: getSubscriptionAccessState wraps getSubscriptionStatus and adds hasUsableTeamAccess, hasUsableEnterpriseAccess, hasUsableMaxAccess flags, propagating blocked state to UI gating throughout settings components.
  • Navigation sidebar refactored from filter to flatMap so plan-gated items (Sim Mailer, Email Polling, SSO, Access Control) are now shown disabled rather than hidden, improving discoverability.
  • hasActiveSubscription renamed to hasPaidSubscription site-wide to clarify that the check also includes past_due rows.
  • A vitest suite is added for the new status helpers covering the key behavioral contract.
  • One minor note: the stale comment in webhooks/subscription.ts says "truly active" but now includes past_due; and disabled nav items in the expanded sidebar have no tooltip explaining the plan requirement.

Confidence Score: 4/5

  • Safe to merge — the past_due two-tier model is coherent and consistently applied across all billing paths; the two P2 findings (stale comment, missing disabled tooltip) are non-blocking.
  • The core logic is sound: ENTITLED vs USABLE status constants are used correctly and consistently across DB queries, server-side guards, and client-side gating. The refactored getEffectiveBillingStatus extraction removes duplicated inline logic. Tests cover the key behavioral contract. The only open concern from prior review threads (enterprise-plus in sqlIsPaid) is pre-existing and scoped to the log-cleanup route, not introduced by this PR. The two new P2 comments are cosmetic/UX improvements that don't affect correctness.
  • apps/sim/lib/billing/webhooks/subscription.ts (stale comment), apps/sim/app/workspace/.../settings-sidebar.tsx (disabled item UX)

Important Files Changed

Filename Overview
apps/sim/lib/billing/subscriptions/utils.ts Core billing status helpers introduced: ENTITLED_SUBSCRIPTION_STATUSES (['active','past_due']), USABLE_SUBSCRIPTION_STATUSES (['active']), hasPaidSubscriptionStatus, hasUsableSubscriptionStatus, hasUsableSubscriptionAccess. Existing checkEnterprisePlan/checkProPlan/checkTeamPlan correctly updated to use hasPaidSubscriptionStatus.
apps/sim/lib/billing/core/access.ts New module extracting getEffectiveBillingStatus and isOrganizationBillingBlocked from route-level code into a reusable server-side module. Logic is equivalent to what was previously in billing/route.ts.
apps/sim/lib/billing/client/utils.ts New getSubscriptionAccessState function added, wrapping getSubscriptionStatus with hasUsableSubscriptionAccess to compute hasUsablePaidAccess, hasUsableTeamAccess, hasUsableEnterpriseAccess, hasUsableMaxAccess correctly blocking past_due users from feature access.
apps/sim/lib/billing/webhooks/subscription.ts hasActiveSubscription renamed to hasPaidSubscription, and ENTITLED_SUBSCRIPTION_STATUSES used in handleSubscriptionCreated to correctly treat past_due subscriptions as non-free. A code comment describing "truly active" subscriptions is now slightly stale since past_due is also included.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx Navigation refactored from filter to flatMap to support disabled state. requiresTeam/requiresEnterprise/requiresMax items are now shown grayed-out rather than hidden. Disabled items show cursor-not-allowed with opacity-50, but no tooltip explains the reason to the user when the sidebar is expanded.
apps/sim/lib/billing/core/subscription.ts hasActiveSubscription renamed to hasPaidSubscription with ENTITLED_SUBSCRIPTION_STATUSES. isEnterpriseOrgAdminOrOwner and isTeamOrgAdminOrOwner now additionally short-circuit on billingBlocked and use USABLE_SUBSCRIPTION_STATUSES for subscription lookup (active-only).
apps/sim/lib/billing/subscriptions/utils.test.ts New test file with good coverage of the key behavioral contract: past_due counts as paid entitlement but not usable access; active + billingBlocked = no access; canEditUsageLimit requires active status only.
apps/sim/lib/billing/plan-helpers.ts SQL-level plan helpers sqlIsPro, sqlIsTeam, sqlIsPaid added. sqlIsPaid misses enterprise-plus (already flagged in previous review thread). isEnterprise only checks plan === 'enterprise'.
apps/sim/app/workspace/[workspaceId]/settings/navigation.ts requiresMax added to NavigationItem interface and applied to inbox (Sim Mailer). credential-sets (Email Polling) now has requiresTeam. Both are shown as disabled rather than hidden when requirements aren't met.
apps/sim/lib/billing/core/billing.ts getSimplifiedBillingSummary now gates plan flags (isPaid, isPro, isTeam, isEnterprise) behind hasPaidSubscriptionStatus, so past_due subscriptions still propagate their plan type correctly to the client.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Stripe Webhook / Status Change] --> B{Subscription Status}
    B -->|active| C[ENTITLED + USABLE]
    B -->|past_due| D[ENTITLED only]
    B -->|canceled / other| E[Neither]

    C --> F[Full feature access]
    C --> G[Can edit usage limits]
    C --> H[Can purchase credits]

    D --> I{billingBlocked flag?}
    I -->|false| J[Plan preserved, features blocked]
    I -->|true| K[Plan preserved, features blocked]

    J --> L[Billing portal accessible]
    J --> M[No downgrade to free tier]
    J --> N[Cannot use team/enterprise features]
    J --> O[Cannot purchase credits]
    J --> P[Cannot edit usage limits]

    K --> L
    K --> M
    K --> N
    K --> O
    K --> P

    E --> Q[Downgrade to free tier\nsyncUsageLimitsFromSubscription]

    style C fill:#22c55e,color:#fff
    style D fill:#f59e0b,color:#fff
    style E fill:#ef4444,color:#fff
    style F fill:#dcfce7
    style G fill:#dcfce7
    style H fill:#dcfce7
    style Q fill:#fee2e2
Loading

Reviews (2): Last reviewed commit: "Fix type errors" | Re-trigger Greptile

@icecrasher321
Copy link
Collaborator Author

bugbot run

@icecrasher321
Copy link
Collaborator Author

@greptile

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@icecrasher321 icecrasher321 merged commit 8800f03 into staging Mar 25, 2026
12 checks passed
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