const database = require('../config/database'); const { v4: uuidv4 } = require('uuid'); const FeatureRule = require('./feature_rule'); const FeatureBusinessRules = require('./feature_business_rules'); class Feature { constructor(data = {}) { this.id = data.id; this.template_id = data.template_id; this.feature_id = data.feature_id; this.name = data.name; this.description = data.description; this.feature_type = data.feature_type; this.complexity = data.complexity; this.display_order = data.display_order; this.usage_count = data.usage_count; this.user_rating = data.user_rating; this.is_default = data.is_default; this.created_by_user = data.created_by_user; this.created_at = data.created_at; this.updated_at = data.updated_at; } // Update feature fields static async update(id, updateData) { const fields = [] const values = [] let idx = 1 const allowed = [ 'name', 'description', 'feature_type', 'complexity', 'display_order', 'is_default' ] for (const key of allowed) { if (updateData[key] !== undefined) { fields.push(`${key} = $${idx++}`) values.push(updateData[key]) } } if (fields.length === 0) { return await Feature.getById(id) } const query = ` UPDATE template_features SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${idx} RETURNING * ` values.push(id) const result = await database.query(query, values) return result.rows.length > 0 ? new Feature(result.rows[0]) : null } // Delete a feature static async delete(id) { const result = await database.query('DELETE FROM template_features WHERE id = $1', [id]) return result.rowCount > 0 } // Get all features for a template static async getByTemplateId(templateId) { const query = ` SELECT * FROM template_features WHERE template_id = $1 ORDER BY CASE feature_type WHEN 'essential' THEN 1 WHEN 'suggested' THEN 2 WHEN 'custom' THEN 3 END, display_order, usage_count DESC, name `; const result = await database.query(query, [templateId]); return result.rows.map(row => new Feature(row)); } // Get popular features across all templates static async getPopularFeatures(limit = 10) { const query = ` SELECT tf.*, t.title as template_title, t.type as template_type FROM template_features tf JOIN templates t ON tf.template_id = t.id WHERE tf.usage_count > 0 ORDER BY tf.usage_count DESC, tf.user_rating DESC LIMIT $1 `; const result = await database.query(query, [limit]); return result.rows.map(row => new Feature(row)); } // Create new feature static async create(featureData) { const id = uuidv4(); // Use the generated id as feature_id if not provided const featureId = featureData.id || id const query = ` INSERT INTO template_features ( id, template_id, feature_id, name, description, feature_type, complexity, display_order, is_default, created_by_user ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * `; const values = [ id, featureData.template_id, featureId, featureData.name, featureData.description, featureData.feature_type, featureData.complexity, featureData.display_order || 0, featureData.is_default || false, featureData.created_by_user || false ]; const result = await database.query(query, values); const created = new Feature(result.rows[0]); // Persist rules (aggregated JSONB) if provided try { let rawRules = []; if (Array.isArray(featureData.logic_rules) && featureData.logic_rules.length > 0) { rawRules = featureData.logic_rules; } else if (Array.isArray(featureData.business_rules) && featureData.business_rules.length > 0) { rawRules = featureData.business_rules; } console.log('🔍 Feature.create - Raw rules data:', { logic_rules: featureData.logic_rules, business_rules: featureData.business_rules, rawRules, template_id: created.template_id, feature_id: created.id, generated_id: created.id }); if (rawRules.length > 0) { // Use the generated id (primary key) as feature_id for business rules await FeatureBusinessRules.upsert(created.template_id, created.id, rawRules); console.log('✅ Feature.create - Business rules stored successfully with id as feature_id:', created.id); } else { console.log('⚠️ Feature.create - No business rules to store'); } } catch (ruleErr) { // Do not block feature creation if rules fail; log and continue console.error('⚠️ Failed to persist aggregated business rules:', ruleErr.message); } return created; } // Increment usage count async incrementUsage(userSession = null, projectId = null) { const client = await database.getClient(); try { await client.query('BEGIN'); // Update usage count const updateQuery = ` UPDATE template_features SET usage_count = usage_count + 1 WHERE id = $1 RETURNING * `; const updateResult = await client.query(updateQuery, [this.id]); // Track usage const trackQuery = ` INSERT INTO feature_usage (template_id, feature_id, user_session, project_id) VALUES ($1, $2, $3, $4) `; await client.query(trackQuery, [this.template_id, this.id, userSession, projectId]); await client.query('COMMIT'); if (updateResult.rows.length > 0) { Object.assign(this, updateResult.rows[0]); } return this; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } // Update rating async updateRating(newRating) { const query = ` UPDATE template_features SET user_rating = $2 WHERE id = $1 RETURNING * `; const result = await database.query(query, [this.id, newRating]); if (result.rows.length > 0) { Object.assign(this, result.rows[0]); } return this; } // Get feature by ID static async getById(id) { const query = ` SELECT tf.*, t.title as template_title, t.type as template_type FROM template_features tf JOIN templates t ON tf.template_id = t.id WHERE tf.id = $1 `; const result = await database.query(query, [id]); return result.rows.length > 0 ? new Feature(result.rows[0]) : null; } // Get features by type static async getByType(featureType, limit = 20) { const query = ` SELECT tf.*, t.title as template_title, t.type as template_type FROM template_features tf JOIN templates t ON tf.template_id = t.id WHERE tf.feature_type = $1 ORDER BY tf.usage_count DESC, tf.user_rating DESC LIMIT $2 `; const result = await database.query(query, [featureType, limit]); return result.rows.map(row => new Feature(row)); } // Get a template_features row by (template_id, feature_id) static async getByFeatureId(templateId, featureId) { const query = ` SELECT * FROM template_features WHERE template_id = $1 AND feature_id = $2 LIMIT 1 ` const result = await database.query(query, [templateId, featureId]) return result.rows.length > 0 ? new Feature(result.rows[0]) : null } // Get feature statistics static async getStats() { const query = ` SELECT feature_type, COUNT(*) as count, AVG(usage_count) as avg_usage, AVG(user_rating) as avg_rating FROM template_features GROUP BY feature_type ORDER BY count DESC `; const result = await database.query(query); return result.rows; } // Search features static async search(searchTerm, templateId = null) { let query = ` SELECT tf.*, t.title as template_title, t.type as template_type FROM template_features tf JOIN templates t ON tf.template_id = t.id WHERE (tf.name ILIKE $1 OR tf.description ILIKE $1) `; const params = [`%${searchTerm}%`]; if (templateId) { query += ` AND tf.template_id = $2`; params.push(templateId); } query += ` ORDER BY tf.usage_count DESC, tf.user_rating DESC`; const result = await database.query(query, params); return result.rows.map(row => new Feature(row)); } // Count features for a template static async countByTemplateId(templateId) { const query = `SELECT COUNT(*) as count FROM template_features WHERE template_id = $1`; const result = await database.query(query, [templateId]); return parseInt(result.rows[0].count) || 0; } // Count features for multiple templates at once static async countByTemplateIds(templateIds) { if (!templateIds || templateIds.length === 0) return {}; const placeholders = templateIds.map((_, i) => `$${i + 1}`).join(','); const query = ` SELECT template_id, COUNT(*) as count FROM template_features WHERE template_id IN (${placeholders}) GROUP BY template_id `; const result = await database.query(query, templateIds); const counts = {}; result.rows.forEach(row => { counts[row.template_id] = parseInt(row.count) || 0; }); return counts; } } module.exports = Feature;