136 lines
5.9 KiB
JavaScript
136 lines
5.9 KiB
JavaScript
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
|
|
};
|