import mongoose from 'mongoose'; const scheduledCampaignSchema = new mongoose.Schema({ // Multi-tenant support tenantId: { type: mongoose.Schema.Types.ObjectId, ref: 'Tenant', required: true, index: true }, accountId: { type: String, required: true, index: true }, campaignId: { type: String, required: true, index: true }, campaignName: { type: String, required: true }, type: { type: String, enum: ['one-time', 'recurring', 'trigger-based'], required: true }, schedule: { // For one-time campaigns startDateTime: Date, // For recurring campaigns recurring: { pattern: { type: String, enum: ['daily', 'weekly', 'monthly', 'custom'] }, frequency: { interval: Number, // e.g., every 2 days unit: { type: String, enum: ['minutes', 'hours', 'days', 'weeks', 'months'] } }, daysOfWeek: [Number], // 0-6 (Sunday-Saturday) daysOfMonth: [Number], // 1-31 timeOfDay: String, // HH:MM format timezone: { type: String, default: 'UTC' }, endDate: Date, maxOccurrences: Number }, // For trigger-based campaigns triggers: [{ type: { type: String, enum: ['user_event', 'date_based', 'condition_met', 'external_api'] }, conditions: mongoose.Schema.Types.Mixed, delay: { value: Number, unit: { type: String, enum: ['minutes', 'hours', 'days'] } } }] }, targetAudience: { type: { type: String, enum: ['all', 'segment', 'group', 'individual', 'dynamic'] }, segmentId: String, groupIds: [String], userIds: [String], dynamicCriteria: mongoose.Schema.Types.Mixed }, messageConfig: { templateId: String, content: String, personalization: { enabled: { type: Boolean, default: true }, fields: [String] }, variations: [{ name: String, templateId: String, weight: Number // For A/B testing }] }, deliverySettings: { priority: { type: String, enum: ['low', 'normal', 'high', 'critical'], default: 'normal' }, rateLimiting: { enabled: { type: Boolean, default: true }, messagesPerHour: Number, messagesPerUser: Number }, retryPolicy: { maxRetries: { type: Number, default: 3 }, retryDelay: { type: Number, default: 300 } // seconds }, quietHours: { enabled: { type: Boolean, default: true }, start: String, // HH:MM format end: String, timezone: String } }, status: { type: String, enum: ['draft', 'scheduled', 'active', 'paused', 'completed', 'cancelled', 'failed'], default: 'draft', index: true }, execution: { nextRunAt: Date, lastRunAt: Date, runCount: { type: Number, default: 0 }, history: [{ runAt: Date, status: String, messagesSent: Number, errors: Number, duration: Number, details: mongoose.Schema.Types.Mixed }] }, statistics: { totalRuns: { type: Number, default: 0 }, totalMessagesSent: { type: Number, default: 0 }, totalDelivered: { type: Number, default: 0 }, totalFailed: { type: Number, default: 0 }, avgDeliveryRate: { type: Number, default: 0 }, lastUpdated: Date }, metadata: { createdBy: String, updatedBy: String, tags: [String], notes: String, integrations: [{ type: String, config: mongoose.Schema.Types.Mixed }] }, notifications: { onStart: { enabled: { type: Boolean, default: false }, channels: [String], // email, slack, webhook recipients: [String] }, onComplete: { enabled: { type: Boolean, default: true }, channels: [String], recipients: [String] }, onFailure: { enabled: { type: Boolean, default: true }, channels: [String], recipients: [String] } } }, { timestamps: true }); // Indexes scheduledCampaignSchema.index({ accountId: 1, status: 1 }); scheduledCampaignSchema.index({ 'execution.nextRunAt': 1, status: 1 }); scheduledCampaignSchema.index({ type: 1, status: 1 }); // Multi-tenant indexes scheduledCampaignSchema.index({ tenantId: 1, accountId: 1, status: 1 }); scheduledCampaignSchema.index({ tenantId: 1, 'execution.nextRunAt': 1, status: 1 }); scheduledCampaignSchema.index({ tenantId: 1, type: 1, status: 1 }); // Virtual for isActive scheduledCampaignSchema.virtual('isActive').get(function() { return this.status === 'active' || this.status === 'scheduled'; }); // Methods scheduledCampaignSchema.methods.calculateNextRun = function() { if (this.type === 'one-time') { return this.schedule.startDateTime; } if (this.type === 'recurring') { const now = new Date(); const recurring = this.schedule.recurring; // Implementation for calculating next run based on pattern // This would use date-fns or similar library for complex date calculations // Simplified version here if (recurring.pattern === 'daily') { const next = new Date(now); next.setDate(next.getDate() + (recurring.frequency?.interval || 1)); return next; } // Add more pattern calculations... } return null; }; scheduledCampaignSchema.methods.shouldRun = function() { const now = new Date(); if (this.status !== 'active' && this.status !== 'scheduled') { return false; } if (this.execution.nextRunAt && this.execution.nextRunAt > now) { return false; } if (this.type === 'recurring') { const recurring = this.schedule.recurring; // Check end date if (recurring.endDate && recurring.endDate < now) { return false; } // Check max occurrences if (recurring.maxOccurrences && this.execution.runCount >= recurring.maxOccurrences) { return false; } } return true; }; scheduledCampaignSchema.methods.recordExecution = async function(result) { this.execution.lastRunAt = new Date(); this.execution.runCount += 1; // Add to history this.execution.history.push({ runAt: new Date(), status: result.status, messagesSent: result.messagesSent || 0, errors: result.errors || 0, duration: result.duration, details: result.details }); // Keep only last 100 executions if (this.execution.history.length > 100) { this.execution.history = this.execution.history.slice(-100); } // Update statistics this.statistics.totalRuns += 1; this.statistics.totalMessagesSent += result.messagesSent || 0; this.statistics.totalDelivered += result.delivered || 0; this.statistics.totalFailed += result.failed || 0; this.statistics.lastUpdated = new Date(); if (this.statistics.totalMessagesSent > 0) { this.statistics.avgDeliveryRate = (this.statistics.totalDelivered / this.statistics.totalMessagesSent) * 100; } // Calculate next run if (this.type === 'recurring' && result.status === 'success') { this.execution.nextRunAt = this.calculateNextRun(); } else if (this.type === 'one-time') { this.status = 'completed'; } await this.save(); }; // Statics scheduledCampaignSchema.statics.findDueSchedules = async function(limit = 100) { const now = new Date(); return this.find({ status: { $in: ['active', 'scheduled'] }, $or: [ { 'execution.nextRunAt': { $lte: now } }, { 'execution.nextRunAt': null } ] }) .limit(limit) .sort({ 'execution.nextRunAt': 1 }); }; scheduledCampaignSchema.statics.findByAccountAndStatus = async function(accountId, status) { return this.find({ accountId, status }) .sort({ createdAt: -1 }); }; export default mongoose.model('ScheduledCampaign', scheduledCampaignSchema);