diff --git a/src/controllers/blog.controller.js b/src/controllers/blog.controller.js
index f1757318..533b9b2d 100644
--- a/src/controllers/blog.controller.js
+++ b/src/controllers/blog.controller.js
@@ -913,7 +913,7 @@ async function generateRSSFeed(req, res) {
${escapeXml(description)}
${escapeXml(author)}
${pubDate}
-${categories ? categories + '\n' : ''}
+${categories ? `${categories}\n` : ''}
`;
}
diff --git a/src/models/SubmissionTracking.model.js b/src/models/SubmissionTracking.model.js
new file mode 100644
index 00000000..e6fc690e
--- /dev/null
+++ b/src/models/SubmissionTracking.model.js
@@ -0,0 +1,318 @@
+/**
+ * Submission Tracking Model
+ * Track submission lifecycle for publications
+ */
+
+const mongoose = require('mongoose');
+
+const SubmissionTrackingSchema = new mongoose.Schema({
+ // Submission identification
+ blogPostId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'BlogPost',
+ required: true,
+ index: true
+ },
+ publicationId: {
+ type: String,
+ required: true,
+ index: true
+ },
+ publicationName: {
+ type: String,
+ required: true
+ },
+
+ // Content details
+ title: { type: String, required: true },
+ wordCount: { type: Number },
+ contentType: {
+ type: String,
+ enum: ['letter', 'oped', 'essay', 'social'],
+ required: true
+ },
+
+ // Submission lifecycle
+ status: {
+ type: String,
+ enum: [
+ 'drafted', // Content created, not yet submitted
+ 'ready', // Ready to submit
+ 'submitted', // Submitted to publication
+ 'under_review', // Acknowledged by publication
+ 'revision_requested', // Publication requested changes
+ 'revised', // Changes made, awaiting re-submission
+ 'accepted', // Accepted for publication
+ 'rejected', // Rejected by publication
+ 'published', // Successfully published
+ 'withdrawn' // Submission withdrawn
+ ],
+ default: 'drafted',
+ index: true
+ },
+
+ // Timeline
+ draftedAt: { type: Date, default: Date.now },
+ submittedAt: { type: Date },
+ reviewStartedAt: { type: Date },
+ acceptedAt: { type: Date },
+ rejectedAt: { type: Date },
+ publishedAt: { type: Date },
+
+ // Publication details
+ publishedUrl: { type: String },
+ publishedTitle: { type: String }, // May differ from submitted title
+ edits: [{
+ type: {
+ type: String,
+ enum: ['minor', 'moderate', 'major']
+ },
+ description: String,
+ date: { type: Date, default: Date.now }
+ }],
+
+ // Submission method
+ submissionMethod: {
+ type: String,
+ enum: ['email', 'form', 'website', 'self-publish']
+ },
+ submissionEmail: { type: String },
+ submissionUrl: { type: String },
+
+ // Response tracking
+ responseTimeHours: { type: Number }, // Time from submission to response
+ expectedResponseDays: { type: Number },
+
+ // Editorial feedback
+ editorName: { type: String },
+ editorContact: { type: String },
+ feedback: { type: String },
+ reasonForRejection: { type: String },
+
+ // Internal notes
+ notes: [{
+ content: String,
+ author: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User'
+ },
+ createdAt: { type: Date, default: Date.now }
+ }],
+
+ // Submission package checklist
+ submissionPackage: {
+ coverLetter: {
+ completed: { type: Boolean, default: false },
+ content: String,
+ lastUpdated: Date
+ },
+ notesToEditor: {
+ completed: { type: Boolean, default: false },
+ content: String,
+ lastUpdated: Date
+ },
+ authorBio: {
+ completed: { type: Boolean, default: false },
+ content: String,
+ lastUpdated: Date
+ },
+ pitchEmail: {
+ completed: { type: Boolean, default: false },
+ content: String,
+ lastUpdated: Date
+ },
+ supportingMaterials: [{
+ name: String,
+ description: String,
+ url: String,
+ completed: { type: Boolean, default: false }
+ }]
+ },
+
+ // Metadata
+ createdBy: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true
+ },
+ lastUpdatedBy: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User'
+ }
+}, {
+ timestamps: true
+});
+
+// Indexes for performance
+SubmissionTrackingSchema.index({ status: 1, submittedAt: -1 });
+SubmissionTrackingSchema.index({ publicationId: 1, status: 1 });
+SubmissionTrackingSchema.index({ createdBy: 1, status: 1 });
+
+// Virtual for submission duration
+SubmissionTrackingSchema.virtual('submissionDurationDays').get(function() {
+ if (!this.submittedAt) return null;
+ const endDate = this.publishedAt || this.rejectedAt || new Date();
+ return Math.floor((endDate - this.submittedAt) / (1000 * 60 * 60 * 24));
+});
+
+// Static methods
+
+/**
+ * Get submissions by status
+ */
+SubmissionTrackingSchema.statics.getByStatus = async function(status) {
+ return await this.find({ status })
+ .populate('blogPostId', 'title slug')
+ .populate('createdBy', 'email')
+ .sort({ submittedAt: -1 });
+};
+
+/**
+ * Get submissions for a specific publication
+ */
+SubmissionTrackingSchema.statics.getByPublication = async function(publicationId) {
+ return await this.find({ publicationId })
+ .populate('blogPostId', 'title slug')
+ .sort({ submittedAt: -1 });
+};
+
+/**
+ * Get acceptance rate for a publication
+ */
+SubmissionTrackingSchema.statics.getAcceptanceRate = async function(publicationId) {
+ const total = await this.countDocuments({
+ publicationId,
+ status: { $in: ['accepted', 'rejected', 'published'] }
+ });
+
+ const accepted = await this.countDocuments({
+ publicationId,
+ status: { $in: ['accepted', 'published'] }
+ });
+
+ return total > 0 ? (accepted / total) * 100 : 0;
+};
+
+/**
+ * Get average response time for a publication
+ */
+SubmissionTrackingSchema.statics.getAverageResponseTime = async function(publicationId) {
+ const result = await this.aggregate([
+ {
+ $match: {
+ publicationId,
+ responseTimeHours: { $exists: true, $gt: 0 }
+ }
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: { $avg: '$responseTimeHours' }
+ }
+ }
+ ]);
+
+ return result.length > 0 ? Math.round(result[0].avgResponseTime) : null;
+};
+
+/**
+ * Get submission statistics
+ */
+SubmissionTrackingSchema.statics.getStatistics = async function() {
+ const total = await this.countDocuments();
+ const byStatus = await this.aggregate([
+ {
+ $group: {
+ _id: '$status',
+ count: { $sum: 1 }
+ }
+ }
+ ]);
+
+ const byPublication = await this.aggregate([
+ {
+ $group: {
+ _id: '$publicationId',
+ count: { $sum: 1 },
+ accepted: {
+ $sum: {
+ $cond: [{ $in: ['$status', ['accepted', 'published']] }, 1, 0]
+ }
+ }
+ }
+ },
+ {
+ $sort: { count: -1 }
+ },
+ {
+ $limit: 10
+ }
+ ]);
+
+ return {
+ total,
+ byStatus: byStatus.reduce((acc, item) => {
+ acc[item._id] = item.count;
+ return acc;
+ }, {}),
+ topPublications: byPublication
+ };
+};
+
+// Instance methods
+
+/**
+ * Update status with automatic timestamp management
+ */
+SubmissionTrackingSchema.methods.updateStatus = async function(newStatus, userId) {
+ this.status = newStatus;
+ this.lastUpdatedBy = userId;
+
+ // Set appropriate timestamps
+ const now = new Date();
+ switch (newStatus) {
+ case 'submitted':
+ if (!this.submittedAt) this.submittedAt = now;
+ break;
+ case 'under_review':
+ if (!this.reviewStartedAt) this.reviewStartedAt = now;
+ // Calculate response time if not already set
+ if (this.submittedAt && !this.responseTimeHours) {
+ this.responseTimeHours = Math.round((now - this.submittedAt) / (1000 * 60 * 60));
+ }
+ break;
+ case 'accepted':
+ if (!this.acceptedAt) this.acceptedAt = now;
+ if (this.submittedAt && !this.responseTimeHours) {
+ this.responseTimeHours = Math.round((now - this.submittedAt) / (1000 * 60 * 60));
+ }
+ break;
+ case 'rejected':
+ if (!this.rejectedAt) this.rejectedAt = now;
+ if (this.submittedAt && !this.responseTimeHours) {
+ this.responseTimeHours = Math.round((now - this.submittedAt) / (1000 * 60 * 60));
+ }
+ break;
+ case 'published':
+ if (!this.publishedAt) this.publishedAt = now;
+ break;
+ }
+
+ return await this.save();
+};
+
+/**
+ * Add note
+ */
+SubmissionTrackingSchema.methods.addNote = async function(content, authorId) {
+ this.notes.push({
+ content,
+ author: authorId,
+ createdAt: new Date()
+ });
+ return await this.save();
+};
+
+const SubmissionTracking = mongoose.model('SubmissionTracking', SubmissionTrackingSchema);
+
+module.exports = SubmissionTracking;