import { prisma, SubscriptionTier, SubscriptionStatus } from '@shieldsai/shared-db'; import { tierConfig } from '@shieldsai/shared-billing'; import { darkwatchScanQueue } from '@shieldsai/jobs'; import { randomUUID } from 'crypto'; const CRON_EXPRESSIONS = { daily: '0 0 * * *', hourly: '0 * * * *', realtime: null, }; export class SchedulerService { async scheduleSubscriptionScans() { const activeSubscriptions = await prisma.subscription.findMany({ where: { tier: { in: [SubscriptionTier.basic, SubscriptionTier.plus, SubscriptionTier.premium] }, status: SubscriptionStatus.active, }, select: { id: true, tier: true, userId: true, }, }); const jobsEnqueued = []; for (const subscription of activeSubscriptions) { const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; const cron = CRON_EXPRESSIONS[frequency]; if (!cron) { continue; } const jobKey = `scheduled-scan:${subscription.id}`; try { await darkwatchScanQueue.add( 'scheduled-scan', { subscriptionId: subscription.id, tier: subscription.tier, scanType: 'scheduled', }, { jobId: jobKey, repeat: { every: frequency === 'daily' ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000, }, priority: subscription.tier === SubscriptionTier.premium ? 1 : 3, } ); jobsEnqueued.push({ subscriptionId: subscription.id, tier: subscription.tier, frequency, }); } catch (error) { if ((error as Error).message?.includes('Duplicate')) { continue; } console.error( `[SchedulerService] Failed to schedule scan for ${subscription.id}:`, error ); } } return jobsEnqueued; } async enqueueOnDemandScan(subscriptionId: string) { const subscription = await prisma.subscription.findUnique({ where: { id: subscriptionId }, select: { id: true, tier: true }, }); if (!subscription) { throw new Error(`Subscription ${subscriptionId} not found`); } return darkwatchScanQueue.add( 'on-demand-scan', { subscriptionId, tier: subscription.tier, scanType: 'on-demand', }, { priority: 1, jobId: `on-demand-scan:${subscriptionId}:${randomUUID()}`, } ); } async enqueueRealtimeTrigger(subscriptionId: string, sourceData: Record) { const subscription = await prisma.subscription.findUnique({ where: { id: subscriptionId }, select: { id: true, tier: true }, }); if (!subscription || subscription.tier !== SubscriptionTier.premium) { throw new Error('Realtime triggers require Premium tier'); } return darkwatchScanQueue.add( 'realtime-trigger', { subscriptionId, tier: subscription.tier, scanType: 'realtime', sourceData, }, { priority: 0, jobId: `realtime-trigger:${subscriptionId}:${randomUUID()}`, } ); } async rescheduleAll() { const repeatableJobs = await darkwatchScanQueue.getRepeatableJobs(); for (const job of repeatableJobs) { await darkwatchScanQueue.removeRepeatableByKey(job.key); } return this.scheduleSubscriptionScans(); } async getScanSchedule(subscriptionId: string) { const subscription = await prisma.subscription.findUnique({ where: { id: subscriptionId }, select: { tier: true }, }); if (!subscription) return null; const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; return { subscriptionId, tier: subscription.tier, frequency, cron: CRON_EXPRESSIONS[frequency], nextRun: frequency === 'realtime' ? 'event-driven' : CRON_EXPRESSIONS[frequency], }; } } export const schedulerService = new SchedulerService();