Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion apps/cdn/src/utils/quota-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ async function handleFreePlanUploadDeduction(

// Refresh the FREE plan with new quota and immediately deduct 1 for this request
// This ensures the refresh operation is immediately successful
// Note: projectNums is preserved to maintain user's actual project count
await tx
.update(subscriptionLimit)
.set({
Expand All @@ -242,7 +243,7 @@ async function handleFreePlanUploadDeduction(
uploadLimit: freePlanLimits.aiNums - 1, // Refresh and deduct in one operation
deployLimit: freePlanLimits.aiNums * 2,
seats: freePlanLimits.seats,
projectNums: freePlanLimits.projectNums,
// projectNums: keep existing value, don't reset
periodStart: newPeriodStart.toISOString(),
periodEnd: nextPeriodEnd.toISOString(),
updatedAt: sql`CURRENT_TIMESTAMP`,
Expand Down
111 changes: 101 additions & 10 deletions packages/auth/utils/subscription-limits/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,28 @@ export async function createOrUpdateSubscriptionLimit(
.then((rows) => rows[0])

if (existingActiveRecord) {
// Update existing active FREE plan
// Update existing active FREE plan - reset quotas to default values
// This ensures users get fresh quota when their FREE plan is refreshed
// Note: projectNums is NOT reset as it represents existing user projects
await db
.update(subscriptionLimit)
.set({
aiNums: limits.aiNums,
enhanceNums: limits.aiNums,
uploadLimit: limits.aiNums,
deployLimit: limits.aiNums * 2,
seats: limits.seats,
periodStart: utcPeriodStart.toISOString(),
periodEnd: utcPeriodEnd.toISOString(),
updatedAt: sql`CURRENT_TIMESTAMP`,
})
.where(eq(subscriptionLimit.id, existingActiveRecord.id))

log.subscription('warn', 'Updated existing FREE plan', {
log.subscription('info', 'Updated FREE plan with quota reset', {
organizationId,
planName: PLAN_TYPES.FREE,
aiNums: limits.aiNums,
seats: limits.seats,
operation: 'create_or_update_subscription_limit'
});
} else {
Expand Down Expand Up @@ -519,6 +530,7 @@ async function handleFreePlanDeduction(

// Refresh the FREE plan with new quota and immediately deduct 1 for this request
// This ensures the refresh operation is immediately successful
// Note: projectNums is preserved to maintain user's actual project count
await tx
.update(subscriptionLimit)
.set({
Expand All @@ -527,10 +539,9 @@ async function handleFreePlanDeduction(
uploadLimit: freePlanLimits.aiNums,
deployLimit: freePlanLimits.aiNums * 2,
seats: freePlanLimits.seats,
projectNums: freePlanLimits.projectNums,
// projectNums: keep existing value, don't reset
periodStart: newPeriodStart.toISOString(),
periodEnd: nextPeriodEnd.toISOString(),
updatedAt: sql`CURRENT_TIMESTAMP`,
})
.where(eq(subscriptionLimit.id, freeLimit.id))

Expand Down Expand Up @@ -648,9 +659,89 @@ export async function getSubscriptionUsage(organizationId: string): Promise<Subs

const authDb = await getAuthDb()

const freeLimit = getLatestActiveLimit(limits, 'free')
// Get current time for expiry check
const { rows } = await db.execute(sql`SELECT NOW() as "dbNow"`)
const [{ dbNow }] = rows as [{ dbNow: string | Date }]
const now = typeof dbNow === 'string' ? new Date(dbNow) : dbNow

let freeLimit = getLatestActiveLimit(limits, 'free')
const paidLimit = getLatestActiveLimit(limits, 'paid')

// Check if FREE plan has expired and needs refresh
if (freeLimit) {
const periodEnd = new Date(freeLimit.periodEnd)
const nowTimestamp = now.getTime()
const periodEndTimestamp = periodEnd.getTime()

if (nowTimestamp > periodEndTimestamp) {
log.subscription('info', 'FREE plan expired during usage query, refreshing', {
organizationId,
periodEnd: freeLimit.periodEnd,
operation: 'getSubscriptionUsage'
});

// Get FREE plan default limits
const { limits: freePlanLimits } = await getPlanLimits(PLAN_TYPES.FREE)

// Calculate new period
let newPeriodStart = new Date(freeLimit.periodStart)
while (addMonths(newPeriodStart, 1).getTime() <= nowTimestamp) {
newPeriodStart = addMonths(newPeriodStart, 1)
}

// Align to UTC midnight
newPeriodStart = new Date(
Date.UTC(
newPeriodStart.getUTCFullYear(),
newPeriodStart.getUTCMonth(),
newPeriodStart.getUTCDate(),
0,
0,
0,
0
)
)

const nextPeriodEnd = addMonths(newPeriodStart, 1)

// Refresh the FREE plan quota
// Note: updatedAt will be automatically updated by Drizzle's .$onUpdate() mechanism
await db
Comment thread
nextify2025 marked this conversation as resolved.
.update(subscriptionLimit)
.set({
aiNums: freePlanLimits.aiNums,
enhanceNums: freePlanLimits.aiNums,
uploadLimit: freePlanLimits.aiNums,
deployLimit: freePlanLimits.aiNums * 2,
seats: freePlanLimits.seats,
// projectNums: keep existing value, don't reset
periodStart: newPeriodStart.toISOString(),
periodEnd: nextPeriodEnd.toISOString(),
updatedAt: sql`CURRENT_TIMESTAMP`,
})
.where(eq(subscriptionLimit.id, freeLimit.id))

// Update the local freeLimit object with refreshed values
freeLimit = {
...freeLimit,
aiNums: freePlanLimits.aiNums,
enhanceNums: freePlanLimits.aiNums,
uploadLimit: freePlanLimits.aiNums,
deployLimit: freePlanLimits.aiNums * 2,
seats: freePlanLimits.seats,
periodStart: newPeriodStart.toISOString(),
periodEnd: nextPeriodEnd.toISOString(),
}

log.subscription('info', 'FREE plan refreshed during usage query', {
organizationId,
newAiNums: freePlanLimits.aiNums,
newPeriodEnd: nextPeriodEnd.toISOString(),
operation: 'getSubscriptionUsage'
});
}
}

const planNamesToQuery = new Set<string>()
if (freeLimit) planNamesToQuery.add(PLAN_TYPES.FREE)
if (paidLimit) planNamesToQuery.add(paidLimit.planName)
Expand Down Expand Up @@ -817,6 +908,7 @@ async function handleFreePlanEnhanceDeduction(

// Refresh the FREE plan with new quota and immediately deduct 1 for this request
// This ensures the refresh operation is immediately successful
// Note: projectNums is preserved to maintain user's actual project count
await tx
.update(subscriptionLimit)
.set({
Expand All @@ -825,10 +917,9 @@ async function handleFreePlanEnhanceDeduction(
uploadLimit: freePlanLimits.aiNums,
deployLimit: freePlanLimits.aiNums * 2,
seats: freePlanLimits.seats,
projectNums: freePlanLimits.projectNums,
// projectNums: keep existing value, don't reset
periodStart: newPeriodStart.toISOString(),
periodEnd: nextPeriodEnd.toISOString(),
updatedAt: sql`CURRENT_TIMESTAMP`,
})
.where(eq(subscriptionLimit.id, freeLimit.id))

Expand Down Expand Up @@ -1036,6 +1127,7 @@ async function handleFreePlanProjectDeduction(

// Refresh the FREE plan with new quota and immediately deduct 1 for this request
// This ensures the refresh operation is immediately successful
// Note: projectNums represents remaining quota, so we refresh to default then deduct 1
await tx
.update(subscriptionLimit)
.set({
Expand All @@ -1044,16 +1136,15 @@ async function handleFreePlanProjectDeduction(
uploadLimit: freePlanLimits.aiNums,
deployLimit: freePlanLimits.aiNums * 2,
seats: freePlanLimits.seats,
projectNums: freePlanLimits.projectNums - 1, // Refresh and deduction in one operation
projectNums: freePlanLimits.projectNums - 1, // Refresh quota and deduct 1 for current project creation
periodStart: newPeriodStart.toISOString(),
periodEnd: nextPeriodEnd.toISOString(),
updatedAt: sql`CURRENT_TIMESTAMP`,
})
.where(eq(subscriptionLimit.id, freeLimit.id))

log.subscription('info', 'FREE plan refreshed and project deducted', {
organizationId,
remaining: freePlanLimits.projectNums - 1,
newProjectNums: freePlanLimits.projectNums - 1,
operation: 'project_usage_deduction'
})
return true // Refresh and deduction completed successfully
Expand Down
Loading