chore: sync service

This commit is contained in:
mahyargdz
2025-07-12 15:35:39 +03:30
parent 7177a13e94
commit 7a0c337cda
2 changed files with 307 additions and 44 deletions
@@ -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;
}
@@ -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<void> {
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<void> {
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<QuotaInfo> {
async syncUserQuotaById(userId: string): Promise<QuotaInfoDto> {
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<UserQuotaStatisticsDto> {
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<void> {
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<void> {
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<BusinessQuotaInfoDto> {
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<BusinessQuotaStatisticsDto> {
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<QuotaSyncResultDto> {
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<QuotaSyncResultDto> {
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<ComprehensiveQuotaSyncResultDto> {
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,
},
};
}
}