@@ -10,6 +10,7 @@ import {
1010} from '@/lib/billing/core/plan'
1111import {
1212 getPlanTierCredits ,
13+ isEnterprise as isPlanEnterprise ,
1314 isPro as isPlanPro ,
1415 isTeam as isPlanTeam ,
1516} from '@/lib/billing/plan-helpers'
@@ -521,12 +522,13 @@ export async function isWorkspaceOnEnterprisePlan(workspaceId: string): Promise<
521522const MAX_PLAN_CREDITS = 25000
522523
523524/**
524- * Whether a resolved subscription entitles the inbox (Sim Mailer) feature: a Max
525- * tier (credits >= 25000, covering `pro_25000` and `team_25000`) or any
526- * enterprise plan.
525+ * Whether a plan tier entitles the inbox (Sim Mailer) feature: a Max tier
526+ * (credits >= 25000, covering `pro_25000` and `team_25000`) or any enterprise
527+ * plan. Subscription status (usable vs entitled) is gated by callers before this
528+ * runs — the predicate is tier-only.
527529 */
528- function isInboxEntitledPlan ( sub : { plan : string } ) : boolean {
529- return getPlanTierCredits ( sub . plan ) >= MAX_PLAN_CREDITS || checkEnterprisePlan ( sub )
530+ function isInboxEntitledPlan ( plan : string ) : boolean {
531+ return getPlanTierCredits ( plan ) >= MAX_PLAN_CREDITS || isPlanEnterprise ( plan )
530532}
531533
532534/**
@@ -554,7 +556,7 @@ export async function hasWorkspaceInboxAccess(workspaceId: string): Promise<bool
554556 if ( ws . organizationId ) {
555557 if ( await isOrganizationBillingBlocked ( ws . organizationId ) ) return false
556558 const orgSub = await getOrganizationSubscriptionUsable ( ws . organizationId )
557- return ! ! orgSub && isInboxEntitledPlan ( orgSub )
559+ return ! ! orgSub && isInboxEntitledPlan ( orgSub . plan )
558560 }
559561
560562 const [ billedSub , billingStatus ] = await Promise . all ( [
@@ -563,13 +565,48 @@ export async function hasWorkspaceInboxAccess(workspaceId: string): Promise<bool
563565 ] )
564566 if ( ! billedSub ) return false
565567 if ( ! hasUsableSubscriptionAccess ( billedSub . status , billingStatus . billingBlocked ) ) return false
566- return isInboxEntitledPlan ( billedSub )
568+ return isInboxEntitledPlan ( billedSub . plan )
567569 } catch ( error ) {
568570 logger . error ( 'Error checking workspace inbox access' , { error, workspaceId } )
569571 return false
570572 }
571573}
572574
575+ /**
576+ * Whether a workspace should RETAIN its provisioned inbox (Sim Mailer)
577+ * infrastructure. Unlike {@link hasWorkspaceInboxAccess}, which gates active use
578+ * on a *usable* (active) subscription, this uses the broader *entitled* status
579+ * set (active OR `past_due`) so a transient payment failure never triggers the
580+ * destructive teardown of a paying customer's inbox.
581+ *
582+ * Reconciliation should delete AgentMail resources only when this returns
583+ * `false` — i.e. the plan is genuinely terminal (canceled, downgraded off
584+ * Max/Enterprise, or gone). Fails open (returns `true`) on any error or
585+ * ambiguity: never tear down on uncertainty.
586+ */
587+ export async function hasWorkspaceInboxGraceAccess ( workspaceId : string ) : Promise < boolean > {
588+ try {
589+ if ( isInboxEnabled ) return true
590+ if ( ! isBillingEnabled ) return true
591+
592+ const { getWorkspaceWithOwner } = await import ( '@/lib/workspaces/permissions/utils' )
593+ const ws = await getWorkspaceWithOwner ( workspaceId , { includeArchived : true } )
594+ if ( ! ws ) return true
595+
596+ if ( ws . organizationId ) {
597+ const { getOrganizationSubscription } = await import ( '@/lib/billing/core/billing' )
598+ const orgSub = await getOrganizationSubscription ( ws . organizationId )
599+ return ! ! orgSub && isInboxEntitledPlan ( orgSub . plan )
600+ }
601+
602+ const billedSub = await getHighestPrioritySubscription ( ws . billedAccountUserId )
603+ return ! ! billedSub && isInboxEntitledPlan ( billedSub . plan )
604+ } catch ( error ) {
605+ logger . error ( 'Error checking workspace inbox grace access' , { error, workspaceId } )
606+ return true
607+ }
608+ }
609+
573610/**
574611 * Check if user has access to live sync (every 5 minutes) for KB connectors
575612 * Returns true if:
0 commit comments