/** * Publication Relationship Model * CRM for tracking editorial relationships with publications */ const mongoose = require('mongoose'); const PublicationRelationshipSchema = new mongoose.Schema({ // Publication identification publicationId: { type: String, required: true, unique: true, index: true }, publicationName: { type: String, required: true }, // Relationship stage relationshipStage: { type: String, enum: [ 'cold', // No prior relationship 'introduced', // First submission made 'engaged', // Multiple submissions, some responses 'established', // Regular submissions, predictable responses 'partnership' // Strong relationship, high acceptance rate ], default: 'cold' }, // Editorial contacts editors: [{ name: { type: String, required: true }, email: { type: String }, role: { type: String }, notes: { type: String }, lastContact: { type: Date }, responsive: { type: Boolean, default: true } }], // Submission statistics (calculated from SubmissionTracking) totalSubmissions: { type: Number, default: 0 }, acceptedSubmissions: { type: Number, default: 0 }, rejectedSubmissions: { type: Number, default: 0 }, acceptanceRate: { type: Number, default: 0 }, // Percentage // Response patterns averageResponseTimeHours: { type: Number }, fastestResponseHours: { type: Number }, slowestResponseHours: { type: Number }, lastResponseAt: { type: Date }, // Publication preferences (learned over time) preferredTopics: [{ type: String }], avoidedTopics: [{ type: String }], stylePreferences: { type: String }, wordCountPreference: { min: { type: Number }, max: { type: Number } }, // Editorial feedback patterns commonFeedback: [{ type: String }], editorialStyle: { type: String, enum: ['hands-off', 'light-touch', 'collaborative', 'heavy-editing'], default: 'light-touch' }, // Relationship quality indicators qualityScore: { type: Number, min: 0, max: 100, default: 50 }, lastSuccessfulPublication: { type: Date }, daysSinceLastSuccess: { type: Number }, // Interaction history interactions: [{ type: { type: String, enum: ['submission', 'response', 'publication', 'rejection', 'meeting', 'email', 'call', 'note'] }, date: { type: Date, default: Date.now }, description: { type: String }, outcome: { type: String, enum: ['positive', 'neutral', 'negative'] }, recordedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } }], // Strategic notes strategy: { type: String }, nextSteps: [{ type: String }], priorities: { type: String, enum: ['high', 'medium', 'low'], default: 'medium' }, // Metadata createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, lastUpdatedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } }, { timestamps: true }); // Indexes for performance PublicationRelationshipSchema.index({ relationshipStage: 1, qualityScore: -1 }); PublicationRelationshipSchema.index({ acceptanceRate: -1 }); PublicationRelationshipSchema.index({ priorities: 1, qualityScore: -1 }); // Virtual for relationship health PublicationRelationshipSchema.virtual('relationshipHealth').get(function() { let health = 'unknown'; if (this.totalSubmissions === 0) { health = 'new'; } else if (this.acceptanceRate >= 50) { health = 'excellent'; } else if (this.acceptanceRate >= 30) { health = 'good'; } else if (this.acceptanceRate >= 15) { health = 'fair'; } else { health = 'poor'; } return health; }); // Static methods /** * Get relationships by stage */ PublicationRelationshipSchema.statics.getByStage = async function(stage) { return await this.find({ relationshipStage: stage }) .sort({ qualityScore: -1, acceptanceRate: -1 }); }; /** * Get top performing relationships */ PublicationRelationshipSchema.statics.getTopPerformers = async function(limit = 10) { return await this.find({ totalSubmissions: { $gte: 3 } }) .sort({ acceptanceRate: -1, qualityScore: -1 }) .limit(limit); }; /** * Get relationships needing attention */ PublicationRelationshipSchema.statics.getNeedingAttention = async function() { const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); return await this.find({ $or: [ { qualityScore: { $lt: 30 } }, { acceptanceRate: { $lt: 10 }, totalSubmissions: { $gte: 5 } }, { lastSuccessfulPublication: { $lt: thirtyDaysAgo } } ] }) .sort({ priorities: -1, qualityScore: 1 }); }; /** * Get CRM summary statistics */ PublicationRelationshipSchema.statics.getSummary = async function() { const total = await this.countDocuments(); const byStage = await this.aggregate([ { $group: { _id: '$relationshipStage', count: { $sum: 1 } } } ]); const averageAcceptanceRate = await this.aggregate([ { $match: { totalSubmissions: { $gte: 3 } } }, { $group: { _id: null, avgAcceptanceRate: { $avg: '$acceptanceRate' }, avgQualityScore: { $avg: '$qualityScore' } } } ]); return { total, byStage: byStage.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}), averageAcceptanceRate: averageAcceptanceRate.length > 0 ? Math.round(averageAcceptanceRate[0].avgAcceptanceRate * 10) / 10 : 0, averageQualityScore: averageAcceptanceRate.length > 0 ? Math.round(averageAcceptanceRate[0].avgQualityScore) : 0 }; }; // Instance methods /** * Update statistics from submission data */ PublicationRelationshipSchema.methods.updateStatistics = async function(submissionData) { this.totalSubmissions = submissionData.total; this.acceptedSubmissions = submissionData.accepted; this.rejectedSubmissions = submissionData.rejected; // Calculate acceptance rate if (this.totalSubmissions > 0) { this.acceptanceRate = (this.acceptedSubmissions / this.totalSubmissions) * 100; } // Update response times if (submissionData.avgResponseTime) { this.averageResponseTimeHours = submissionData.avgResponseTime; } if (submissionData.fastestResponse) { this.fastestResponseHours = submissionData.fastestResponse; } if (submissionData.slowestResponse) { this.slowestResponseHours = submissionData.slowestResponse; } // Update days since last success if (this.lastSuccessfulPublication) { const daysSince = Math.floor((new Date() - this.lastSuccessfulPublication) / (1000 * 60 * 60 * 24)); this.daysSinceLastSuccess = daysSince; } // Calculate quality score await this.calculateQualityScore(); // Update relationship stage based on statistics await this.updateRelationshipStage(); return await this.save(); }; /** * Calculate quality score (0-100) */ PublicationRelationshipSchema.methods.calculateQualityScore = async function() { let score = 50; // Base score // Acceptance rate component (40 points max) score += (this.acceptanceRate * 0.4); // Submission volume component (10 points max) score += Math.min(this.totalSubmissions * 2, 10); // Response time component (15 points max) if (this.averageResponseTimeHours) { const expectedHours = 168; // 7 days if (this.averageResponseTimeHours <= expectedHours) { score += 15; } else if (this.averageResponseTimeHours <= expectedHours * 2) { score += 10; } else { score += 5; } } // Recency component (15 points max) if (this.lastSuccessfulPublication) { if (this.daysSinceLastSuccess <= 30) { score += 15; } else if (this.daysSinceLastSuccess <= 90) { score += 10; } else if (this.daysSinceLastSuccess <= 180) { score += 5; } } // Editor relationship component (10 points max) const responsiveEditors = this.editors.filter(e => e.responsive).length; score += Math.min(responsiveEditors * 5, 10); // Cap at 100 this.qualityScore = Math.min(Math.round(score), 100); }; /** * Update relationship stage based on statistics */ PublicationRelationshipSchema.methods.updateRelationshipStage = async function() { if (this.totalSubmissions === 0) { this.relationshipStage = 'cold'; } else if (this.acceptanceRate >= 40 && this.totalSubmissions >= 5) { this.relationshipStage = 'partnership'; } else if (this.acceptanceRate >= 25 && this.totalSubmissions >= 3) { this.relationshipStage = 'established'; } else if (this.totalSubmissions >= 3) { this.relationshipStage = 'engaged'; } else if (this.totalSubmissions >= 1) { this.relationshipStage = 'introduced'; } }; /** * Add interaction */ PublicationRelationshipSchema.methods.addInteraction = async function(type, description, outcome, userId) { this.interactions.push({ type, description, outcome, date: new Date(), recordedBy: userId }); // Update last contact for relevant interaction types if (['response', 'email', 'call', 'meeting'].includes(type)) { this.lastResponseAt = new Date(); } return await this.save(); }; /** * Add or update editor contact */ PublicationRelationshipSchema.methods.addEditor = async function(editorData) { const existingEditor = this.editors.find(e => e.email === editorData.email); if (existingEditor) { // Update existing editor Object.assign(existingEditor, editorData); } else { // Add new editor this.editors.push(editorData); } return await this.save(); }; const PublicationRelationship = mongoose.model('PublicationRelationship', PublicationRelationshipSchema); module.exports = PublicationRelationship;