import { EntityManager } from "@mikro-orm/core"; 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"; import { BusinessQuotaInfoDto, BusinessQuotaStatisticsDto, ComprehensiveQuotaSyncResultDto, QuotaInfoDto, QuotaSyncResultDto, UserQuotaStatisticsDto, } from "../DTO/quota-statistics.dto"; @Injectable() export class QuotaSyncService { private readonly logger = new Logger(QuotaSyncService.name); constructor( private readonly wildDuckUsersService: WildDuckUsersService, private readonly em: EntityManager, ) {} /** * Sync quota usage every 2 hours */ @Cron(CronExpression.EVERY_2_HOURS) async syncAllUsersQuota(): Promise { const em = this.em.fork(); this.logger.log("Starting quota synchronization for all users"); try { const users = await em.find(User, { emailEnabled: true, deletedAt: null, wildduckUserId: { $ne: null }, }); this.logger.log(`Found ${users.length} users to sync quota for`); let successCount = 0; let errorCount = 0; for (const user of users) { try { await this.syncUserQuotaWithEntityManager(user, em); successCount++; } catch (error) { errorCount++; this.logger.error(`Failed to sync quota for user ${user.id}: ${error instanceof Error ? error.message : "Unknown error"}`); } } // Flush all changes at once await em.flush(); this.logger.log(`Quota sync completed: ${successCount} success, ${errorCount} errors`); } catch (error) { this.logger.error("Failed to sync quota for users", `${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Sync business quota usage every 4 hours */ @Cron(CronExpression.EVERY_4_HOURS) 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 */ private async syncUserQuotaWithEntityManager(user: User, _em: EntityManager): Promise { if (!user.wildduckUserId) { this.logger.warn(`User ${user.id} has no WildDuck user ID, skipping quota sync`); return; } try { const wildduckUser = await firstValueFrom(this.wildDuckUsersService.getUser(user.wildduckUserId)); if (!wildduckUser || !wildduckUser.success) { this.logger.warn(`Failed to fetch WildDuck user data for user ${user.id}`); return; } const quotaInfo: QuotaInfoDto = { used: wildduckUser.limits?.quota?.used || 0, allowed: wildduckUser.limits?.quota?.allowed || 0, }; // Update local user quota info user.emailQuotaUsed = quotaInfo.used; user.emailQuota = quotaInfo.allowed; user.lastQuotaSync = new Date(); this.logger.debug(`Synced quota for user ${user.id}: ${quotaInfo.used}/${quotaInfo.allowed} bytes`); } catch (error) { this.logger.error(`Error syncing quota for user ${user.id}:`, `${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } /** * Sync quota for a specific user */ async syncUserQuota(user: User): Promise { const em = this.em.fork(); // Re-fetch the user with the current entity manager const managedUser = await em.findOne(User, { id: user.id }); if (!managedUser) { throw new Error(`User ${user.id} not found`); } await this.syncUserQuotaWithEntityManager(managedUser, em); await em.flush(); } /** * Manually trigger quota sync for a specific user */ async syncUserQuotaById(userId: string): Promise { const em = this.em.fork(); const user = await em.findOne(User, { id: userId, emailEnabled: true, deletedAt: null, }); if (!user) { throw new Error(`User ${userId} not found or not email enabled`); } await this.syncUserQuotaWithEntityManager(user, em); await em.flush(); return { used: user.emailQuotaUsed || 0, allowed: user.emailQuota || 0, }; } /** * Get quota statistics for all users */ async getQuotaStatistics(): Promise { const em = this.em.fork(); const users = await em.find(User, { emailEnabled: true, deletedAt: null, emailQuotaUsed: { $ne: null }, }); const totalQuotaUsed = users.reduce((sum, user) => sum + (user.emailQuotaUsed || 0), 0); const totalQuotaAllowed = users.reduce((sum, user) => sum + (user.emailQuota || 0), 0); const averageUsagePercent = totalQuotaAllowed > 0 ? (totalQuotaUsed / totalQuotaAllowed) * 100 : 0; // Find the most recent sync time const lastSyncUser = await em.findOne( User, { emailEnabled: true, deletedAt: null, lastQuotaSync: { $ne: null }, }, { orderBy: { lastQuotaSync: "DESC" }, }, ); return { totalUsers: users.length, totalQuotaUsed, totalQuotaAllowed, averageUsagePercent: Math.round(averageUsagePercent * 100) / 100, lastSyncTime: lastSyncUser?.lastQuotaSync || null, }; } /** * 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 { this.logger.log("Manual quota sync triggered"); await this.syncAllUsersQuota(); // Return basic statistics const em = this.em.fork(); const users = await em.find(User, { emailEnabled: true, deletedAt: null, wildduckUserId: { $ne: null }, }); const recentSyncs = await em.count(User, { emailEnabled: true, deletedAt: null, lastQuotaSync: { $gte: new Date(Date.now() - 60000) }, // Last minute }); return { success: recentSyncs, 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, }, }; } }