tractatus/src/controllers/cases.controller.js
TheFlow ac2db33732 fix(submissions): restructure Economist package and fix article display
- 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>
2025-10-24 08:47:42 +13:00

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
};