Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
313 lines
8.8 KiB
JavaScript
313 lines
8.8 KiB
JavaScript
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); |