fix(org-limits): remove fallbacks for enterprise plan (#2255)

* fix(org-limits): remove fallbacks for enterprise plan

* remove comment

* remove comments

* make logger use new helper
This commit is contained in:
Vikhyath Mondreti
2025-12-08 21:43:43 -08:00
committed by GitHub
parent e390ba0491
commit 0db5ba1b27
5 changed files with 167 additions and 83 deletions
@@ -1,9 +1,10 @@
import { db } from '@sim/db'
import { member, subscription } from '@sim/db/schema'
import { member, organization, subscription } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { getPlanPricing } from '@/lib/billing/core/billing'
import { requireStripeClient } from '@/lib/billing/stripe-client'
import { isBillingEnabled } from '@/lib/core/config/environment'
import { createLogger } from '@/lib/logs/console/logger'
@@ -172,6 +173,39 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
})
.where(eq(subscription.id, orgSubscription.id))
// Update orgUsageLimit to reflect new seat count (seats × basePrice as minimum)
const { basePrice } = getPlanPricing('team')
const newMinimumLimit = newSeatCount * basePrice
const orgData = await db
.select({ orgUsageLimit: organization.orgUsageLimit })
.from(organization)
.where(eq(organization.id, organizationId))
.limit(1)
const currentOrgLimit =
orgData.length > 0 && orgData[0].orgUsageLimit
? Number.parseFloat(orgData[0].orgUsageLimit)
: 0
// Update if new minimum is higher than current limit
if (newMinimumLimit > currentOrgLimit) {
await db
.update(organization)
.set({
orgUsageLimit: newMinimumLimit.toFixed(2),
updatedAt: new Date(),
})
.where(eq(organization.id, organizationId))
logger.info('Updated organization usage limit for seat change', {
organizationId,
newSeatCount,
newMinimumLimit,
previousLimit: currentOrgLimit,
})
}
logger.info('Successfully updated seat count', {
organizationId,
stripeSubscriptionId: orgSubscription.stripeSubscriptionId,
@@ -1,7 +1,6 @@
import { db } from '@sim/db'
import { member, organization, userStats } from '@sim/db/schema'
import { and, eq, inArray } from 'drizzle-orm'
import { getOrganizationSubscription, getPlanPricing } from '@/lib/billing/core/billing'
import { getUserUsageLimit } from '@/lib/billing/core/usage'
import { isBillingEnabled } from '@/lib/core/config/environment'
import { createLogger } from '@/lib/logs/console/logger'
@@ -108,19 +107,10 @@ export async function checkUsageStatus(userId: string): Promise<UsageData> {
)
}
}
// Determine org cap
let orgCap = org.orgUsageLimit ? Number.parseFloat(String(org.orgUsageLimit)) : 0
// Determine org cap from orgUsageLimit (should always be set for team/enterprise)
const orgCap = org.orgUsageLimit ? Number.parseFloat(String(org.orgUsageLimit)) : 0
if (!orgCap || Number.isNaN(orgCap)) {
// Fall back to minimum billing amount from Stripe subscription
const orgSub = await getOrganizationSubscription(org.id)
if (orgSub?.seats) {
const { basePrice } = getPlanPricing(orgSub.plan)
orgCap = (orgSub.seats ?? 0) * basePrice
} else {
// If no subscription, use team default
const { basePrice } = getPlanPricing('team')
orgCap = basePrice // Default to 1 seat minimum
}
logger.warn('Organization missing usage limit', { orgId: org.id })
}
if (pooledUsage >= orgCap) {
isExceeded = true
+76 -48
View File
@@ -22,6 +22,56 @@ import { getEmailPreferences } from '@/lib/messaging/email/unsubscribe'
const logger = createLogger('UsageManagement')
export interface OrgUsageLimitResult {
limit: number
minimum: number
}
/**
* Calculates the effective usage limit for a team or enterprise organization.
* - Enterprise: Uses orgUsageLimit directly (fixed pricing)
* - Team: Uses orgUsageLimit but never below seats × basePrice
*/
export async function getOrgUsageLimit(
organizationId: string,
plan: string,
seats: number | null
): Promise<OrgUsageLimitResult> {
const orgData = await db
.select({ orgUsageLimit: organization.orgUsageLimit })
.from(organization)
.where(eq(organization.id, organizationId))
.limit(1)
const configured =
orgData.length > 0 && orgData[0].orgUsageLimit
? Number.parseFloat(orgData[0].orgUsageLimit)
: null
if (plan === 'enterprise') {
// Enterprise: Use configured limit directly (no per-seat minimum)
if (configured !== null) {
return { limit: configured, minimum: configured }
}
logger.warn('Enterprise org missing usage limit', { orgId: organizationId })
return { limit: 0, minimum: 0 }
}
const { basePrice } = getPlanPricing(plan)
const minimum = (seats ?? 0) * basePrice
if (configured !== null) {
return { limit: Math.max(configured, minimum), minimum }
}
logger.warn('Team org missing usage limit, using seats × basePrice fallback', {
orgId: organizationId,
seats,
minimum,
})
return { limit: minimum, minimum }
}
/**
* Handle new user setup when they join the platform
* Creates userStats record with default free credits
@@ -87,22 +137,13 @@ export async function getUserUsageData(userId: string): Promise<UsageData> {
? Number.parseFloat(stats.currentUsageLimit)
: getFreeTierLimit()
} else {
// Team/Enterprise: Use organization limit but never below minimum (seats × cost per seat)
const orgData = await db
.select({ orgUsageLimit: organization.orgUsageLimit })
.from(organization)
.where(eq(organization.id, subscription.referenceId))
.limit(1)
const { basePrice } = getPlanPricing(subscription.plan)
const minimum = (subscription.seats ?? 0) * basePrice
if (orgData.length > 0 && orgData[0].orgUsageLimit) {
const configured = Number.parseFloat(orgData[0].orgUsageLimit)
limit = Math.max(configured, minimum)
} else {
limit = minimum
}
// Team/Enterprise: Use organization limit
const orgLimit = await getOrgUsageLimit(
subscription.referenceId,
subscription.plan,
subscription.seats
)
limit = orgLimit.limit
}
const percentUsed = limit > 0 ? Math.min((currentUsage / limit) * 100, 100) : 0
@@ -159,24 +200,15 @@ export async function getUserUsageLimitInfo(userId: string): Promise<UsageLimitI
minimumLimit = getPerUserMinimumLimit(subscription)
canEdit = canEditUsageLimit(subscription)
} else {
// Team/Enterprise: Use organization limits (users cannot edit)
const orgData = await db
.select({ orgUsageLimit: organization.orgUsageLimit })
.from(organization)
.where(eq(organization.id, subscription.referenceId))
.limit(1)
const { basePrice } = getPlanPricing(subscription.plan)
const minimum = (subscription.seats ?? 0) * basePrice
if (orgData.length > 0 && orgData[0].orgUsageLimit) {
const configured = Number.parseFloat(orgData[0].orgUsageLimit)
currentLimit = Math.max(configured, minimum)
} else {
currentLimit = minimum
}
minimumLimit = minimum
canEdit = false // Team/enterprise members cannot edit limits
// Team/Enterprise: Use organization limits
const orgLimit = await getOrgUsageLimit(
subscription.referenceId,
subscription.plan,
subscription.seats
)
currentLimit = orgLimit.limit
minimumLimit = orgLimit.minimum
canEdit = false
}
return {
@@ -323,27 +355,23 @@ export async function getUserUsageLimit(userId: string): Promise<number> {
return Number.parseFloat(userStatsQuery[0].currentUsageLimit)
}
// Team/Enterprise: Use organization limit but never below minimum
const orgData = await db
.select({ orgUsageLimit: organization.orgUsageLimit })
// Team/Enterprise: Verify org exists then use organization limit
const orgExists = await db
.select({ id: organization.id })
.from(organization)
.where(eq(organization.id, subscription.referenceId))
.limit(1)
if (orgData.length === 0) {
if (orgExists.length === 0) {
throw new Error(`Organization not found: ${subscription.referenceId} for user: ${userId}`)
}
if (orgData[0].orgUsageLimit) {
const configured = Number.parseFloat(orgData[0].orgUsageLimit)
const { basePrice } = getPlanPricing(subscription.plan)
const minimum = (subscription.seats ?? 0) * basePrice
return Math.max(configured, minimum)
}
// If org hasn't set a custom limit, use minimum (seats × cost per seat)
const { basePrice } = getPlanPricing(subscription.plan)
return (subscription.seats ?? 0) * basePrice
const orgLimit = await getOrgUsageLimit(
subscription.referenceId,
subscription.plan,
subscription.seats
)
return orgLimit.limit
}
/**
+46 -4
View File
@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import * as schema from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { getPlanPricing } from '@/lib/billing/core/billing'
import { syncUsageLimitsFromSubscription } from '@/lib/billing/core/usage'
import { createLogger } from '@/lib/logs/console/logger'
@@ -145,11 +146,52 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
plan: subscription.plan,
})
} else {
// Organization subscription - sync usage limits for all members
// Organization subscription - set org usage limit and sync member limits
const organizationId = subscription.referenceId
// Set orgUsageLimit for team plans (enterprise is set via webhook with custom pricing)
if (subscription.plan === 'team') {
const { basePrice } = getPlanPricing(subscription.plan)
const seats = subscription.seats ?? 1
const orgLimit = seats * basePrice
// Only set if not already set or if updating to a higher value based on seats
const orgData = await db
.select({ orgUsageLimit: schema.organization.orgUsageLimit })
.from(schema.organization)
.where(eq(schema.organization.id, organizationId))
.limit(1)
const currentLimit =
orgData.length > 0 && orgData[0].orgUsageLimit
? Number.parseFloat(orgData[0].orgUsageLimit)
: 0
// Update if no limit set, or if new seat-based minimum is higher
if (currentLimit < orgLimit) {
await db
.update(schema.organization)
.set({
orgUsageLimit: orgLimit.toFixed(2),
updatedAt: new Date(),
})
.where(eq(schema.organization.id, organizationId))
logger.info('Set organization usage limit for team plan', {
organizationId,
seats,
basePrice,
orgLimit,
previousLimit: currentLimit,
})
}
}
// Sync usage limits for all members
const members = await db
.select({ userId: schema.member.userId })
.from(schema.member)
.where(eq(schema.member.organizationId, subscription.referenceId))
.where(eq(schema.member.organizationId, organizationId))
if (members.length > 0) {
for (const member of members) {
@@ -158,7 +200,7 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
} catch (memberError) {
logger.error('Failed to sync usage limits for organization member', {
userId: member.userId,
organizationId: subscription.referenceId,
organizationId,
subscriptionId: subscription.id,
error: memberError,
})
@@ -166,7 +208,7 @@ export async function syncSubscriptionUsageLimits(subscription: SubscriptionData
}
logger.info('Synced usage limits for organization members', {
organizationId: subscription.referenceId,
organizationId,
memberCount: members.length,
subscriptionId: subscription.id,
plan: subscription.plan,
+7 -17
View File
@@ -1,7 +1,6 @@
import { db } from '@sim/db'
import {
member,
organization,
userStats,
user as userTable,
workflow,
@@ -10,7 +9,11 @@ import {
import { eq, sql } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { checkUsageStatus, maybeSendUsageThresholdEmail } from '@/lib/billing/core/usage'
import {
checkUsageStatus,
getOrgUsageLimit,
maybeSendUsageThresholdEmail,
} from '@/lib/billing/core/usage'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { isBillingEnabled } from '@/lib/core/config/environment'
import { redactApiKeys } from '@/lib/core/security/redaction'
@@ -386,21 +389,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
limit,
})
} else if (sub?.referenceId) {
let orgLimit = 0
const orgRows = await db
.select({ orgUsageLimit: organization.orgUsageLimit })
.from(organization)
.where(eq(organization.id, sub.referenceId))
.limit(1)
const { getPlanPricing } = await import('@/lib/billing/core/billing')
const { basePrice } = getPlanPricing(sub.plan)
const minimum = (sub.seats || 1) * basePrice
if (orgRows.length > 0 && orgRows[0].orgUsageLimit) {
const configured = Number.parseFloat(orgRows[0].orgUsageLimit)
orgLimit = Math.max(configured, minimum)
} else {
orgLimit = minimum
}
// Get org usage limit using shared helper
const { limit: orgLimit } = await getOrgUsageLimit(sub.referenceId, sub.plan, sub.seats)
const [{ sum: orgUsageBefore }] = await db
.select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` })