From 7a0c337cda0934411e0c2862f91bc1885bba61fa Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 12 Jul 2025 15:35:39 +0330 Subject: [PATCH] chore: sync service --- .../quota-sync/DTO/quota-statistics.dto.ts | 96 +++++-- .../quota-sync/services/quota-sync.service.ts | 255 ++++++++++++++++-- 2 files changed, 307 insertions(+), 44 deletions(-) diff --git a/src/modules/quota-sync/DTO/quota-statistics.dto.ts b/src/modules/quota-sync/DTO/quota-statistics.dto.ts index af11fb1..bc5b7ed 100644 --- a/src/modules/quota-sync/DTO/quota-statistics.dto.ts +++ b/src/modules/quota-sync/DTO/quota-statistics.dto.ts @@ -1,39 +1,79 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; - -export class QuotaStatisticsDto { - @ApiProperty({ description: "Total number of email users", example: 150 }) - totalUsers: number; - - @ApiProperty({ description: "Total quota used by all users in bytes", example: 5368709120 }) - totalQuotaUsed: number; - - @ApiProperty({ description: "Total quota allowed for all users in bytes", example: 21474836480 }) - totalQuotaAllowed: number; - - @ApiProperty({ description: "Average quota usage percentage", example: 25.0 }) - averageUsagePercent: number; - - @ApiPropertyOptional({ - description: "Last time quota was synced", - example: "2024-01-15T10:30:00Z", - type: String, - format: "date-time", - }) - lastSyncTime: Date | null; -} +import { ApiProperty } from "@nestjs/swagger"; export class QuotaInfoDto { - @ApiProperty({ description: "Quota used in bytes", example: 1073741824 }) + @ApiProperty({ description: "Used quota in bytes", example: 67108864 }) used: number; - @ApiProperty({ description: "Quota allowed in bytes", example: 5368709120 }) + @ApiProperty({ description: "Allowed quota in bytes", example: 134217728 }) allowed: number; } -export class SyncResultDto { - @ApiProperty({ description: "Number of successfully synced users", example: 145 }) +export class BusinessQuotaInfoDto { + @ApiProperty({ description: "Business ID", example: "12345678-1234-1234-1234-123456789012" }) + businessId: string; + + @ApiProperty({ description: "Business name", example: "Acme Corp" }) + businessName: string; + + @ApiProperty({ description: "Total quota allocated in bytes", example: 1073741824 }) + totalQuota: number; + + @ApiProperty({ description: "Used quota in bytes", example: 536870912 }) + usedQuota: number; + + @ApiProperty({ description: "Remaining quota in bytes", example: 536870912 }) + remainingQuota: number; + + @ApiProperty({ description: "Number of users in this business", example: 5 }) + userCount: number; +} + +export class UserQuotaStatisticsDto { + @ApiProperty({ description: "Total number of users", example: 150 }) + totalUsers: number; + + @ApiProperty({ description: "Total quota used across all users in bytes", example: 10737418240 }) + totalQuotaUsed: number; + + @ApiProperty({ description: "Total quota allowed across all users in bytes", example: 21474836480 }) + totalQuotaAllowed: number; + + @ApiProperty({ description: "Average usage percentage", example: 50.0 }) + averageUsagePercent: number; + + @ApiProperty({ description: "Last sync time", example: "2024-01-15T10:30:00Z", nullable: true }) + lastSyncTime: Date | null; +} + +export class BusinessQuotaStatisticsDto { + @ApiProperty({ description: "Total number of businesses", example: 10 }) + totalBusinesses: number; + + @ApiProperty({ description: "Total quota allocated across all businesses in bytes", example: 10737418240 }) + totalQuotaAllocated: number; + + @ApiProperty({ description: "Total quota used across all businesses in bytes", example: 5368709120 }) + totalQuotaUsed: number; + + @ApiProperty({ description: "Average usage percentage", example: 50.0 }) + averageUsagePercent: number; + + @ApiProperty({ description: "Detailed information for each business", type: [BusinessQuotaInfoDto] }) + businesses: BusinessQuotaInfoDto[]; +} + +export class QuotaSyncResultDto { + @ApiProperty({ description: "Number of successful syncs", example: 100 }) success: number; - @ApiProperty({ description: "Number of users with sync errors", example: 5 }) + @ApiProperty({ description: "Number of failed syncs", example: 2 }) errors: number; } + +export class ComprehensiveQuotaSyncResultDto { + @ApiProperty({ description: "User sync results", type: QuotaSyncResultDto }) + userSync: QuotaSyncResultDto; + + @ApiProperty({ description: "Business sync results", type: QuotaSyncResultDto }) + businessSync: QuotaSyncResultDto; +} diff --git a/src/modules/quota-sync/services/quota-sync.service.ts b/src/modules/quota-sync/services/quota-sync.service.ts index 4d30a51..4b61a05 100644 --- a/src/modules/quota-sync/services/quota-sync.service.ts +++ b/src/modules/quota-sync/services/quota-sync.service.ts @@ -3,13 +3,17 @@ import { Injectable, Logger } from "@nestjs/common"; import { Cron, CronExpression } from "@nestjs/schedule"; import { firstValueFrom } from "rxjs"; +import { Business } from "../../businesses/entities/business.entity"; import { WildDuckUsersService } from "../../mail-server/services/wildduck-users.service"; import { User } from "../../users/entities/user.entity"; - -interface QuotaInfo { - used: number; - allowed: number; -} +import { + BusinessQuotaInfoDto, + BusinessQuotaStatisticsDto, + ComprehensiveQuotaSyncResultDto, + QuotaInfoDto, + QuotaSyncResultDto, + UserQuotaStatisticsDto, +} from "../DTO/quota-statistics.dto"; @Injectable() export class QuotaSyncService { @@ -23,7 +27,7 @@ export class QuotaSyncService { /** * Sync quota usage every 15 minutes */ - @Cron(CronExpression.EVERY_2_HOURS) + @Cron(CronExpression.EVERY_10_SECONDS) // @Cron("0 */15 * * * *") async syncAllUsersQuota(): Promise { const em = this.em.fork(); @@ -60,6 +64,43 @@ export class QuotaSyncService { } } + /** + * Sync business quota usage every 4 hours + */ + @Cron(CronExpression.EVERY_10_SECONDS) + async syncAllBusinessQuotas(): Promise { + const em = this.em.fork(); + this.logger.log("Starting business quota synchronization"); + + try { + const businesses = await em.find(Business, { + deletedAt: null, + }); + + this.logger.log(`Found ${businesses.length} businesses to sync quota for`); + + let successCount = 0; + let errorCount = 0; + + for (const business of businesses) { + try { + await this.syncBusinessQuotaWithEntityManager(business, em); + successCount++; + } catch (error) { + errorCount++; + this.logger.error(`Failed to sync quota for business ${business.id}: ${error instanceof Error ? error.message : "Unknown error"}`); + } + } + + // Flush all changes at once + await em.flush(); + + this.logger.log(`Business quota sync completed: ${successCount} success, ${errorCount} errors`); + } catch (error) { + this.logger.error("Failed to sync quota for businesses", `${error instanceof Error ? error.message : "Unknown error"}`); + } + } + /** * Sync quota for a specific user with provided entity manager */ @@ -77,7 +118,7 @@ export class QuotaSyncService { return; } - const quotaInfo: QuotaInfo = { + const quotaInfo: QuotaInfoDto = { used: wildduckUser.limits?.quota?.used || 0, allowed: wildduckUser.limits?.quota?.allowed || 0, }; @@ -113,7 +154,7 @@ export class QuotaSyncService { /** * Manually trigger quota sync for a specific user */ - async syncUserQuotaById(userId: string): Promise { + async syncUserQuotaById(userId: string): Promise { const em = this.em.fork(); const user = await em.findOne(User, { id: userId, @@ -137,13 +178,7 @@ export class QuotaSyncService { /** * Get quota statistics for all users */ - async getQuotaStatistics(): Promise<{ - totalUsers: number; - totalQuotaUsed: number; - totalQuotaAllowed: number; - averageUsagePercent: number; - lastSyncTime: Date | null; - }> { + async getQuotaStatistics(): Promise { const em = this.em.fork(); const users = await em.find(User, { emailEnabled: true, @@ -177,10 +212,131 @@ export class QuotaSyncService { }; } + /** + * Sync quota for a specific business with provided entity manager + */ + private async syncBusinessQuotaWithEntityManager(business: Business, em: EntityManager): Promise { + try { + // Get all active users for this business + const users = await em.find(User, { + business: business.id, + emailEnabled: true, + deletedAt: null, + emailQuotaUsed: { $ne: null }, + }); + + // Calculate total used quota by summing all users' usage + const totalUsedQuota = users.reduce((sum, user) => sum + Number(user.emailQuotaUsed || 0), 0); + + // Convert business quota to number for calculations + const businessQuota = Number(business.quota); + + // Update business quota usage + business.usedQuota = totalUsedQuota; + business.remainingQuota = Math.max(0, businessQuota - totalUsedQuota); + + this.logger.debug(`Synced business quota for ${business.name}: ${totalUsedQuota}/${businessQuota} bytes (${users.length} users)`); + } catch (error) { + this.logger.error(`Error syncing business quota for ${business.id}:`, `${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + /** + * Sync quota for a specific business + */ + async syncBusinessQuota(business: Business): Promise { + const em = this.em.fork(); + + // Re-fetch the business with the current entity manager + const managedBusiness = await em.findOne(Business, { id: business.id }); + if (!managedBusiness) { + throw new Error(`Business ${business.id} not found`); + } + + await this.syncBusinessQuotaWithEntityManager(managedBusiness, em); + await em.flush(); + } + + /** + * Manually trigger quota sync for a specific business + */ + async syncBusinessQuotaById(businessId: string): Promise { + const em = this.em.fork(); + const business = await em.findOne(Business, { + id: businessId, + deletedAt: null, + }); + + if (!business) { + throw new Error(`Business ${businessId} not found`); + } + + await this.syncBusinessQuotaWithEntityManager(business, em); + await em.flush(); + + // Get user count for this business + const userCount = await em.count(User, { + business: businessId, + emailEnabled: true, + deletedAt: null, + }); + + return { + businessId: business.id, + businessName: business.name, + totalQuota: Number(business.quota), + usedQuota: Number(business.usedQuota), + remainingQuota: Number(business.remainingQuota), + userCount, + }; + } + + /** + * Get quota statistics for all businesses + */ + async getBusinessQuotaStatistics(): Promise { + const em = this.em.fork(); + const businesses = await em.find(Business, { + deletedAt: null, + }); + + const totalQuotaAllocated = businesses.reduce((sum, business) => sum + Number(business.quota), 0); + const totalQuotaUsed = businesses.reduce((sum, business) => sum + Number(business.usedQuota), 0); + const averageUsagePercent = totalQuotaAllocated > 0 ? (totalQuotaUsed / totalQuotaAllocated) * 100 : 0; + + // Get detailed info for each business + const businessInfos: BusinessQuotaInfoDto[] = []; + for (const business of businesses) { + const userCount = await em.count(User, { + business: business.id, + emailEnabled: true, + deletedAt: null, + }); + + businessInfos.push({ + businessId: business.id, + businessName: business.name, + totalQuota: Number(business.quota), + usedQuota: Number(business.usedQuota), + remainingQuota: Number(business.remainingQuota), + userCount, + }); + } + + return { + totalBusinesses: businesses.length, + totalQuotaAllocated, + totalQuotaUsed, + averageUsagePercent: Math.round(averageUsagePercent * 100) / 100, + businesses: businessInfos, + }; + } + /** * Manual sync for all users (useful for admin endpoints) */ - async forceSyncAllUsers(): Promise<{ success: number; errors: number }> { + async forceSyncAllUsers(): Promise { this.logger.log("Manual quota sync triggered"); await this.syncAllUsersQuota(); @@ -203,4 +359,71 @@ export class QuotaSyncService { errors: users.length - recentSyncs, }; } + + /** + * Manual sync for all businesses (useful for admin endpoints) + */ + async forceSyncAllBusinesses(): Promise { + this.logger.log("Manual business quota sync triggered"); + await this.syncAllBusinessQuotas(); + + // Return basic statistics + const em = this.em.fork(); + const businesses = await em.find(Business, { + deletedAt: null, + }); + + // Count businesses that were recently updated (assuming sync was successful) + const recentSyncs = businesses.filter((business) => business.updatedAt && business.updatedAt > new Date(Date.now() - 300000)).length; // Last 5 minutes + + return { + success: recentSyncs, + errors: businesses.length - recentSyncs, + }; + } + + /** + * Comprehensive sync for both users and businesses + */ + async syncAllQuotas(): Promise { + this.logger.log("Starting comprehensive quota sync for users and businesses"); + + // First sync all users + await this.syncAllUsersQuota(); + + // Then sync all businesses based on updated user data + await this.syncAllBusinessQuotas(); + + // Get statistics for both + const em = this.em.fork(); + + const users = await em.find(User, { + emailEnabled: true, + deletedAt: null, + wildduckUserId: { $ne: null }, + }); + + const businesses = await em.find(Business, { + deletedAt: null, + }); + + const recentUserSyncs = await em.count(User, { + emailEnabled: true, + deletedAt: null, + lastQuotaSync: { $gte: new Date(Date.now() - 60000) }, // Last minute + }); + + const recentBusinessSyncs = businesses.filter((business) => business.updatedAt && business.updatedAt > new Date(Date.now() - 300000)).length; // Last 5 minutes + + return { + userSync: { + success: recentUserSyncs, + errors: users.length - recentUserSyncs, + }, + businessSync: { + success: recentBusinessSyncs, + errors: businesses.length - recentBusinessSyncs, + }, + }; + } }