mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)` })
|
||||
|
||||
Reference in New Issue
Block a user