const { all, audit, now, one, parseJson, run } = require('../db'); const SOURCES = ['common', 'industry', 'business', 'custom']; // How many asset categories reference a class. Categories link by unique asset_class_id; older // records that only stored asset_class_number fall back to matching on the (non-unique) number. function categoryUsage() { const byId = {}; const byNumber = {}; for (const row of all("SELECT metadata FROM reference_values WHERE type = 'category'")) { const meta = parseJson(row.metadata, {}); if (meta.asset_class_id) byId[meta.asset_class_id] = (byId[meta.asset_class_id] || 0) + 1; else if (meta.asset_class_number) byNumber[meta.asset_class_number] = (byNumber[meta.asset_class_number] || 0) + 1; } // Class numbers are not unique (e.g. 00.12 is shared by Computers, Casinos, and a dozen others). A // category that only stored a number can't be pinned to one class, so the number fallback is only // safe when exactly one class carries that number — otherwise it would falsely flag every sharing // class as "assigned." const numberOwners = {}; for (const row of all('SELECT class_number FROM asset_classes WHERE class_number IS NOT NULL')) { numberOwners[row.class_number] = (numberOwners[row.class_number] || 0) + 1; } return { byId, byNumber, numberOwners }; } function fromRow(row, usage) { if (!row) return null; const idCount = usage.byId[row.id] || 0; const uniqueNumber = row.class_number && usage.numberOwners[row.class_number] === 1; const numberCount = uniqueNumber ? (usage.byNumber[row.class_number] || 0) : 0; return { id: row.id, class_number: row.class_number, description: row.description, gds: row.gds, ads: row.ads, source: row.source, table: row.source, // back-compat alias used by the category picker category_count: idCount + numberCount }; } function list() { const usage = categoryUsage(); return all('SELECT * FROM asset_classes ORDER BY description').map((row) => fromRow(row, usage)); } function getClass(id) { return fromRow(one('SELECT * FROM asset_classes WHERE id = ?', [id]), categoryUsage()); } function normalize(body) { const num = (value) => { if (value === '' || value === null || value === undefined) return null; const n = Number(value); return Number.isFinite(n) ? n : null; }; return { class_number: body.class_number ? String(body.class_number).trim() : null, description: String(body.description || '').trim(), gds: num(body.gds), ads: num(body.ads), source: SOURCES.includes(body.source) ? body.source : 'custom' }; } function createClass(body, userId) { const input = normalize(body); if (!input.description) throw Object.assign(new Error('A description is required'), { status: 400 }); const ts = now(); const result = run( 'INSERT INTO asset_classes (class_number, description, gds, ads, source, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [input.class_number, input.description, input.gds, input.ads, input.source, ts, ts] ); audit(userId, 'create', 'asset_class', result.lastInsertRowid, null, input); return getClass(result.lastInsertRowid); } function updateClass(id, body, userId) { const before = one('SELECT * FROM asset_classes WHERE id = ?', [id]); if (!before) return null; const input = normalize({ ...before, ...body }); if (!input.description) throw Object.assign(new Error('A description is required'), { status: 400 }); run( 'UPDATE asset_classes SET class_number = ?, description = ?, gds = ?, ads = ?, source = ?, updated_at = ? WHERE id = ?', [input.class_number, input.description, input.gds, input.ads, input.source, now(), id] ); audit(userId, 'update', 'asset_class', id, before, input); return getClass(id); } function deleteClass(id, userId) { const before = one('SELECT * FROM asset_classes WHERE id = ?', [id]); if (!before) return false; run('DELETE FROM asset_classes WHERE id = ?', [id]); audit(userId, 'delete', 'asset_class', id, before, null); return true; } module.exports = { SOURCES, createClass, deleteClass, getClass, list, updateClass };