/** * Feedback Controller * Handle feedback submissions with Tractatus governance */ const Feedback = require('../models/Feedback.model'); const { BoundaryEnforcer, PluralisticDeliberator, CrossReferenceValidator } = require('../services/feedback-governance.service'); const logger = require('../utils/logger.util'); const { getClientIp } = require('../utils/security-logger'); // Initialize governance components const boundaryEnforcer = new BoundaryEnforcer(); const deliberator = new PluralisticDeliberator(); const validator = new CrossReferenceValidator(); /** * POST /api/feedback/submit * Submit feedback with automatic governance classification */ async function submit(req, res) { try { const { type, content, name, email } = req.body; // Step 1: Classify feedback and determine governance pathway const classification = boundaryEnforcer.classify(type, content); // Step 2: Validate classification const classificationValidation = boundaryEnforcer.validate(classification); if (!classificationValidation.valid) { logger.error('[Feedback] Invalid classification:', classificationValidation.error); return res.status(500).json({ success: false, error: 'Classification error', message: 'Unable to classify feedback. Please try again.' }); } // Step 3: Create feedback record with governance data const feedback = await Feedback.create({ type, pathway: classification.pathway, content, submittedBy: { name, email }, metadata: { user_agent: req.get('user-agent'), ip: getClientIp(req), source_page: req.get('referer'), referrer: req.get('referer') }, governance: { boundaryCheck: 'completed', deliberationRequired: classification.pathway === 'deliberation', stakeholders: classification.stakeholders || [], constraints: classification.constraints, classificationReason: classification.reason } }); // Step 4: If deliberation required, initiate deliberation process if (classification.pathway === 'deliberation') { const deliberation = await deliberator.initiate( feedback.feedbackId, classification.stakeholders, content ); await Feedback.update(feedback.feedbackId, { 'governance.deliberationId': deliberation.deliberationId, 'governance.deliberationStatus': 'awaiting_votes', 'governance.deliberationDeadline': deliberation.deadline }); logger.info(`[Feedback] Deliberation initiated: ${deliberation.deliberationId}`); } logger.info(`[Feedback] New submission: ${feedback.feedbackId} | Pathway: ${classification.pathway}`); // Step 5: Return appropriate response based on pathway const messages = { autonomous: 'Thank you! An AI assistant will respond shortly with relevant information.', deliberation: 'Thank you! Your feedback requires multi-stakeholder review. You will receive a response within 72 hours.', human_mandatory: 'Thank you! Your inquiry requires personal attention. You will receive a response within 48 hours.' }; res.status(201).json({ success: true, message: messages[classification.pathway], feedbackId: feedback.feedbackId, pathway: classification.pathway, trackingUrl: `/api/feedback/status/${feedback.feedbackId}` }); } catch (error) { logger.error('[Feedback] Submit error:', error); res.status(500).json({ success: false, error: 'Failed to submit feedback', message: 'An error occurred. Please try again.' }); } } /** * GET /api/feedback/status/:feedbackId * Check status of feedback submission (public) */ async function getStatus(req, res) { try { const { feedbackId } = req.params; const feedback = await Feedback.findById(feedbackId); if (!feedback) { return res.status(404).json({ success: false, error: 'Feedback not found' }); } // Return limited public information res.json({ success: true, feedbackId: feedback.feedbackId, status: feedback.status, pathway: feedback.pathway, submittedAt: feedback.created_at, hasResponse: !!feedback.response.content, responsePreview: feedback.response.content ? `${feedback.response.content.substring(0, 200)}...` : null }); } catch (error) { logger.error('[Feedback] Get status error:', error); res.status(500).json({ success: false, error: 'Failed to fetch status' }); } } /** * GET /api/feedback/admin/stats * Get feedback statistics (admin) */ async function getStats(req, res) { try { const stats = await Feedback.getStatistics(); res.json({ success: true, stats }); } catch (error) { logger.error('[Feedback] Stats error:', error); res.status(500).json({ success: false, error: 'Failed to fetch statistics' }); } } /** * GET /api/feedback/admin/queue * Get feedback queue filtered by pathway */ async function getQueue(req, res) { try { const { pathway, status = 'pending', limit = 20, skip = 0 } = req.query; if (!pathway) { return res.status(400).json({ success: false, error: 'Pathway parameter required (autonomous, deliberation, human_mandatory)' }); } const feedback = await Feedback.findByPathway(pathway, { status, limit: parseInt(limit), skip: parseInt(skip) }); res.json({ success: true, pathway, queue: feedback, count: feedback.length }); } catch (error) { logger.error('[Feedback] Queue error:', error); res.status(500).json({ success: false, error: 'Failed to fetch queue' }); } } /** * GET /api/feedback/admin/list * List all feedback with filtering */ async function list(req, res) { try { const { status, pathway, limit = 20, skip = 0 } = req.query; let feedback; if (pathway) { feedback = await Feedback.findByPathway(pathway, { status, limit: parseInt(limit), skip: parseInt(skip) }); } else if (status) { feedback = await Feedback.findByStatus(status, { limit: parseInt(limit), skip: parseInt(skip) }); } else { feedback = await Feedback.getRecent(parseInt(limit)); } res.json({ success: true, feedback, count: feedback.length }); } catch (error) { logger.error('[Feedback] List error:', error); res.status(500).json({ success: false, error: 'Failed to fetch feedback' }); } } /** * GET /api/feedback/admin/:id * Get single feedback by ID */ async function getById(req, res) { try { const { id } = req.params; const feedback = await Feedback.findById(id); if (!feedback) { return res.status(404).json({ success: false, error: 'Feedback not found' }); } res.json({ success: true, feedback }); } catch (error) { logger.error('[Feedback] Get by ID error:', error); res.status(500).json({ success: false, error: 'Failed to fetch feedback' }); } } /** * POST /api/feedback/admin/:id/response * Add response to feedback (human or AI) */ async function addResponse(req, res) { try { const { id } = req.params; const { content, respondedBy } = req.body; const feedback = await Feedback.findById(id); if (!feedback) { return res.status(404).json({ success: false, error: 'Feedback not found' }); } // If AI response, validate against constraints let validationResult = null; if (respondedBy === 'ai') { validationResult = validator.validate(content, feedback.governance.constraints); if (!validationResult.valid) { const report = validator.getReport(validationResult); if (report.critical > 0) { logger.warn('[Feedback] AI response blocked - critical violations:', validationResult.violations); return res.status(400).json({ success: false, error: 'AI response violates governance constraints', violations: validationResult.violations, recommendation: report.recommendation }); } // Warnings allowed but logged logger.warn('[Feedback] AI response has warnings:', validationResult.violations); } } // Add response to feedback await Feedback.addResponse(id, { content, respondedBy, // 'ai', 'human', or 'deliberated' validatedBy: respondedBy === 'ai' ? 'CrossReferenceValidator' : null, validationStatus: validationResult ? (validationResult.valid ? 'pass' : 'warning') : null }); logger.info(`[Feedback] Response added to ${feedback.feedbackId} by ${respondedBy}`); res.json({ success: true, message: 'Response added successfully', validation: validationResult ? validator.getReport(validationResult) : null }); } catch (error) { logger.error('[Feedback] Add response error:', error); res.status(500).json({ success: false, error: 'Failed to add response' }); } } /** * POST /api/feedback/admin/:id/deliberate * Initiate deliberation process for feedback */ async function initiateDeliberation(req, res) { try { const { id } = req.params; const { stakeholders } = req.body; const feedback = await Feedback.findById(id); if (!feedback) { return res.status(404).json({ success: false, error: 'Feedback not found' }); } if (feedback.pathway !== 'deliberation') { return res.status(400).json({ success: false, error: 'Feedback does not require deliberation', currentPathway: feedback.pathway }); } const deliberation = await deliberator.initiate( feedback.feedbackId, stakeholders || feedback.governance.stakeholders, feedback.content ); await Feedback.update(id, { 'governance.deliberationId': deliberation.deliberationId, 'governance.deliberationStatus': 'awaiting_votes', 'governance.deliberationDeadline': deliberation.deadline, status: 'in_progress' }); logger.info(`[Feedback] Deliberation initiated: ${deliberation.deliberationId}`); res.json({ success: true, message: 'Deliberation initiated', deliberation }); } catch (error) { logger.error('[Feedback] Initiate deliberation error:', error); res.status(500).json({ success: false, error: 'Failed to initiate deliberation' }); } } /** * POST /api/feedback/admin/deliberation/:deliberationId/vote * Submit vote for deliberation */ async function submitVote(req, res) { try { const { deliberationId } = req.params; const { vote, constraints } = req.body; // In production, this would: // 1. Record vote in database // 2. Check if all stakeholders voted // 3. If complete, process deliberation result // 4. Update feedback with deliberation outcome // For now, simulated vote processing const votes = [ { stakeholder: req.user.id, vote, constraints } // In production, fetch all votes from database ]; const result = await deliberator.process(deliberationId, votes); logger.info(`[Feedback] Vote submitted for ${deliberationId}: ${vote}`); res.json({ success: true, message: 'Vote recorded', deliberationResult: result }); } catch (error) { logger.error('[Feedback] Submit vote error:', error); res.status(500).json({ success: false, error: 'Failed to submit vote' }); } } /** * POST /api/feedback/ai/generate-response * AI generates response and validates against constraints */ async function validateAIResponse(req, res) { try { const { feedbackId, aiResponse } = req.body; const feedback = await Feedback.findById(feedbackId); if (!feedback) { return res.status(404).json({ success: false, error: 'Feedback not found' }); } // Validate AI response against constraints const validationResult = validator.validate(aiResponse, feedback.governance.constraints); const report = validator.getReport(validationResult); res.json({ success: true, validation: validationResult, report, approved: report.critical === 0 }); } catch (error) { logger.error('[Feedback] Validate AI response error:', error); res.status(500).json({ success: false, error: 'Failed to validate AI response' }); } } /** * PUT /api/feedback/admin/:id * Update feedback */ async function update(req, res) { try { const { id } = req.params; const { status, priority } = req.body; const updateData = {}; if (status) updateData.status = status; if (priority) updateData.priority = priority; await Feedback.update(id, updateData); logger.info(`[Feedback] Updated ${id}:`, updateData); res.json({ success: true, message: 'Feedback updated successfully' }); } catch (error) { logger.error('[Feedback] Update error:', error); res.status(500).json({ success: false, error: 'Failed to update feedback' }); } } /** * DELETE /api/feedback/admin/:id * Delete feedback */ async function deleteFeedback(req, res) { try { const { id } = req.params; const feedback = await Feedback.findById(id); if (!feedback) { return res.status(404).json({ success: false, error: 'Feedback not found' }); } // In production, implement soft delete or archive // For now, log the deletion request logger.warn(`[Feedback] Delete requested for ${feedback.feedbackId}`); res.json({ success: true, message: 'Feedback deletion logged (implement soft delete in production)' }); } catch (error) { logger.error('[Feedback] Delete error:', error); res.status(500).json({ success: false, error: 'Failed to delete feedback' }); } } module.exports = { submit, getStatus, getStats, getQueue, list, getById, addResponse, initiateDeliberation, submitVote, validateAIResponse, update, deleteFeedback };