/** * ResponseTemplate Model * Pre-written response templates for common inquiries * * Framework enforcement: All templates scanned for governance violations * (inst_016, inst_017, inst_018, inst_079) */ const { ObjectId } = require('mongodb'); const { getCollection } = require('../utils/db.util'); const ContentGovernanceChecker = require('../services/ContentGovernanceChecker.service'); class ResponseTemplate { /** * Create a new response template * Includes framework governance check (inst_016, inst_017, inst_018, inst_079) */ static async create(data) { const collection = await getCollection('response_templates'); // Framework governance check on template content const fullText = [data.subject, data.content].filter(Boolean).join('\n'); const governanceCheck = await ContentGovernanceChecker.scanContent(fullText, { type: 'response_template', context: { name: data.name, category: data.category, created_by: data.created_by } }); const template = { // Template identification name: data.name, description: data.description || null, // Template content subject: data.subject || null, // For email templates content: data.content, // Template text (supports {placeholders}) // Categorization category: data.category || 'general', // general, media, technical, partnership, case, rejection language: data.language || 'en', // Usage metadata projects: data.projects || ['tractatus'], // Which projects can use this template tags: data.tags || [], // Template variables (placeholders) variables: data.variables || [], // [{name: 'contact_name', description: 'Name of the contact', required: true}] // Who can use this template visibility: data.visibility || 'public', // public, private, team created_by: data.created_by ? new ObjectId(data.created_by) : null, // Framework governance check results governance_check: { passed: governanceCheck.success, scanned_at: governanceCheck.scannedAt, violations: governanceCheck.violations || [], summary: governanceCheck.summary }, // Usage statistics usage_stats: { times_used: 0, last_used: null }, // Status active: data.active !== undefined ? data.active : true, created_at: new Date(), updated_at: new Date() }; const result = await collection.insertOne(template); return { ...template, _id: result.insertedId, governance_check: governanceCheck // Return full check results }; } /** * Find template by ID */ static async findById(id) { const collection = await getCollection('response_templates'); return await collection.findOne({ _id: new ObjectId(id) }); } /** * List templates with filtering */ static async list(filters = {}, options = {}) { const collection = await getCollection('response_templates'); const { limit = 50, skip = 0 } = options; const query = { active: true }; if (filters.category) query.category = filters.category; if (filters.language) query.language = filters.language; if (filters.project) query.projects = filters.project; if (filters.tag) query.tags = filters.tag; if (filters.visibility) query.visibility = filters.visibility; return await collection .find(query) .sort({ name: 1 }) .skip(skip) .limit(limit) .toArray(); } /** * Search templates by name or content */ static async search(searchTerm, options = {}) { const collection = await getCollection('response_templates'); const { limit = 20 } = options; return await collection .find({ active: true, $or: [ { name: { $regex: searchTerm, $options: 'i' } }, { description: { $regex: searchTerm, $options: 'i' } }, { content: { $regex: searchTerm, $options: 'i' } }, { tags: { $regex: searchTerm, $options: 'i' } } ] }) .limit(limit) .toArray(); } /** * Render template with variables */ static async render(id, variables = {}) { const template = await this.findById(id); if (!template) { throw new Error('Template not found'); } const rendered = { subject: template.subject, content: template.content }; // Replace placeholders with actual values Object.keys(variables).forEach(key => { const placeholder = new RegExp(`\\{${key}\\}`, 'g'); if (rendered.subject) { rendered.subject = rendered.subject.replace(placeholder, variables[key]); } rendered.content = rendered.content.replace(placeholder, variables[key]); }); // Track usage await this.incrementUsage(id); return rendered; } /** * Increment usage counter */ static async incrementUsage(id) { const collection = await getCollection('response_templates'); await collection.updateOne( { _id: new ObjectId(id) }, { $inc: { 'usage_stats.times_used': 1 }, $set: { 'usage_stats.last_used': new Date(), updated_at: new Date() } } ); } /** * Update template */ static async update(id, data) { const collection = await getCollection('response_templates'); const updateData = { ...data, updated_at: new Date() }; await collection.updateOne( { _id: new ObjectId(id) }, { $set: updateData } ); return await this.findById(id); } /** * Deactivate template (soft delete) */ static async deactivate(id) { return await this.update(id, { active: false }); } /** * Delete template */ static async delete(id) { const collection = await getCollection('response_templates'); return await collection.deleteOne({ _id: new ObjectId(id) }); } /** * Get popular templates */ static async getPopular(limit = 10) { const collection = await getCollection('response_templates'); return await collection .find({ active: true }) .sort({ 'usage_stats.times_used': -1 }) .limit(limit) .toArray(); } /** * Get statistics */ static async getStats() { const collection = await getCollection('response_templates'); const [total, active, byCategory, byLanguage, topUsed] = await Promise.all([ collection.countDocuments(), collection.countDocuments({ active: true }), collection.aggregate([ { $match: { active: true } }, { $group: { _id: '$category', count: { $sum: 1 } } } ]).toArray(), collection.aggregate([ { $match: { active: true } }, { $group: { _id: '$language', count: { $sum: 1 } } } ]).toArray(), collection.find({ active: true }) .sort({ 'usage_stats.times_used': -1 }) .limit(5) .toArray() ]); return { total, active, by_category: byCategory.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}), by_language: byLanguage.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}), top_used: topUsed.map(t => ({ name: t.name, times_used: t.usage_stats.times_used })) }; } } module.exports = ResponseTemplate;