Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d384d41
fix(billing): team usage tracking cleanup, shared pool of limits for …
icecrasher321 Aug 25, 2025
19392e3
address greptile commments
icecrasher321 Aug 25, 2025
6717a35
fix lint
icecrasher321 Aug 25, 2025
2677de2
remove usage of deprecated cols"
icecrasher321 Aug 25, 2025
b64d4b2
update periodStart and periodEnd correctly
icecrasher321 Aug 25, 2025
232caf3
fix lint
icecrasher321 Aug 25, 2025
00391c0
fix type issue
icecrasher321 Aug 25, 2025
d810ded
fix(billing): cleaned up billing, still more work to do on UI and pop…
waleedlatif1 Aug 26, 2025
8fcb93c
fix upgrade
icecrasher321 Aug 26, 2025
2c984ca
cleanup
icecrasher321 Aug 26, 2025
880eda3
progress
icecrasher321 Aug 26, 2025
8705390
works
icecrasher321 Aug 26, 2025
82e85c7
Remove 78th migration to prepare for merge with staging
icecrasher321 Aug 26, 2025
108ae39
Merge remote-tracking branch 'origin/staging' into fix/billing-cleanup
icecrasher321 Aug 26, 2025
1908439
fix migration conflict
icecrasher321 Aug 26, 2025
23272fa
remove useless test file
icecrasher321 Aug 26, 2025
cc0f65f
fix
icecrasher321 Aug 26, 2025
6b26f08
Fix undefined seat pricing display and handle cancelled subscription …
icecrasher321 Aug 26, 2025
7c1dd08
cleanup code
icecrasher321 Aug 26, 2025
c97fb24
cleanup to use helpers for pulling pricing limits
icecrasher321 Aug 26, 2025
957c4b6
cleanup more things
icecrasher321 Aug 26, 2025
32bb21e
cleanup
icecrasher321 Aug 27, 2025
a70ea65
restore environment ts file
icecrasher321 Aug 27, 2025
986e44d
remove unused files
icecrasher321 Aug 27, 2025
f0ce996
fix(team-management): fix team management UI, consolidate components
waleedlatif1 Aug 27, 2025
d3a35e0
use session data instead of subscription data in settings navigation
waleedlatif1 Aug 27, 2025
45d4423
remove unused code
icecrasher321 Aug 27, 2025
3360cd8
fix UI for enterprise plans
waleedlatif1 Aug 27, 2025
83b977a
added enterprise plan support
waleedlatif1 Aug 28, 2025
882ec54
progress
icecrasher321 Aug 28, 2025
98a1101
billing state machine
icecrasher321 Aug 28, 2025
534244a
split overage and base into separate invoices
icecrasher321 Aug 28, 2025
6865df6
fix badge logic
icecrasher321 Aug 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/sim/app/api/auth/webhook/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { toNextJsHandler } from 'better-auth/next-js'
import { auth } from '@/lib/auth'

export const dynamic = 'force-dynamic'

// Handle Stripe webhooks through better-auth
export const { GET, POST } = toNextJsHandler(auth.handler)
77 changes: 77 additions & 0 deletions apps/sim/app/api/billing/portal/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { requireStripeClient } from '@/lib/billing/stripe-client'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
import { subscription as subscriptionTable, user } from '@/db/schema'

const logger = createLogger('BillingPortal')

export async function POST(request: NextRequest) {
const session = await getSession()

try {
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json().catch(() => ({}))
const context: 'user' | 'organization' =
body?.context === 'organization' ? 'organization' : 'user'
const organizationId: string | undefined = body?.organizationId || undefined
const returnUrl: string =
body?.returnUrl || `${env.NEXT_PUBLIC_APP_URL}/workspace?billing=updated`

const stripe = requireStripeClient()

let stripeCustomerId: string | null = null

if (context === 'organization') {
if (!organizationId) {
return NextResponse.json({ error: 'organizationId is required' }, { status: 400 })
}

const rows = await db
.select({ customer: subscriptionTable.stripeCustomerId })
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.referenceId, organizationId),
eq(subscriptionTable.status, 'active')
)
)
.limit(1)

stripeCustomerId = rows.length > 0 ? rows[0].customer || null : null
} else {
const rows = await db
.select({ customer: user.stripeCustomerId })
.from(user)
.where(eq(user.id, session.user.id))
.limit(1)

stripeCustomerId = rows.length > 0 ? rows[0].customer || null : null
}

if (!stripeCustomerId) {
logger.error('Stripe customer not found for portal session', {
context,
organizationId,
userId: session.user.id,
})
return NextResponse.json({ error: 'Stripe customer not found' }, { status: 404 })
}

const portal = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: returnUrl,
})

return NextResponse.json({ url: portal.url })
} catch (error) {
logger.error('Failed to create billing portal session', { error })
return NextResponse.json({ error: 'Failed to create billing portal session' }, { status: 500 })
}
}
28 changes: 27 additions & 1 deletion apps/sim/app/api/billing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getSimplifiedBillingSummary } from '@/lib/billing/core/billing'
import { getOrganizationBillingData } from '@/lib/billing/core/organization-billing'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
import { member } from '@/db/schema'
import { member, userStats } from '@/db/schema'

const logger = createLogger('UnifiedBillingAPI')

Expand Down Expand Up @@ -45,6 +45,16 @@ export async function GET(request: NextRequest) {
if (context === 'user') {
// Get user billing (may include organization if they're part of one)
billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined)
// Attach billingBlocked status for the current user
const stats = await db
.select({ blocked: userStats.billingBlocked })
.from(userStats)
.where(eq(userStats.userId, session.user.id))
.limit(1)
billingData = {
...billingData,
billingBlocked: stats.length > 0 ? !!stats[0].blocked : false,
}
} else {
// Get user role in organization for permission checks first
const memberRecord = await db
Expand Down Expand Up @@ -78,8 +88,10 @@ export async function GET(request: NextRequest) {
subscriptionStatus: rawBillingData.subscriptionStatus,
totalSeats: rawBillingData.totalSeats,
usedSeats: rawBillingData.usedSeats,
seatsCount: rawBillingData.seatsCount,
totalCurrentUsage: rawBillingData.totalCurrentUsage,
totalUsageLimit: rawBillingData.totalUsageLimit,
minimumBillingAmount: rawBillingData.minimumBillingAmount,
averageUsagePerMember: rawBillingData.averageUsagePerMember,
billingPeriodStart: rawBillingData.billingPeriodStart?.toISOString() || null,
billingPeriodEnd: rawBillingData.billingPeriodEnd?.toISOString() || null,
Expand All @@ -92,11 +104,25 @@ export async function GET(request: NextRequest) {

const userRole = memberRecord[0].role

// Include the requesting user's blocked flag as well so UI can reflect it
const stats = await db
.select({ blocked: userStats.billingBlocked })
.from(userStats)
.where(eq(userStats.userId, session.user.id))
.limit(1)

// Merge blocked flag into data for convenience
billingData = {
...billingData,
billingBlocked: stats.length > 0 ? !!stats[0].blocked : false,
}

return NextResponse.json({
success: true,
context,
data: billingData,
userRole,
billingBlocked: billingData.billingBlocked,
})
}

Expand Down
72 changes: 27 additions & 45 deletions apps/sim/app/api/billing/update-cost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,52 +115,34 @@ export async function POST(req: NextRequest) {
const userStatsRecords = await db.select().from(userStats).where(eq(userStats.userId, userId))

if (userStatsRecords.length === 0) {
// Create new user stats record (same logic as ExecutionLogger)
await db.insert(userStats).values({
id: crypto.randomUUID(),
userId: userId,
totalManualExecutions: 0,
totalApiCalls: 0,
totalWebhookTriggers: 0,
totalScheduledExecutions: 0,
totalChatExecutions: 0,
totalTokensUsed: totalTokens,
totalCost: costToStore.toString(),
currentPeriodCost: costToStore.toString(),
// Copilot usage tracking
totalCopilotCost: costToStore.toString(),
totalCopilotTokens: totalTokens,
totalCopilotCalls: 1,
lastActive: new Date(),
})

logger.info(`[${requestId}] Created new user stats record`, {
userId,
totalCost: costToStore,
totalTokens,
})
} else {
// Update existing user stats record (same logic as ExecutionLogger)
const updateFields = {
totalTokensUsed: sql`total_tokens_used + ${totalTokens}`,
totalCost: sql`total_cost + ${costToStore}`,
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
// Copilot usage tracking increments
totalCopilotCost: sql`total_copilot_cost + ${costToStore}`,
totalCopilotTokens: sql`total_copilot_tokens + ${totalTokens}`,
totalCopilotCalls: sql`total_copilot_calls + 1`,
totalApiCalls: sql`total_api_calls`,
lastActive: new Date(),
}

await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))

logger.info(`[${requestId}] Updated user stats record`, {
userId,
addedCost: costToStore,
addedTokens: totalTokens,
})
logger.error(
`[${requestId}] User stats record not found - should be created during onboarding`,
{
userId,
}
)
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
}
// Update existing user stats record (same logic as ExecutionLogger)
const updateFields = {
totalTokensUsed: sql`total_tokens_used + ${totalTokens}`,
totalCost: sql`total_cost + ${costToStore}`,
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
// Copilot usage tracking increments
totalCopilotCost: sql`total_copilot_cost + ${costToStore}`,
totalCopilotTokens: sql`total_copilot_tokens + ${totalTokens}`,
totalCopilotCalls: sql`total_copilot_calls + 1`,
totalApiCalls: sql`total_api_calls`,
lastActive: new Date(),
}

await db.update(userStats).set(updateFields).where(eq(userStats.userId, userId))

logger.info(`[${requestId}] Updated user stats record`, {
userId,
addedCost: costToStore,
addedTokens: totalTokens,
})

const duration = Date.now() - startTime

Expand Down
116 changes: 0 additions & 116 deletions apps/sim/app/api/billing/webhooks/stripe/route.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export async function GET(
.select({
currentPeriodCost: userStats.currentPeriodCost,
currentUsageLimit: userStats.currentUsageLimit,
usageLimitSetBy: userStats.usageLimitSetBy,
usageLimitUpdatedAt: userStats.usageLimitUpdatedAt,
lastPeriodCost: userStats.lastPeriodCost,
})
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/api/organizations/[id]/members/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
userEmail: user.email,
currentPeriodCost: userStats.currentPeriodCost,
currentUsageLimit: userStats.currentUsageLimit,
usageLimitSetBy: userStats.usageLimitSetBy,
usageLimitUpdatedAt: userStats.usageLimitUpdatedAt,
})
.from(member)
Expand Down
Loading