import prisma from "@shieldai/db"; import { SubscriptionTier } from "@shieldai/types"; const TIER_CONFIG = { [SubscriptionTier.BASIC]: { intervalMinutes: 1440, cron: "0 0 * * *" }, [SubscriptionTier.PLUS]: { intervalMinutes: 360, cron: "0 */6 * * *" }, [SubscriptionTier.PREMIUM]: { intervalMinutes: 60, cron: "0 * * * *" }, } as const; export class ScanScheduler { /** * Get the scan interval (in minutes) for a given subscription tier. */ public static getIntervalForTier(tier: SubscriptionTier): number { return TIER_CONFIG[tier]?.intervalMinutes ?? TIER_CONFIG[SubscriptionTier.BASIC].intervalMinutes; } /** * Get the cron expression for a given subscription tier. */ public static getCronForTier(tier: SubscriptionTier): string { return TIER_CONFIG[tier]?.cron ?? TIER_CONFIG[SubscriptionTier.BASIC].cron; } /** * Ensure a user has an active scan schedule based on their subscription tier. * Creates or updates the schedule record. */ async ensureScheduleForUser(userId: string): Promise<{ scheduled: boolean; intervalMinutes: number }> { const user = await prisma.user.findUnique({ where: { id: userId }, select: { subscriptionTier: true }, }); if (!user) { return { scheduled: false, intervalMinutes: 0 }; } const tier = user.subscriptionTier ?? SubscriptionTier.BASIC; const config = TIER_CONFIG[tier]; const nextScan = this.calculateNextScan(); const schedule = await prisma.scanSchedule.upsert({ where: { userId }, update: { intervalMinutes: config.intervalMinutes, cronExpression: config.cron, nextScanAt: nextScan, }, create: { userId, intervalMinutes: config.intervalMinutes, cronExpression: config.cron, status: "ACTIVE", nextScanAt: nextScan, }, }); return { scheduled: schedule.status === "ACTIVE", intervalMinutes: schedule.intervalMinutes, }; } /** * Get all active schedules that are due for scanning. */ async getDueSchedules(): Promise> { const now = new Date(); const due = await prisma.scanSchedule.findMany({ where: { status: "ACTIVE", OR: [ { nextScanAt: { lte: now } }, { nextScanAt: null }, ], }, select: { userId: true, intervalMinutes: true, cronExpression: true, }, }); return due; } /** * Mark a schedule as scanned and compute the next scan time. */ async markScanned(userId: string): Promise { const schedule = await prisma.scanSchedule.findUnique({ where: { userId } }); if (!schedule) { throw new Error(`ScanSchedule not found for user ${userId}`); } const nextScan = this.calculateNextScan(schedule.intervalMinutes); await prisma.scanSchedule.update({ where: { userId }, data: { lastScanAt: new Date(), nextScanAt: nextScan, }, }); return nextScan; } /** * Pause scheduling for a user (e.g., on subscription downgrade or pause). */ async pauseSchedule(userId: string): Promise { await prisma.scanSchedule.updateMany({ where: { userId, status: "ACTIVE" }, data: { status: "PAUSED" }, }); } /** * Resume scheduling for a user and recalculate based on current tier. */ async resumeSchedule(userId: string): Promise { await this.ensureScheduleForUser(userId); } /** * Get the current schedule for a user. */ async getSchedule(userId: string) { return prisma.scanSchedule.findUnique({ where: { userId }, }); } /** * List all active schedules (for admin/monitoring). */ async listActiveSchedules(limit = 100, offset = 0) { return prisma.scanSchedule.findMany({ where: { status: "ACTIVE" }, include: { user: { select: { id: true, email: true, subscriptionTier: true, }, }, }, orderBy: { nextScanAt: "asc" }, take: limit, skip: offset, }); } /** * Calculate the next scan time based on interval. */ private calculateNextScan(intervalMinutes?: number): Date { const minutes = intervalMinutes ?? 60; const next = new Date(); next.setMinutes(next.getMinutes() + minutes); return next; } }