const { all, audit, now, one, run, seedBooksForCompany, seedReferenceValuesForCompany } = require('../db'); // Companies are stored in the `entities` table. Each gets its own books/GL, PM plans, alerts, and // (in phase 2) reference data. Disposing a company is a soft status change that preserves all history. function listCompanies({ includeDisposed = false } = {}) { const where = includeDisposed ? '' : "WHERE status = 'active'"; return all(`SELECT * FROM entities ${where} ORDER BY status, name`); } function getCompany(id) { return one('SELECT * FROM entities WHERE id = ?', [id]); } function activeCount() { return one("SELECT COUNT(*) AS c FROM entities WHERE status = 'active'").c; } function createCompany(body, userId) { const name = String(body.name || '').trim(); if (!name) throw Object.assign(new Error('A company name is required'), { status: 400 }); const ts = now(); const result = run( `INSERT INTO entities (name, legal_name, federal_tax_id, state_tax_id, local_tax_id, fiscal_year_end_month, base_currency, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)`, [ name, body.legal_name || null, body.federal_tax_id || null, body.state_tax_id || null, body.local_tax_id || null, Number(body.fiscal_year_end_month || 12), body.base_currency || 'USD', ts, ts ] ); // A new company starts with the standard book set + reference data (categories/locations/depts). seedBooksForCompany(result.lastInsertRowid); seedReferenceValuesForCompany(result.lastInsertRowid); audit(userId, 'create', 'entity', result.lastInsertRowid, null, { name }); return getCompany(result.lastInsertRowid); } function updateCompany(id, body, userId) { const before = getCompany(id); if (!before) return null; run( `UPDATE entities SET name = ?, legal_name = ?, federal_tax_id = ?, state_tax_id = ?, local_tax_id = ?, fiscal_year_end_month = ?, base_currency = ?, updated_at = ? WHERE id = ?`, [ body.name !== undefined ? String(body.name).trim() || before.name : before.name, body.legal_name !== undefined ? body.legal_name : before.legal_name, body.federal_tax_id !== undefined ? body.federal_tax_id : before.federal_tax_id, body.state_tax_id !== undefined ? body.state_tax_id : before.state_tax_id, body.local_tax_id !== undefined ? body.local_tax_id : before.local_tax_id, body.fiscal_year_end_month !== undefined ? Number(body.fiscal_year_end_month) : before.fiscal_year_end_month, body.base_currency !== undefined ? body.base_currency : before.base_currency, now(), id ] ); audit(userId, 'update', 'entity', id, before, body); return getCompany(id); } // Archive/retire a company: it stays in the database (assets, books, GL preserved) but is hidden from // active selection and can no longer be the active company. Guards the last active company. function disposeCompany(id, body, userId) { const before = getCompany(id); if (!before) return { ok: false, status: 404, error: 'Company not found' }; if (before.status === 'disposed') return { ok: false, status: 400, error: 'Company is already disposed' }; if (activeCount() <= 1) return { ok: false, status: 400, error: 'At least one active company is required' }; run("UPDATE entities SET status = 'disposed', disposed_at = ?, disposed_reason = ?, updated_at = ? WHERE id = ?", [now(), body.reason || null, now(), id]); audit(userId, 'dispose', 'entity', id, before, { reason: body.reason || null }); return { ok: true, company: getCompany(id) }; } function restoreCompany(id, userId) { const before = getCompany(id); if (!before) return null; run("UPDATE entities SET status = 'active', disposed_at = NULL, disposed_reason = NULL, updated_at = ? WHERE id = ?", [now(), id]); audit(userId, 'restore', 'entity', id, before, null); return getCompany(id); } // ---- Per-company user access ------------------------------------------------ // A user who can manage users (admin.users / wildcard) sees every company; this prevents an admin // from locking themselves out of any company. Required lazily to avoid a load-time cycle. function isCompanyAdmin(user) { const { capabilitiesForRole } = require('./roles'); const { capabilitySetIncludes } = require('./accessControl'); return capabilitySetIncludes(capabilitiesForRole(user.role), 'admin.users'); } // Active companies a user may see/switch to. Admins → all; others → their assigned companies, falling // back to the default company when they have none (so a user is never locked out). function companiesForUser(user) { if (isCompanyAdmin(user)) { return all("SELECT * FROM entities WHERE status = 'active' ORDER BY name"); } const rows = all( `SELECT e.* FROM entities e JOIN user_companies uc ON uc.entity_id = e.id WHERE uc.user_id = ? AND e.status = 'active' ORDER BY e.name`, [user.id] ); if (rows.length) return rows; const def = one("SELECT * FROM entities WHERE status = 'active' ORDER BY id LIMIT 1"); return def ? [def] : []; } function companyIdsForUser(user) { return companiesForUser(user).map((c) => c.id); } // Explicit assignments stored for a user (independent of the admin bypass). function assignedCompanyIds(userId) { return all('SELECT entity_id FROM user_companies WHERE user_id = ?', [userId]).map((r) => r.entity_id); } function setUserCompanies(userId, ids, actorId) { const valid = (Array.isArray(ids) ? ids : []).filter((id) => one('SELECT id FROM entities WHERE id = ?', [id])); run('DELETE FROM user_companies WHERE user_id = ?', [userId]); for (const id of valid) run('INSERT OR IGNORE INTO user_companies (user_id, entity_id) VALUES (?, ?)', [userId, id]); if (actorId) audit(actorId, 'set_companies', 'user', userId, null, { company_ids: valid }); } module.exports = { assignedCompanyIds, companiesForUser, companyIdsForUser, createCompany, disposeCompany, getCompany, isCompanyAdmin, listCompanies, restoreCompany, setUserCompanies, updateCompany };