const mysql = require('mysql2/promise'); const fs = require('fs'); const path = require('path'); const NODE_ENV = process.env.NODE_ENV || 'development'; function getSSLConfig() { const useSSL = String(process.env.DB_SSL || '').toLowerCase() === 'true'; const caPath = process.env.DB_SSL_CA_PATH; if (!useSSL) return undefined; try { if (caPath) { const resolved = path.resolve(caPath); if (fs.existsSync(resolved)) { return { ca: fs.readFileSync(resolved), rejectUnauthorized: false }; } } } catch (_) {} return { rejectUnauthorized: false }; } let dbConfig; if (NODE_ENV === 'development') { dbConfig = { host: process.env.DEV_DB_HOST || 'localhost', port: Number(process.env.DEV_DB_PORT) || 3306, user: process.env.DEV_DB_USER || 'root', password: process.env.DEV_DB_PASSWORD || '', database: process.env.DEV_DB_NAME || 'profitplanet_centralserver', ssl: undefined }; } else { dbConfig = { host: process.env.DB_HOST, port: Number(process.env.DB_PORT) || 3306, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: getSSLConfig() }; } const pool = mysql.createPool({ ...dbConfig, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); async function ensureUserExistsByEmail(conn, email) { const [rows] = await conn.query('SELECT id, email FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1', [email]); if (!rows.length) { const err = new Error('Top node user not found'); err.status = 404; throw err; } return rows[0]; } async function createMatrix({ name, topNodeEmail }) { const conn = await pool.getConnection(); try { await conn.beginTransaction(); // Ensure columns for multi-instance (idempotent) try { await conn.query(`ALTER TABLE user_tree_edges ADD COLUMN matrix_instance_id INT NULL`); } catch (_) {} try { await conn.query(`ALTER TABLE user_tree_edges ADD COLUMN rogue_user BOOLEAN DEFAULT FALSE`); } catch (_) {} try { await conn.query(`ALTER TABLE user_tree_closure ADD COLUMN matrix_instance_id INT NOT NULL`); } catch (_) {} try { await conn.query(`ALTER TABLE user_matrix_metadata ADD COLUMN matrix_instance_id INT NOT NULL`); } catch (_) {} const topUser = await ensureUserExistsByEmail(conn, topNodeEmail); // Create new matrix instance const [instRes] = await conn.query( `INSERT INTO matrix_instances (root_user_id, name, is_active, max_depth, ego_activated_at, immediate_children_count, first_free_position) VALUES (?, ?, TRUE, NULL, NOW(), 0, NULL)`, // CHANGED: first_free_position NULL at root [topUser.id, name] ); const matrixInstanceId = instRes.insertId; // Insert metadata row (primary keyed by matrix_instance_id) await conn.query( `INSERT INTO user_matrix_metadata (matrix_instance_id, root_user_id, ego_activated_at, last_bfs_fill_at, immediate_children_count, first_free_position, name, is_active, max_depth) VALUES (?, ?, NOW(), NULL, 0, 1, ?, TRUE, NULL)`, [matrixInstanceId, topUser.id, name] ); // Self closure scoped to instance await conn.query( `INSERT IGNORE INTO user_tree_closure (matrix_instance_id, ancestor_user_id, descendant_user_id, depth) VALUES (?, ?, ?, 0)`, [matrixInstanceId, topUser.id, topUser.id] ); await conn.commit(); return { name, matrixInstanceId, masterTopUserId: topUser.id, masterTopUserEmail: topUser.email, rootUserId: topUser.id }; } catch (e) { await conn.rollback(); throw e; } finally { conn.release(); } } async function ensureUserExists(conn, userId) { const [rows] = await conn.query('SELECT id FROM users WHERE id = ?', [userId]); if (!rows.length) { const err = new Error('User not found'); err.status = 404; throw err; } } async function activateEgoMatrix(rootUserId) { const conn = await pool.getConnection(); try { await conn.beginTransaction(); await ensureUserExists(conn, rootUserId); await conn.query( 'INSERT IGNORE INTO user_tree_closure (ancestor_user_id, descendant_user_id, depth) VALUES (?, ?, 0)', [rootUserId, rootUserId] ); const [childRows] = await conn.query( 'SELECT position FROM user_tree_edges WHERE parent_user_id = ? ORDER BY position ASC', [rootUserId] ); const positions = new Set(childRows.map(r => r.position)); let firstFreePosition = null; for (let i = 1; i <= 5; i++) { if (!positions.has(i)) { firstFreePosition = i; break; } } const immediateChildrenCount = childRows.length; await conn.query( ` INSERT INTO user_matrix_metadata (root_user_id, ego_activated_at, last_bfs_fill_at, immediate_children_count, first_free_position, max_depth) VALUES (?, NOW(), NULL, ?, ?, 5) ON DUPLICATE KEY UPDATE ego_activated_at = COALESCE(ego_activated_at, VALUES(ego_activated_at)), immediate_children_count = VALUES(immediate_children_count), first_free_position = VALUES(first_free_position), max_depth = 5, updated_at = CURRENT_TIMESTAMP `, [rootUserId, immediateChildrenCount, firstFreePosition] ); const [metaRows] = await conn.query( 'SELECT ego_activated_at, last_bfs_fill_at, immediate_children_count, first_free_position FROM user_matrix_metadata WHERE root_user_id = ?', [rootUserId] ); const meta = metaRows[0] || { ego_activated_at: null, last_bfs_fill_at: null, immediate_children_count: immediateChildrenCount, first_free_position: firstFreePosition }; await conn.commit(); return { rootUserId, egoActivatedAt: meta.ego_activated_at, lastBfsFillAt: meta.last_bfs_fill_at, immediateChildrenCount: meta.immediate_children_count, firstFreePosition: meta.first_free_position }; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } } async function getMatrixStats() { const conn = await pool.getConnection(); try { const [matRows] = await conn.query(` SELECT mi.id AS matrixInstanceId, mi.root_user_id, mi.name, mi.is_active, mi.ego_activated_at, mi.max_depth, u.email FROM matrix_instances mi JOIN users u ON u.id = mi.root_user_id ORDER BY mi.id ASC `); if (!matRows.length) return { activeMatrices: 0, totalMatrices: 0, totalUsersSubscribed: 0, matrices: [] }; const instanceIds = matRows.map(r => r.matrixInstanceId); // Count users per instance honoring max_depth const [cntRows] = await conn.query(` SELECT c.matrix_instance_id AS mid, COUNT(*) AS cnt FROM user_tree_closure c JOIN matrix_instances mi ON mi.id = c.matrix_instance_id WHERE c.matrix_instance_id IN (?) AND c.depth BETWEEN 0 AND COALESCE(mi.max_depth, 2147483647) GROUP BY c.matrix_instance_id `, [instanceIds]); const countsMap = new Map(cntRows.map(r => [Number(r.mid), Number(r.cnt)])); // Rogue counts const [rogueRows] = await conn.query(` SELECT matrix_instance_id AS mid, COUNT(*) AS rogueCnt FROM user_tree_edges WHERE matrix_instance_id IN (?) AND rogue_user = TRUE `, [instanceIds]); const rogueMap = new Map(rogueRows.map(r => [Number(r.mid), Number(r.rogueCnt)])); // Distinct subscribed users across active matrices const [distinctRows] = await conn.query(` SELECT COUNT(DISTINCT c.descendant_user_id) AS total FROM user_tree_closure c JOIN matrix_instances mi ON mi.id = c.matrix_instance_id WHERE mi.is_active = TRUE AND c.matrix_instance_id IN (?) AND c.depth BETWEEN 0 AND COALESCE(mi.max_depth, 2147483647) `, [instanceIds]); const totalDistinct = Number(distinctRows[0]?.total || 0); const activeMatrices = matRows.filter(r => !!r.is_active).length; const matrices = matRows.map(r => ({ matrixInstanceId: Number(r.matrixInstanceId), rootUserId: Number(r.root_user_id), name: r.name || null, isActive: !!r.is_active, usersCount: countsMap.get(Number(r.matrixInstanceId)) || 0, rogueUsersCount: rogueMap.get(Number(r.matrixInstanceId)) || 0, createdAt: r.ego_activated_at || null, topNodeEmail: r.email })); return { activeMatrices, totalMatrices: matRows.length, totalUsersSubscribed: totalDistinct, matrices }; } finally { conn.release(); } } async function resolveRootUserId({ rootUserId, matrixId, matrixInstanceId, topNodeEmail }) { const conn = await pool.getConnection(); try { // Prefer explicit matrixInstanceId (matrixId backward compatible) const midRaw = matrixInstanceId || matrixId; const mid = Number.parseInt(midRaw, 10); if (Number.isFinite(mid) && mid > 0) { const [rows] = await conn.query( `SELECT id, root_user_id FROM matrix_instances WHERE id = ? LIMIT 1`, [mid] ); if (!rows.length) { const err = new Error('Matrix instance not found'); err.status = 404; throw err; } return { rootUserId: Number(rows[0].root_user_id), matrixInstanceId: Number(rows[0].id) }; } // Direct root user id (find first instance with that root) const rid = Number.parseInt(rootUserId, 10); if (Number.isFinite(rid) && rid > 0) { const [rows] = await conn.query( `SELECT id FROM matrix_instances WHERE root_user_id = ? ORDER BY id ASC LIMIT 1`, [rid] ); if (rows.length) { return { rootUserId: rid, matrixInstanceId: Number(rows[0].id) }; } } // Resolve by email const email = (topNodeEmail || '').trim(); if (email) { const [uRows] = await conn.query( `SELECT id FROM users WHERE LOWER(email)=LOWER(?) LIMIT 1`, [email] ); if (!uRows.length) { const err = new Error('Top node user not found'); err.status = 404; throw err; } const uid = Number(uRows[0].id); const [instRows] = await conn.query( `SELECT id FROM matrix_instances WHERE root_user_id = ? ORDER BY id ASC LIMIT 1`, [uid] ); if (instRows.length) { return { rootUserId: uid, matrixInstanceId: Number(instRows[0].id) }; } } // Fallback: pick first existing instance const [firstInst] = await conn.query(`SELECT id, root_user_id FROM matrix_instances ORDER BY id ASC LIMIT 1`); if (firstInst.length) { return { rootUserId: Number(firstInst[0].root_user_id), matrixInstanceId: Number(firstInst[0].id) }; } const err = new Error('Matrix resolver parameters required'); err.status = 400; throw err; } finally { conn.release(); } } // NEW: search user candidates to add to a matrix async function getUserSearchCandidates({ q, type = 'all', rootUserId, matrixInstanceId, limit = 20, offset = 0 }) { const conn = await pool.getConnection(); try { const whereParts = []; const params = []; // Exclude users already in selected matrix instance whereParts.push(`u.id NOT IN ( SELECT descendant_user_id FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? )`); params.push(Number(matrixInstanceId), Number(rootUserId)); whereParts.push(`u.role = 'user'`); if (type === 'personal' || type === 'company') { whereParts.push(`u.user_type = ?`); params.push(type); } const like = `%${q.toLowerCase()}%`; whereParts.push(`( LOWER(u.email) LIKE ? OR LOWER(CONCAT(COALESCE(p.first_name,''),' ',COALESCE(p.last_name,''))) LIKE ? OR LOWER(COALESCE(c.company_name,'')) LIKE ? )`); params.push(like, like, like); const whereSql = `WHERE ${whereParts.join(' AND ')}`; const [countRows] = await conn.query(` SELECT COUNT(*) AS total FROM users u LEFT JOIN personal_profiles p ON p.user_id = u.id LEFT JOIN company_profiles c ON c.user_id = u.id ${whereSql} `, params); const total = Number(countRows[0]?.total || 0); const [rows] = await conn.query(` SELECT u.id AS userId, u.email, u.user_type AS userType, CASE WHEN u.user_type='personal' THEN TRIM(CONCAT(COALESCE(p.first_name,''),' ',COALESCE(p.last_name,''))) WHEN u.user_type='company' THEN COALESCE(c.company_name,'') ELSE '' END AS name FROM users u LEFT JOIN personal_profiles p ON p.user_id = u.id LEFT JOIN company_profiles c ON c.user_id = u.id ${whereSql} ORDER BY u.created_at DESC, u.email ASC LIMIT ? OFFSET ? `, params.concat([Number(limit), Number(offset)])); return { total, items: rows.map(r => ({ userId: Number(r.userId), email: r.email, userType: r.userType, name: r.name || '' })) }; } finally { conn.release(); } } // NEW: fetch matrix users (descendants) under a root async function getMatrixUsers({ rootUserId, matrixInstanceId, maxDepth = 5, limit = 100, offset = 0, includeRoot = false, rogueOnly = false }) { const conn = await pool.getConnection(); try { let policyDepth = null; try { const [pRows] = await conn.query( `SELECT max_depth FROM matrix_instances WHERE id = ? LIMIT 1`, [matrixInstanceId] ); if (pRows.length) policyDepth = pRows[0].max_depth == null ? null : Number(pRows[0].max_depth); } catch (_) {} let requestedDepth = Number(maxDepth); if (!Number.isFinite(requestedDepth) || requestedDepth < 0) requestedDepth = 0; const depthLimit = policyDepth == null ? requestedDepth : Math.min(requestedDepth, policyDepth); const startDepth = includeRoot ? 0 : 1; if (startDepth > depthLimit) return []; const rogueClause = rogueOnly ? 'AND (e.rogue_user = TRUE)' : ''; const [rows] = await conn.query(` SELECT c.descendant_user_id AS userId, c.depth, u.email, u.user_type AS userType, u.role, u.created_at AS createdAt, e.parent_user_id AS parentUserId, e.position, e.rogue_user AS rogueUser, CASE WHEN u.user_type='personal' THEN TRIM(CONCAT(COALESCE(pp.first_name,''),' ',COALESCE(pp.last_name,''))) WHEN u.user_type='company' THEN COALESCE(cp.company_name,'') ELSE '' END AS name FROM user_tree_closure c JOIN users u ON u.id = c.descendant_user_id LEFT JOIN user_tree_edges e ON e.child_user_id = c.descendant_user_id AND e.matrix_instance_id = c.matrix_instance_id AND e.parent_user_id != e.child_user_id LEFT JOIN personal_profiles pp ON pp.user_id = u.id LEFT JOIN company_profiles cp ON cp.user_id = u.id WHERE c.matrix_instance_id = ? AND c.ancestor_user_id = ? AND c.depth BETWEEN ? AND ? ${rogueClause} ORDER BY c.depth ASC, u.created_at ASC, u.id ASC LIMIT ? OFFSET ? `, [matrixInstanceId, rootUserId, startDepth, depthLimit, Number(limit), Number(offset)]); return rows.map(r => ({ userId: Number(r.userId), email: r.email, userType: r.userType, role: r.role, depth: Number(r.depth), level: Number(r.depth), parentUserId: r.parentUserId ? Number(r.parentUserId) : null, position: r.position != null ? Number(r.position) : null, rogueUser: !!r.rogueUser, name: r.name || r.email, createdAt: r.createdAt })); } finally { conn.release(); } } async function addUserToMatrix({ rootUserId, matrixInstanceId, matrixId, topNodeEmail, childUserId, forceParentFallback = false, parentUserId, actorUserId }) { const conn = await pool.getConnection(); try { await conn.beginTransaction(); // Resolve (matrixId backward compatible) const resolved = await resolveRootUserId({ rootUserId, matrixId: matrixId || matrixInstanceId, matrixInstanceId, topNodeEmail }); const rid = resolved.rootUserId; const mid = resolved.matrixInstanceId; // PREVENT additions to inactive instances const [miRows] = await conn.query( `SELECT is_active FROM matrix_instances WHERE id = ? LIMIT 1`, [mid] ); if (!miRows.length) { const err = new Error('Matrix instance not found'); err.status = 404; throw err; } if (!miRows[0].is_active) { const err = new Error('Matrix instance is inactive'); err.status = 409; throw err; } const cId = Number(childUserId); if (!Number.isFinite(cId) || cId <= 0) { const err = new Error('Invalid childUserId'); err.status = 400; throw err; } const [childRows] = await conn.query(`SELECT id FROM users WHERE id = ? LIMIT 1`, [cId]); if (!childRows.length) { const err = new Error('Child user not found'); err.status = 404; throw err; } let parentId = Number(parentUserId) > 0 ? Number(parentUserId) : null; let explicitRootChoice = false; if (parentId === rid) { explicitRootChoice = true; // admin explicitly chose root } if (!parentId) { // referral parent discovery const [refRows] = await conn.query( ` SELECT t.created_by_user_id AS parent_user_id FROM referral_token_usage u JOIN referral_tokens t ON t.id = u.referral_token_id WHERE u.used_by_user_id = ? ORDER BY u.id ASC LIMIT 1 `, [cId] ); if (refRows.length) parentId = Number(refRows[0].parent_user_id); } let fallback_reason = null; let rogueFlag = false; // Check parent within matrix instance (when parentId exists and is not root) if (parentId && parentId !== rid) { const [inMatrixRows] = await conn.query( ` SELECT depth FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ? LIMIT 1 `, [mid, rid, parentId] ); if (!inMatrixRows.length) { // Referrer not in matrix -> fallback decision if (forceParentFallback) { fallback_reason = 'referrer_not_in_matrix'; parentId = rid; rogueFlag = true; } else { const err = new Error('Referrer is not part of this matrix'); err.status = 400; throw err; } } } if (!parentId) { // No referral parent found fallback_reason = 'referrer_not_in_matrix'; parentId = rid; rogueFlag = true; } // Duplicate check (already in matrix) const [dupRows] = await conn.query( ` SELECT 1 FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ? LIMIT 1 `, [mid, rid, cId] ); if (dupRows.length) { const err = new Error('User already in matrix'); err.status = 409; throw err; } // Determine policy depth let maxDepthPolicy = null; try { const [pRows] = await conn.query(`SELECT max_depth FROM matrix_instances WHERE id = ? LIMIT 1`, [mid]); if (pRows.length) maxDepthPolicy = pRows[0].max_depth == null ? null : Number(pRows[0].max_depth); } catch (_) {} let assignPos = null; if (parentId === rid) { // ROOT: unlimited children, sequential position // Do not mark rogue if admin explicitly chose root if (explicitRootChoice) { rogueFlag = false; fallback_reason = null; } const [rootPosRows] = await conn.query( `SELECT MAX(position) AS maxPos FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ?`, [mid, rid] ); const maxPos = Number(rootPosRows[0]?.maxPos || 0); assignPos = maxPos + 1; // sequential position under root } else { // NON-ROOT: enforce 1–5 const [childPosRows] = await conn.query( `SELECT position FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ? ORDER BY position ASC`, [mid, parentId] ); const used = new Set(childPosRows.map(r => Number(r.position))); for (let i = 1; i <= 5; i++) if (!used.has(i)) { assignPos = i; break; } if (!assignPos) { // BFS within the chosen parent subtree only const [candRows] = await conn.query( ` SELECT d.descendant_user_id AS candidate_id FROM user_tree_closure AS d WHERE d.matrix_instance_id = ? AND d.ancestor_user_id = ? AND d.descendant_user_id != ? ORDER BY d.depth ASC, d.descendant_user_id ASC `, [mid, parentId, parentId] ); for (const row of candRows) { const candidateParentId = Number(row.candidate_id); // skip root in BFS if (candidateParentId === rid) continue; // ensure candidate within max depth policy (parent depth + 1) if (maxDepthPolicy != null) { const [depthRows] = await conn.query( `SELECT depth FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ? LIMIT 1`, [mid, rid, candidateParentId] ); const candDepthFromRoot = Number(depthRows[0]?.depth ?? 0); if (candDepthFromRoot + 1 > maxDepthPolicy) continue; } const [candPosRows] = await conn.query( `SELECT position FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ? ORDER BY position ASC`, [mid, candidateParentId] ); const candUsed = new Set(candPosRows.map(r => Number(r.position))); for (let i = 1; i <= 5; i++) if (!candUsed.has(i)) { assignPos = i; break; } if (assignPos) { parentId = candidateParentId; break; } } if (!assignPos) { // no slot in subtree -> decide fallback if (forceParentFallback) { fallback_reason = 'referrer_full'; parentId = rid; rogueFlag = true; // root sequential const [rootPosRows] = await conn.query( `SELECT MAX(position) AS maxPos FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ?`, [mid, rid] ); const maxPos = Number(rootPosRows[0]?.maxPos || 0); assignPos = maxPos + 1; } else { const err = new Error('Parent subtree is full (5-ary); no available position'); err.status = 409; throw err; } } } } // Insert edge await conn.query( `INSERT INTO user_tree_edges (matrix_instance_id, parent_user_id, child_user_id, position, rogue_user) VALUES (?,?,?,?,?)`, [mid, parentId, cId, assignPos, rogueFlag] ); // Self closure for child await conn.query( `INSERT IGNORE INTO user_tree_closure (matrix_instance_id, ancestor_user_id, descendant_user_id, depth) VALUES (?,?,?,0)`, [mid, cId, cId] ); // Ancestor closure rows const [ancestorRows] = await conn.query( `SELECT ancestor_user_id, depth FROM user_tree_closure WHERE matrix_instance_id = ? AND descendant_user_id = ? ORDER BY depth ASC`, [mid, parentId] ); if (ancestorRows.length) { const values = ancestorRows .map(r => `(${mid}, ${Number(r.ancestor_user_id)}, ${cId}, ${Number(r.depth) + 1})`) .join(','); await conn.query( `INSERT IGNORE INTO user_tree_closure (matrix_instance_id, ancestor_user_id, descendant_user_id, depth) VALUES ${values}` ); } // Update instance metadata: immediate children count for root; do not compute first_free_position for root if (parentId === rid) { const [rootChildrenRows] = await conn.query( `SELECT COUNT(*) AS cnt FROM user_tree_edges WHERE matrix_instance_id = ? AND parent_user_id = ?`, [mid, rid] ); await conn.query( `UPDATE matrix_instances SET immediate_children_count = ? WHERE id = ?`, [Number(rootChildrenRows[0]?.cnt || 0), mid] ); } // Log try { await conn.query( ` INSERT INTO user_action_logs (action, performed_by_user_id, affected_user_id, details, created_at) VALUES ('matrix_add_user', ?, ?, ?, NOW()) `, [ Number(actorUserId) || null, cId, JSON.stringify({ matrixInstanceId: mid, rootUserId: rid, parentUserId: parentId, position: assignPos, rogue_user: rogueFlag, fallback_reason }) ] ); } catch (_) {} await conn.commit(); return { matrixInstanceId: mid, rootUserId: rid, parentUserId: parentId, childUserId: cId, position: assignPos, rogue_user: rogueFlag, fallback_reason }; } catch (e) { try { await conn.rollback(); } catch (_) {} throw e; } finally { conn.release(); } } // NEW: resolve instance containing a given user via closure async function resolveInstanceForUser(userId) { const conn = await pool.getConnection(); try { const [rows] = await conn.query( `SELECT matrix_instance_id AS mid FROM user_tree_closure WHERE descendant_user_id = ? ORDER BY matrix_instance_id ASC LIMIT 1`, [Number(userId)] ); if (!rows.length) return null; return { matrixInstanceId: Number(rows[0].mid) }; } finally { conn.release(); } } // NEW: fetch instance basic info async function getInstanceInfo(instanceId) { const conn = await pool.getConnection(); try { const [rows] = await conn.query( `SELECT id, root_user_id, max_depth FROM matrix_instances WHERE id = ? LIMIT 1`, [Number(instanceId)] ); return rows.length ? rows[0] : null; } finally { conn.release(); } } // NEW: list distinct matrix instances a user belongs to async function listInstancesForUser(userId) { const conn = await pool.getConnection(); try { const [rows] = await conn.query( ` SELECT DISTINCT mi.id AS id, mi.name FROM matrix_instances mi JOIN user_tree_closure c ON c.matrix_instance_id = mi.id WHERE c.descendant_user_id = ? ORDER BY mi.id ASC `, [Number(userId)] ); return rows.map(r => ({ id: Number(r.id), name: r.name || null })); } finally { conn.release(); } } // NEW: check if user belongs to a specific instance async function userBelongsToInstance(userId, instanceId) { const conn = await pool.getConnection(); try { const [rows] = await conn.query( ` SELECT 1 FROM user_tree_closure WHERE matrix_instance_id = ? AND descendant_user_id = ? LIMIT 1 `, [Number(instanceId), Number(userId)] ); return rows.length > 0; } finally { conn.release(); } } // NEW: deactivate a matrix instance by id (idempotent) async function deactivateInstance(instanceId) { const conn = await pool.getConnection(); try { const id = Number(instanceId); if (!Number.isFinite(id) || id <= 0) { const err = new Error('Invalid matrix instance id'); err.status = 400; throw err; } const [rows] = await conn.query( `SELECT id, is_active FROM matrix_instances WHERE id = ? LIMIT 1`, [id] ); if (!rows.length) { const err = new Error('Matrix instance not found'); err.status = 404; throw err; } const wasActive = !!rows[0].is_active; // Try to set deactivated_at if column exists; fallback otherwise try { await conn.query( `UPDATE matrix_instances SET is_active = FALSE, deactivated_at = COALESCE(deactivated_at, NOW()), updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [id] ); } catch (e) { if (e && e.code === 'ER_BAD_FIELD_ERROR') { await conn.query( `UPDATE matrix_instances SET is_active = FALSE, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [id] ); } else { throw e; } } return { matrixInstanceId: id, wasActive, isActive: false, status: wasActive ? 'deactivated' : 'already_inactive' }; } finally { conn.release(); } } // NEW: activate a matrix instance by id (idempotent) async function activateInstance(instanceId) { const conn = await pool.getConnection(); try { const id = Number(instanceId); if (!Number.isFinite(id) || id <= 0) { const err = new Error('Invalid matrix instance id'); err.status = 400; throw err; } const [rows] = await conn.query( `SELECT id, is_active FROM matrix_instances WHERE id = ? LIMIT 1`, [id] ); if (!rows.length) { const err = new Error('Matrix instance not found'); err.status = 404; throw err; } const wasActive = !!rows[0].is_active; if (!wasActive) { // Try to clear deactivated_at if column exists try { await conn.query( `UPDATE matrix_instances SET is_active = TRUE, deactivated_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [id] ); } catch (e) { if (e && e.code === 'ER_BAD_FIELD_ERROR') { await conn.query( `UPDATE matrix_instances SET is_active = TRUE, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [id] ); } else { throw e; } } } return { matrixInstanceId: id, wasActive, isActive: true, status: wasActive ? 'already_active' : 'activated' }; } finally { conn.release(); } } module.exports = { createMatrix, ensureUserExistsByEmail, activateEgoMatrix, getMatrixStats, resolveRootUserId, getUserSearchCandidates, getMatrixUsers, addUserToMatrix, resolveInstanceForUser, // NEW getInstanceInfo, // NEW deactivateInstance, // NEW activateInstance, // NEW listInstancesForUser, // NEW userBelongsToInstance // NEW };