From 0b76c44932c207df204d4f2a30c7ef1392c89eed Mon Sep 17 00:00:00 2001 From: tianzx Date: Tue, 2 Sep 2025 19:33:17 +0800 Subject: [PATCH 1/3] chore: remove outdated `updatedAt` field updates and refine FREE plan refresh logic in quota management - Remove unnecessary `updatedAt` updates; rely on Drizzle's `$onUpdate` mechanism. - Refine FREE plan quota refresh logic to preserve `projectNums` value and streamline period calculations. --- apps/cdn/src/utils/quota-management.ts | 6 +- .../auth/utils/subscription-limits/core.ts | 115 +++++++++++++++--- 2 files changed, 102 insertions(+), 19 deletions(-) diff --git a/apps/cdn/src/utils/quota-management.ts b/apps/cdn/src/utils/quota-management.ts index b64193d..7dbf1f6 100644 --- a/apps/cdn/src/utils/quota-management.ts +++ b/apps/cdn/src/utils/quota-management.ts @@ -104,7 +104,6 @@ async function attemptPaidPlanUploadDeduction( .update(subscriptionLimit) .set({ uploadLimit: sql`(${subscriptionLimit.uploadLimit}) - 1`, - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -234,6 +233,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({ @@ -242,10 +242,9 @@ 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`, }) .where(eq(subscriptionLimit.id, freeLimit.id)) @@ -476,7 +475,6 @@ export async function restoreUploadQuotaOnDeletion(c: AppContext): Promise<{ .update(subscriptionLimit) .set({ uploadLimit: sql`(${subscriptionLimit.uploadLimit}) + 1`, - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( diff --git a/packages/auth/utils/subscription-limits/core.ts b/packages/auth/utils/subscription-limits/core.ts index f74cd13..eb2b36d 100644 --- a/packages/auth/utils/subscription-limits/core.ts +++ b/packages/auth/utils/subscription-limits/core.ts @@ -129,17 +129,27 @@ 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({ - updatedAt: sql`CURRENT_TIMESTAMP`, + aiNums: limits.aiNums, + enhanceNums: limits.aiNums, + uploadLimit: limits.aiNums, + deployLimit: limits.aiNums * 2, + seats: limits.seats, + periodStart: utcPeriodStart.toISOString(), + periodEnd: utcPeriodEnd.toISOString(), }) .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 { @@ -172,7 +182,7 @@ export async function createOrUpdateSubscriptionLimit( // First deactivate existing paid plans await tx .update(subscriptionLimit) - .set({ isActive: false, updatedAt: sql`CURRENT_TIMESTAMP` }) + .set({ isActive: false }) .where( and( eq(subscriptionLimit.organizationId, organizationId), @@ -283,7 +293,6 @@ async function attemptPaidPlanDeduction( .update(subscriptionLimit) .set({ aiNums: sql`(${subscriptionLimit.aiNums}) - 1`, - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -336,7 +345,6 @@ async function attemptPaidPlanEnhanceDeduction( .update(subscriptionLimit) .set({ enhanceNums: sql`(${subscriptionLimit.enhanceNums}) - 1`, - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -519,6 +527,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({ @@ -527,10 +536,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)) @@ -648,9 +656,88 @@ export async function getSubscriptionUsage(organizationId: string): Promise 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 + .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(), + }) + .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() if (freeLimit) planNamesToQuery.add(PLAN_TYPES.FREE) if (paidLimit) planNamesToQuery.add(paidLimit.planName) @@ -817,6 +904,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({ @@ -825,10 +913,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)) @@ -905,7 +992,6 @@ async function attemptPaidPlanProjectDeduction( .update(subscriptionLimit) .set({ projectNums: sql`(${subscriptionLimit.projectNums}) - 1`, - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -1036,6 +1122,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: For project quota, we increment existing projectNums by 1 (user is creating a new project) await tx .update(subscriptionLimit) .set({ @@ -1044,10 +1131,9 @@ async function handleFreePlanProjectDeduction( uploadLimit: freePlanLimits.aiNums, deployLimit: freePlanLimits.aiNums * 2, seats: freePlanLimits.seats, - projectNums: freePlanLimits.projectNums - 1, // Refresh and deduction in one operation + projectNums: freeLimit.projectNums + 1, // Increment existing count by 1 (new project) periodStart: newPeriodStart.toISOString(), periodEnd: nextPeriodEnd.toISOString(), - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where(eq(subscriptionLimit.id, freeLimit.id)) @@ -1515,7 +1601,6 @@ async function attemptPaidPlanDeployDeduction( .update(subscriptionLimit) .set({ deployLimit: sql`(${subscriptionLimit.deployLimit}) - 1`, - updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( From ebde7f33b5f2ceeb50b64c237e899f3939a14fe9 Mon Sep 17 00:00:00 2001 From: tianzx Date: Tue, 2 Sep 2025 19:37:33 +0800 Subject: [PATCH 2/3] chore: refine FREE plan project quota refresh logic - Update `projectNums` handling to refresh and deduct for new project creation. - Improve log clarity with `newProjectNums` field. --- packages/auth/utils/subscription-limits/core.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/auth/utils/subscription-limits/core.ts b/packages/auth/utils/subscription-limits/core.ts index eb2b36d..13f7262 100644 --- a/packages/auth/utils/subscription-limits/core.ts +++ b/packages/auth/utils/subscription-limits/core.ts @@ -1122,7 +1122,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: For project quota, we increment existing projectNums by 1 (user is creating a new project) + // Note: projectNums represents remaining quota, so we refresh to default then deduct 1 await tx .update(subscriptionLimit) .set({ @@ -1131,7 +1131,7 @@ async function handleFreePlanProjectDeduction( uploadLimit: freePlanLimits.aiNums, deployLimit: freePlanLimits.aiNums * 2, seats: freePlanLimits.seats, - projectNums: freeLimit.projectNums + 1, // Increment existing count by 1 (new project) + projectNums: freePlanLimits.projectNums - 1, // Refresh quota and deduct 1 for current project creation periodStart: newPeriodStart.toISOString(), periodEnd: nextPeriodEnd.toISOString(), }) @@ -1139,7 +1139,7 @@ async function handleFreePlanProjectDeduction( 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 From c781b055d2b059e95c8d9e4b2170c0bf2832a19c Mon Sep 17 00:00:00 2001 From: tianzx Date: Tue, 2 Sep 2025 19:44:24 +0800 Subject: [PATCH 3/3] chore: add `updatedAt` field updates to quota and subscription-related logic - Ensure `updatedAt` is updated in all relevant operations for consistency across quota management and subscription limits. --- apps/cdn/src/utils/quota-management.ts | 3 +++ packages/auth/utils/subscription-limits/core.ts | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/cdn/src/utils/quota-management.ts b/apps/cdn/src/utils/quota-management.ts index 7dbf1f6..fa79613 100644 --- a/apps/cdn/src/utils/quota-management.ts +++ b/apps/cdn/src/utils/quota-management.ts @@ -104,6 +104,7 @@ async function attemptPaidPlanUploadDeduction( .update(subscriptionLimit) .set({ uploadLimit: sql`(${subscriptionLimit.uploadLimit}) - 1`, + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -245,6 +246,7 @@ async function handleFreePlanUploadDeduction( // projectNums: keep existing value, don't reset periodStart: newPeriodStart.toISOString(), periodEnd: nextPeriodEnd.toISOString(), + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where(eq(subscriptionLimit.id, freeLimit.id)) @@ -475,6 +477,7 @@ export async function restoreUploadQuotaOnDeletion(c: AppContext): Promise<{ .update(subscriptionLimit) .set({ uploadLimit: sql`(${subscriptionLimit.uploadLimit}) + 1`, + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( diff --git a/packages/auth/utils/subscription-limits/core.ts b/packages/auth/utils/subscription-limits/core.ts index 13f7262..4b3061c 100644 --- a/packages/auth/utils/subscription-limits/core.ts +++ b/packages/auth/utils/subscription-limits/core.ts @@ -142,6 +142,7 @@ export async function createOrUpdateSubscriptionLimit( seats: limits.seats, periodStart: utcPeriodStart.toISOString(), periodEnd: utcPeriodEnd.toISOString(), + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where(eq(subscriptionLimit.id, existingActiveRecord.id)) @@ -182,7 +183,7 @@ export async function createOrUpdateSubscriptionLimit( // First deactivate existing paid plans await tx .update(subscriptionLimit) - .set({ isActive: false }) + .set({ isActive: false, updatedAt: sql`CURRENT_TIMESTAMP` }) .where( and( eq(subscriptionLimit.organizationId, organizationId), @@ -293,6 +294,7 @@ async function attemptPaidPlanDeduction( .update(subscriptionLimit) .set({ aiNums: sql`(${subscriptionLimit.aiNums}) - 1`, + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -345,6 +347,7 @@ async function attemptPaidPlanEnhanceDeduction( .update(subscriptionLimit) .set({ enhanceNums: sql`(${subscriptionLimit.enhanceNums}) - 1`, + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -714,6 +717,7 @@ export async function getSubscriptionUsage(organizationId: string): Promise`(${subscriptionLimit.projectNums}) - 1`, + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and( @@ -1601,6 +1606,7 @@ async function attemptPaidPlanDeployDeduction( .update(subscriptionLimit) .set({ deployLimit: sql`(${subscriptionLimit.deployLimit}) - 1`, + updatedAt: sql`CURRENT_TIMESTAMP`, }) .where( and(