const fs = require('fs'); const path = require('path'); const { DatabaseSync } = require('node:sqlite'); const bcrypt = require('bcryptjs'); const { expandRuleSet } = require('./rule-catalog'); const DATA_DIR = process.env.MIXEDASSETS_DATA_DIR || path.join(process.cwd(), 'data'); const DB_PATH = process.env.MIXEDASSETS_DB_PATH || path.join(DATA_DIR, 'mixedassets.sqlite'); fs.mkdirSync(DATA_DIR, { recursive: true }); const db = new DatabaseSync(DB_PATH); db.exec('PRAGMA foreign_keys = ON;'); db.exec('PRAGMA journal_mode = WAL;'); function now() { return new Date().toISOString(); } function json(value, fallback = null) { if (value === undefined) return fallback === null ? null : JSON.stringify(fallback); return JSON.stringify(value); } function parseJson(value, fallback = null) { if (value === null || value === undefined || value === '') return fallback; try { return JSON.parse(value); } catch { return fallback; } } function bind(statement, params) { if (Array.isArray(params)) return statement; return statement; } function args(params) { if (Array.isArray(params)) return params; if (params && Object.keys(params).length > 0) return [params]; return []; } function one(sql, params = {}) { const statement = bind(db.prepare(sql), params); return statement.get(...args(params)); } function all(sql, params = {}) { const statement = bind(db.prepare(sql), params); return statement.all(...args(params)); } function run(sql, params = {}) { const statement = bind(db.prepare(sql), params); return statement.run(...args(params)); } function tx(fn) { db.exec('BEGIN IMMEDIATE'); try { const result = fn(); db.exec('COMMIT'); return result; } catch (error) { db.exec('ROLLBACK'); throw error; } } function initialize() { db.exec(` CREATE TABLE IF NOT EXISTS teams ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, team_id INTEGER REFERENCES teams(id), name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin','finance','operations','viewer')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS user_preferences ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, preferences_json TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, legal_name TEXT, tax_id TEXT, fiscal_year_end_month INTEGER NOT NULL DEFAULT 12, base_currency TEXT NOT NULL DEFAULT 'USD', created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS reference_values ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, name TEXT NOT NULL, code TEXT, metadata TEXT, UNIQUE(type, name) ); CREATE TABLE IF NOT EXISTS employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, workday_worker_id TEXT UNIQUE, employee_number TEXT, name TEXT NOT NULL, email TEXT, department TEXT, location TEXT, manager_name TEXT, status TEXT NOT NULL DEFAULT 'active', source TEXT NOT NULL DEFAULT 'manual', source_payload TEXT, last_synced_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS assets ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id TEXT NOT NULL UNIQUE, entity_id INTEGER REFERENCES entities(id), template_id INTEGER, description TEXT NOT NULL, category TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'in_service', acquisition_cost REAL NOT NULL DEFAULT 0, acquired_date TEXT, in_service_date TEXT, useful_life_months INTEGER NOT NULL DEFAULT 60, salvage_value REAL NOT NULL DEFAULT 0, land_value REAL NOT NULL DEFAULT 0, prior_depreciation REAL NOT NULL DEFAULT 0, location TEXT, department TEXT, group_name TEXT, custodian TEXT, vendor TEXT, serial_number TEXT, invoice_number TEXT, gl_asset_account TEXT, gl_expense_account TEXT, gl_accumulated_account TEXT, barcode_value TEXT, condition TEXT, new_or_used TEXT DEFAULT 'new', listed_property INTEGER NOT NULL DEFAULT 0, business_use_percent REAL NOT NULL DEFAULT 100, investment_use_percent REAL NOT NULL DEFAULT 0, disposal_date TEXT, disposal_price REAL, disposal_expense REAL, disposal_type TEXT, property_type TEXT, notes TEXT, custom_fields TEXT, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS asset_books ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE, book_type TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1, cost REAL, depreciation_method TEXT NOT NULL DEFAULT 'straight_line', convention TEXT NOT NULL DEFAULT 'half_year', useful_life_months INTEGER, section_179_amount REAL NOT NULL DEFAULT 0, bonus_percent REAL NOT NULL DEFAULT 0, business_use_percent REAL, investment_use_percent REAL, manual_depreciation TEXT, UNIQUE(asset_id, book_type) ); CREATE TABLE IF NOT EXISTS asset_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, defaults TEXT NOT NULL, custom_fields TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tax_rule_sets ( id INTEGER PRIMARY KEY AUTOINCREMENT, jurisdiction TEXT NOT NULL, name TEXT NOT NULL, effective_date TEXT NOT NULL, version TEXT NOT NULL, rules_json TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1, source_note TEXT, created_at TEXT NOT NULL, UNIQUE(jurisdiction, version) ); CREATE TABLE IF NOT EXISTS leases ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER REFERENCES assets(id) ON DELETE CASCADE, lessor TEXT, contract_value REAL NOT NULL DEFAULT 0, start_date TEXT, end_date TEXT, discount_rate REAL NOT NULL DEFAULT 0, payment_frequency TEXT NOT NULL DEFAULT 'monthly', notes TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS warranties ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE, provider TEXT, phone TEXT, start_date TEXT, expiration_date TEXT, warranty_limit TEXT, actual_usage TEXT, coverage_details TEXT, document_name TEXT, document_base64 TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS asset_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE, file_name TEXT NOT NULL, mime_type TEXT NOT NULL, size INTEGER NOT NULL, content_base64 TEXT NOT NULL, uploaded_by INTEGER REFERENCES users(id), uploaded_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER REFERENCES assets(id) ON DELETE CASCADE, title TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'maintenance', status TEXT NOT NULL DEFAULT 'open', due_date TEXT, assigned_to INTEGER REFERENCES users(id), notes TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS checkouts ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE, checked_out_to TEXT NOT NULL, destination TEXT, due_date TEXT, checked_out_at TEXT NOT NULL, checked_in_at TEXT, notes TEXT ); CREATE TABLE IF NOT EXISTS asset_assignments ( id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE, employee_id INTEGER REFERENCES employees(id), department TEXT, location TEXT, status TEXT NOT NULL DEFAULT 'assigned', assigned_at TEXT NOT NULL, released_at TEXT, assigned_by INTEGER REFERENCES users(id), release_reason TEXT, notes TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS workday_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, tenant TEXT, base_url TEXT, token_url TEXT, client_id TEXT, client_secret TEXT, workers_path TEXT NOT NULL DEFAULT '/workers', enabled INTEGER NOT NULL DEFAULT 0, last_sync_at TEXT, last_sync_status TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS saved_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, report_type TEXT NOT NULL, options_json TEXT NOT NULL, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS app_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, actor_id INTEGER REFERENCES users(id), action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id TEXT, before_json TEXT, after_json TEXT, created_at TEXT NOT NULL ); `); seed(); } function seed() { const createdAt = now(); if (!one('SELECT id FROM teams LIMIT 1')) { run('INSERT INTO teams (name, description, created_at) VALUES (?, ?, ?)', [ 'Finance Operations', 'Default MixedAssets team', createdAt ]); } const team = one('SELECT id FROM teams WHERE name = ?', ['Finance Operations']); if (!one('SELECT id FROM users LIMIT 1')) { run( 'INSERT INTO users (team_id, name, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [ team.id, 'MixedAssets Admin', 'admin@mixedassets.local', bcrypt.hashSync(process.env.MIXEDASSETS_ADMIN_PASSWORD || 'ChangeMe123!', 12), 'admin', createdAt, createdAt ] ); } if (!one('SELECT id FROM entities LIMIT 1')) { run( 'INSERT INTO entities (name, legal_name, fiscal_year_end_month, base_currency, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', ['Demo Company', 'Demo Company LLC', 12, 'USD', createdAt, createdAt] ); } const refs = [ ['location', 'Headquarters', 'HQ'], ['location', 'Warehouse', 'WH'], ['department', 'Accounting', 'ACCT'], ['department', 'Operations', 'OPS'], ['category', 'Computer Equipment', 'COMP'], ['category', 'Office Furniture', 'FURN'], ['category', 'Vehicles', 'AUTO'], ['category', 'Leasehold Improvements', 'LHI'] ]; for (const item of refs) { run('INSERT OR IGNORE INTO reference_values (type, name, code, metadata) VALUES (?, ?, ?, ?)', [ item[0], item[1], item[2], '{}' ]); } run( `INSERT OR IGNORE INTO employees ( employee_number, name, email, department, location, manager_name, status, source, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ['E-1001', 'Jordan Avery', 'jordan.avery@example.com', 'Operations', 'Headquarters', 'MixedAssets Admin', 'active', 'seed', createdAt, createdAt] ); run( `INSERT OR IGNORE INTO workday_connections ( name, tenant, base_url, token_url, workers_path, enabled, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, ['Primary Workday', '', '', '', '/workers', 0, createdAt, createdAt] ); const starterTemplate = { category: 'Computer Equipment', useful_life_months: 60, depreciation_method: 'macrs_gds_200db_5', convention: 'half_year', gl_asset_account: '1500', gl_expense_account: '6400', gl_accumulated_account: '1590', business_use_percent: 100 }; run( 'INSERT OR IGNORE INTO asset_templates (name, description, defaults, custom_fields, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [ 'Laptop or workstation', 'Standard computer equipment template with 5-year tax life.', json(starterTemplate), json([ { key: 'assigned_employee_id', label: 'Employee ID', type: 'text', required: false }, { key: 'software_profile', label: 'Software profile', type: 'text', required: false } ]), createdAt, createdAt ] ); const federalPath = path.join(process.cwd(), 'data', 'tax-rules', 'federal-2026.json'); if (fs.existsSync(federalPath)) { const rules = expandRuleSet(JSON.parse(fs.readFileSync(federalPath, 'utf8'))); run( `INSERT INTO tax_rule_sets (jurisdiction, name, effective_date, version, rules_json, active, source_note, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(jurisdiction, version) DO UPDATE SET name = excluded.name, effective_date = excluded.effective_date, rules_json = excluded.rules_json, active = excluded.active, source_note = excluded.source_note`, [ rules.jurisdiction || 'US-FED', rules.name || 'Federal starter depreciation rules', rules.effectiveDate || rules.effective_date || '2026-01-01', rules.version || '2026.2', json(rules), 1, rules.sourceNote || 'Starter rule template. Confirm current tax law before production filing.', createdAt ] ); } const settings = { server_fqdn: process.env.MIXEDASSETS_SERVER_FQDN || 'localhost', cors_allowed_domains: process.env.MIXEDASSETS_CORS_ORIGINS || 'http://localhost:5173', database_driver: 'node:sqlite', database_path: DB_PATH, default_books: 'GAAP,FEDERAL,STATE,AMT,ACE,USER' }; for (const [key, value] of Object.entries(settings)) { run('INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)', [key, String(value), createdAt]); } } function audit(actorId, action, entityType, entityId, beforeValue, afterValue) { run( 'INSERT INTO audit_logs (actor_id, action, entity_type, entity_id, before_json, after_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [actorId || null, action, entityType, entityId === undefined ? null : String(entityId), json(beforeValue), json(afterValue), now()] ); } module.exports = { DB_PATH, all, audit, db, initialize, json, now, one, parseJson, run, tx };