- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
453 lines
11 KiB
JavaScript
453 lines
11 KiB
JavaScript
/**
|
|
* Case Study Controller
|
|
* Community case study submissions with AI review
|
|
*/
|
|
|
|
const CaseSubmission = require('../models/CaseSubmission.model');
|
|
const ModerationQueue = require('../models/ModerationQueue.model');
|
|
const GovernanceLog = require('../models/GovernanceLog.model');
|
|
const BoundaryEnforcer = require('../services/BoundaryEnforcer.service');
|
|
const logger = require('../utils/logger.util');
|
|
|
|
/**
|
|
* Submit case study (public)
|
|
* POST /api/cases/submit
|
|
*
|
|
* Phase 1: Manual review (no AI)
|
|
* Phase 2: Add AI categorization with claudeAPI.reviewCaseStudy()
|
|
*/
|
|
async function submitCase(req, res) {
|
|
try {
|
|
const { submitter, case_study } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!submitter?.name || !submitter?.email) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required submitter information'
|
|
});
|
|
}
|
|
|
|
if (!case_study?.title || !case_study?.description || !case_study?.failure_mode) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required case study information'
|
|
});
|
|
}
|
|
|
|
logger.info(`Case study submitted: ${case_study.title} by ${submitter.name}`);
|
|
|
|
// Create submission (Phase 1: no AI review yet)
|
|
const submission = await CaseSubmission.create({
|
|
submitter,
|
|
case_study,
|
|
ai_review: {
|
|
relevance_score: 0.5, // Default, will be AI-assessed in Phase 2
|
|
completeness_score: 0.5,
|
|
recommended_category: 'uncategorized'
|
|
},
|
|
moderation: {
|
|
status: 'pending'
|
|
}
|
|
});
|
|
|
|
// Add to moderation queue for human review
|
|
await ModerationQueue.create({
|
|
type: 'CASE_SUBMISSION',
|
|
reference_collection: 'case_submissions',
|
|
reference_id: submission._id,
|
|
quadrant: 'OPERATIONAL',
|
|
data: {
|
|
submitter,
|
|
case_study
|
|
},
|
|
priority: 'medium',
|
|
status: 'PENDING_APPROVAL',
|
|
requires_human_approval: true,
|
|
human_required_reason: 'All case submissions require human review and approval'
|
|
});
|
|
|
|
logger.info(`Case submission created: ${submission._id}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Thank you for your submission. We will review it shortly.',
|
|
submission_id: submission._id,
|
|
governance: {
|
|
human_review: true,
|
|
note: 'All case studies are reviewed by humans before publication'
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Submit case error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred while submitting your case study'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get case submission statistics (admin)
|
|
* GET /api/cases/submissions/stats
|
|
*/
|
|
async function getStats(req, res) {
|
|
try {
|
|
const total = await CaseSubmission.countDocuments({});
|
|
const pending = await CaseSubmission.countDocuments({ 'moderation.status': 'pending' });
|
|
const approved = await CaseSubmission.countDocuments({ 'moderation.status': 'approved' });
|
|
const rejected = await CaseSubmission.countDocuments({ 'moderation.status': 'rejected' });
|
|
const needsInfo = await CaseSubmission.countDocuments({ 'moderation.status': 'needs_info' });
|
|
|
|
res.json({
|
|
success: true,
|
|
stats: {
|
|
total,
|
|
pending,
|
|
approved,
|
|
rejected,
|
|
needs_info: needsInfo
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get stats error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all case submissions (admin)
|
|
* GET /api/cases/submissions?status=pending&failure_mode=pattern_bias&score=high&sort=relevance_score
|
|
*/
|
|
async function listSubmissions(req, res) {
|
|
try {
|
|
const {
|
|
status,
|
|
failure_mode,
|
|
score,
|
|
sort = 'submitted_at',
|
|
limit = 20,
|
|
skip = 0
|
|
} = req.query;
|
|
|
|
// Build query filter
|
|
const filter = {};
|
|
|
|
if (status) {
|
|
filter['moderation.status'] = status;
|
|
}
|
|
|
|
if (failure_mode) {
|
|
filter['case_study.failure_mode'] = failure_mode;
|
|
}
|
|
|
|
// AI score filtering
|
|
if (score) {
|
|
if (score === 'high') {
|
|
filter['ai_review.relevance_score'] = { $gte: 0.7 };
|
|
} else if (score === 'medium') {
|
|
filter['ai_review.relevance_score'] = { $gte: 0.4, $lt: 0.7 };
|
|
} else if (score === 'low') {
|
|
filter['ai_review.relevance_score'] = { $lt: 0.4 };
|
|
}
|
|
}
|
|
|
|
// Build sort options
|
|
const sortOptions = {};
|
|
if (sort === 'submitted_at') {
|
|
sortOptions.submitted_at = -1; // Newest first
|
|
} else if (sort === 'relevance_score') {
|
|
sortOptions['ai_review.relevance_score'] = -1; // Highest first
|
|
} else if (sort === 'completeness_score') {
|
|
sortOptions['ai_review.completeness_score'] = -1; // Highest first
|
|
}
|
|
|
|
// Query database
|
|
const submissions = await CaseSubmission.find(filter)
|
|
.sort(sortOptions)
|
|
.limit(parseInt(limit))
|
|
.skip(parseInt(skip))
|
|
.lean();
|
|
|
|
const total = await CaseSubmission.countDocuments(filter);
|
|
|
|
res.json({
|
|
success: true,
|
|
submissions,
|
|
pagination: {
|
|
total,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip),
|
|
hasMore: parseInt(skip) + submissions.length < total
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List submissions error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List high-relevance pending submissions (admin)
|
|
* GET /api/cases/submissions/high-relevance
|
|
*/
|
|
async function listHighRelevance(req, res) {
|
|
try {
|
|
const { limit = 10 } = req.query;
|
|
|
|
const submissions = await CaseSubmission.findHighRelevance({
|
|
limit: parseInt(limit)
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
count: submissions.length,
|
|
submissions
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List high relevance error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get case submission by ID (admin)
|
|
* GET /api/cases/submissions/:id
|
|
*/
|
|
async function getSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const submission = await CaseSubmission.findById(id);
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Case submission not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
submission
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get submission error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Approve case submission (admin)
|
|
* POST /api/cases/submissions/:id/approve
|
|
*/
|
|
async function approveSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { notes } = req.body;
|
|
|
|
const submission = await CaseSubmission.findById(id);
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Case submission not found'
|
|
});
|
|
}
|
|
|
|
if (submission.moderation.status === 'approved') {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Submission is already approved'
|
|
});
|
|
}
|
|
|
|
const success = await CaseSubmission.approve(id, req.user._id, notes || '');
|
|
|
|
if (!success) {
|
|
return res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to approve submission'
|
|
});
|
|
}
|
|
|
|
logger.info(`Case submission approved: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Case submission approved successfully',
|
|
note: 'You can now publish this as a case study document'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Approve submission error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reject case submission (admin)
|
|
* POST /api/cases/submissions/:id/reject
|
|
*/
|
|
async function rejectSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { reason } = req.body;
|
|
|
|
if (!reason) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Rejection reason is required'
|
|
});
|
|
}
|
|
|
|
const submission = await CaseSubmission.findById(id);
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Case submission not found'
|
|
});
|
|
}
|
|
|
|
const success = await CaseSubmission.reject(id, req.user._id, reason);
|
|
|
|
if (!success) {
|
|
return res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to reject submission'
|
|
});
|
|
}
|
|
|
|
logger.info(`Case submission rejected: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Case submission rejected',
|
|
note: 'Consider notifying the submitter with feedback'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Reject submission error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request more information (admin)
|
|
* POST /api/cases/submissions/:id/request-info
|
|
*/
|
|
async function requestMoreInfo(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { requested_info } = req.body;
|
|
|
|
if (!requested_info) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Requested information must be specified'
|
|
});
|
|
}
|
|
|
|
const submission = await CaseSubmission.findById(id);
|
|
|
|
if (!submission) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Case submission not found'
|
|
});
|
|
}
|
|
|
|
const success = await CaseSubmission.requestInfo(id, req.user._id, requested_info);
|
|
|
|
if (!success) {
|
|
return res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to update submission'
|
|
});
|
|
}
|
|
|
|
logger.info(`More info requested for case ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Information request recorded',
|
|
note: 'Remember to contact submitter separately to request additional information'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Request info error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete case submission (admin)
|
|
* DELETE /api/cases/submissions/:id
|
|
*/
|
|
async function deleteSubmission(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const success = await CaseSubmission.delete(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Case submission not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Case submission deleted: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Case submission deleted successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Delete submission error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
submitCase,
|
|
getStats,
|
|
listSubmissions,
|
|
listHighRelevance,
|
|
getSubmission,
|
|
approveSubmission,
|
|
rejectSubmission,
|
|
requestMoreInfo,
|
|
deleteSubmission
|
|
};
|