Skip to content

Commit 780870c

Browse files
icecrasher321waleedlatif1greptile-apps[bot]aadamgoughAdam Gough
authored
fix(billing): make subscription table source of truth for period start and period end (#1114)
* fix(billing): vercel cron not processing billing periods * fix(billing): cleanup unused POST and fix bug with billing timing check * make subscriptions table source of truth for dates * update org routes * make everything dependent on stripe webhook --------- Co-authored-by: Waleed Latif <[email protected]> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Adam Gough <[email protected]> Co-authored-by: Adam Gough <[email protected]>
1 parent 917552f commit 780870c

File tree

9 files changed

+109
-220
lines changed

9 files changed

+109
-220
lines changed

apps/sim/app/api/billing/daily/route.ts

Lines changed: 0 additions & 150 deletions
This file was deleted.

apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { and, eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
4+
import { getUserUsageData } from '@/lib/billing/core/usage'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { db } from '@/db'
67
import { member, user, userStats } from '@/db/schema'
@@ -80,8 +81,6 @@ export async function GET(
8081
.select({
8182
currentPeriodCost: userStats.currentPeriodCost,
8283
currentUsageLimit: userStats.currentUsageLimit,
83-
billingPeriodStart: userStats.billingPeriodStart,
84-
billingPeriodEnd: userStats.billingPeriodEnd,
8584
usageLimitSetBy: userStats.usageLimitSetBy,
8685
usageLimitUpdatedAt: userStats.usageLimitUpdatedAt,
8786
lastPeriodCost: userStats.lastPeriodCost,
@@ -90,11 +89,22 @@ export async function GET(
9089
.where(eq(userStats.userId, memberId))
9190
.limit(1)
9291

92+
const computed = await getUserUsageData(memberId)
93+
9394
if (usageData.length > 0) {
9495
memberData = {
9596
...memberData,
96-
usage: usageData[0],
97-
} as typeof memberData & { usage: (typeof usageData)[0] }
97+
usage: {
98+
...usageData[0],
99+
billingPeriodStart: computed.billingPeriodStart,
100+
billingPeriodEnd: computed.billingPeriodEnd,
101+
},
102+
} as typeof memberData & {
103+
usage: (typeof usageData)[0] & {
104+
billingPeriodStart: Date | null
105+
billingPeriodEnd: Date | null
106+
}
107+
}
98108
}
99109
}
100110

apps/sim/app/api/organizations/[id]/members/route.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { and, eq } from 'drizzle-orm'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { getEmailSubject, renderInvitationEmail } from '@/components/emails/render-email'
55
import { getSession } from '@/lib/auth'
6+
import { getUserUsageData } from '@/lib/billing/core/usage'
67
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
78
import { sendEmail } from '@/lib/email/mailer'
89
import { quickValidateEmail } from '@/lib/email/validation'
@@ -63,7 +64,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
6364

6465
// Include usage data if requested and user has admin access
6566
if (includeUsage && hasAdminAccess) {
66-
const membersWithUsage = await db
67+
const base = await db
6768
.select({
6869
id: member.id,
6970
userId: member.userId,
@@ -74,8 +75,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
7475
userEmail: user.email,
7576
currentPeriodCost: userStats.currentPeriodCost,
7677
currentUsageLimit: userStats.currentUsageLimit,
77-
billingPeriodStart: userStats.billingPeriodStart,
78-
billingPeriodEnd: userStats.billingPeriodEnd,
7978
usageLimitSetBy: userStats.usageLimitSetBy,
8079
usageLimitUpdatedAt: userStats.usageLimitUpdatedAt,
8180
})
@@ -84,6 +83,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
8483
.leftJoin(userStats, eq(user.id, userStats.userId))
8584
.where(eq(member.organizationId, organizationId))
8685

86+
const membersWithUsage = await Promise.all(
87+
base.map(async (row) => {
88+
const usage = await getUserUsageData(row.userId)
89+
return {
90+
...row,
91+
billingPeriodStart: usage.billingPeriodStart,
92+
billingPeriodEnd: usage.billingPeriodEnd,
93+
}
94+
})
95+
)
96+
8797
return NextResponse.json({
8898
success: true,
8999
data: membersWithUsage,

apps/sim/lib/billing/core/billing-periods.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,9 @@ export async function initializeBillingPeriod(
145145
end = billingPeriod.end
146146
}
147147

148-
// Update user stats with billing period info
149148
await db
150149
.update(userStats)
151150
.set({
152-
billingPeriodStart: start,
153-
billingPeriodEnd: end,
154151
currentPeriodCost: '0',
155152
})
156153
.where(eq(userStats.userId, userId))
@@ -212,14 +209,12 @@ export async function resetUserBillingPeriod(userId: string): Promise<void> {
212209
newPeriodEnd = billingPeriod.end
213210
}
214211

215-
// Archive current period cost and reset for new period
212+
// Archive current period cost and reset for new period (no longer updating period dates in user_stats)
216213
await db
217214
.update(userStats)
218215
.set({
219-
lastPeriodCost: currentPeriodCost, // Archive previous period
220-
currentPeriodCost: '0', // Reset to zero for new period
221-
billingPeriodStart: newPeriodStart,
222-
billingPeriodEnd: newPeriodEnd,
216+
lastPeriodCost: currentPeriodCost,
217+
currentPeriodCost: '0',
223218
})
224219
.where(eq(userStats.userId, userId))
225220

apps/sim/lib/billing/core/billing.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getUserUsageData } from '@/lib/billing/core/usage'
99
import { requireStripeClient } from '@/lib/billing/stripe-client'
1010
import { createLogger } from '@/lib/logs/console/logger'
1111
import { db } from '@/db'
12-
import { member, organization, subscription, user, userStats } from '@/db/schema'
12+
import { member, organization, subscription, user } from '@/db/schema'
1313

1414
const logger = createLogger('Billing')
1515

@@ -673,45 +673,21 @@ export async function getUsersAndOrganizationsForOverageBilling(): Promise<{
673673
continue // Skip free plans
674674
}
675675

676-
// Check if subscription period ends today
676+
// Check if subscription period ends today (range-based, inclusive of day)
677677
let shouldBillToday = false
678678

679679
if (sub.periodEnd) {
680680
const periodEnd = new Date(sub.periodEnd)
681-
periodEnd.setUTCHours(0, 0, 0, 0) // Normalize to start of day
681+
const endsToday = periodEnd >= today && periodEnd < tomorrow
682682

683-
// Bill if the subscription period ends today
684-
if (periodEnd.getTime() === today.getTime()) {
683+
if (endsToday) {
685684
shouldBillToday = true
686685
logger.info('Subscription period ends today', {
687686
referenceId: sub.referenceId,
688687
plan: sub.plan,
689688
periodEnd: sub.periodEnd,
690689
})
691690
}
692-
} else {
693-
// Fallback: Check userStats billing period for users
694-
const userStatsRecord = await db
695-
.select({
696-
billingPeriodEnd: userStats.billingPeriodEnd,
697-
})
698-
.from(userStats)
699-
.where(eq(userStats.userId, sub.referenceId))
700-
.limit(1)
701-
702-
if (userStatsRecord.length > 0 && userStatsRecord[0].billingPeriodEnd) {
703-
const billingPeriodEnd = new Date(userStatsRecord[0].billingPeriodEnd)
704-
billingPeriodEnd.setUTCHours(0, 0, 0, 0) // Normalize to start of day
705-
706-
if (billingPeriodEnd.getTime() === today.getTime()) {
707-
shouldBillToday = true
708-
logger.info('User billing period ends today (from userStats)', {
709-
userId: sub.referenceId,
710-
plan: sub.plan,
711-
billingPeriodEnd: userStatsRecord[0].billingPeriodEnd,
712-
})
713-
}
714-
}
715691
}
716692

717693
if (shouldBillToday) {

apps/sim/lib/billing/core/organization-billing.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ export async function getOrganizationBillingData(
9494
// User stats fields
9595
currentPeriodCost: userStats.currentPeriodCost,
9696
currentUsageLimit: userStats.currentUsageLimit,
97-
billingPeriodStart: userStats.billingPeriodStart,
98-
billingPeriodEnd: userStats.billingPeriodEnd,
9997
lastActive: userStats.lastActive,
10098
})
10199
.from(member)
@@ -151,10 +149,9 @@ export async function getOrganizationBillingData(
151149

152150
const averageUsagePerMember = members.length > 0 ? totalCurrentUsage / members.length : 0
153151

154-
// Get billing period from first member (should be consistent across org)
155-
const firstMember = membersWithUsage[0]
156-
const billingPeriodStart = firstMember?.billingPeriodStart || null
157-
const billingPeriodEnd = firstMember?.billingPeriodEnd || null
152+
// Billing period comes from the organization's subscription
153+
const billingPeriodStart = subscription.periodStart || null
154+
const billingPeriodEnd = subscription.periodEnd || null
158155

159156
return {
160157
organizationId,

apps/sim/lib/billing/core/usage.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export async function getUserUsageData(userId: string): Promise<UsageData> {
4141
}
4242

4343
const stats = userStatsData[0]
44+
const subscription = await getHighestPrioritySubscription(userId)
4445
const currentUsage = Number.parseFloat(
4546
stats.currentPeriodCost?.toString() ?? stats.totalCost.toString()
4647
)
@@ -49,14 +50,19 @@ export async function getUserUsageData(userId: string): Promise<UsageData> {
4950
const isWarning = percentUsed >= 80
5051
const isExceeded = currentUsage >= limit
5152

53+
// Derive billing period dates from subscription (source of truth).
54+
// For free users or missing dates, expose nulls.
55+
const billingPeriodStart = subscription?.periodStart ?? null
56+
const billingPeriodEnd = subscription?.periodEnd ?? null
57+
5258
return {
5359
currentUsage,
5460
limit,
5561
percentUsed,
5662
isWarning,
5763
isExceeded,
58-
billingPeriodStart: stats.billingPeriodStart,
59-
billingPeriodEnd: stats.billingPeriodEnd,
64+
billingPeriodStart,
65+
billingPeriodEnd,
6066
lastPeriodCost: Number.parseFloat(stats.lastPeriodCost?.toString() || '0'),
6167
}
6268
} catch (error) {

0 commit comments

Comments
 (0)