package database import "fmt" // Migrate runs the schema bootstrap. SQLite doesn't need a migration tool — // CREATE TABLE IF NOT EXISTS is idempotent so this is safe to call on every start. func Migrate(db *DB) error { stmts := []string{ `CREATE TABLE IF NOT EXISTS departments ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, cost_center TEXT NOT NULL DEFAULT '', active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')) )`, `CREATE TABLE IF NOT EXISTS gl_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, description TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('revenue','cogs','opex','capex','headcount')), favour_high INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1 )`, `CREATE TABLE IF NOT EXISTS budgets ( id INTEGER PRIMARY KEY AUTOINCREMENT, fiscal_year INTEGER NOT NULL, fiscal_period INTEGER NOT NULL CHECK(fiscal_period BETWEEN 1 AND 12), version TEXT NOT NULL DEFAULT 'original' CHECK(version IN ('original','forecast_1','forecast_2','forecast_3')), department_id INTEGER NOT NULL REFERENCES departments(id), gl_account_id INTEGER NOT NULL REFERENCES gl_accounts(id), amount REAL NOT NULL, currency TEXT NOT NULL DEFAULT 'DKK', notes TEXT NOT NULL DEFAULT '', created_by TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), UNIQUE(fiscal_year, fiscal_period, version, department_id, gl_account_id) )`, `CREATE TABLE IF NOT EXISTS actuals ( id INTEGER PRIMARY KEY AUTOINCREMENT, fiscal_year INTEGER NOT NULL, fiscal_period INTEGER NOT NULL CHECK(fiscal_period BETWEEN 1 AND 12), department_id INTEGER NOT NULL REFERENCES departments(id), gl_account_id INTEGER NOT NULL REFERENCES gl_accounts(id), amount REAL NOT NULL, currency TEXT NOT NULL DEFAULT 'DKK', source TEXT NOT NULL DEFAULT '', ingested_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')), UNIQUE(fiscal_year, fiscal_period, department_id, gl_account_id) )`, // Seed reference data if not already present `INSERT OR IGNORE INTO departments (code, name, cost_center) VALUES ('ENG', 'Engineering', 'CC-100'), ('SALES', 'Sales', 'CC-200'), ('MKTG', 'Marketing', 'CC-300'), ('FINANCE', 'Finance & Operations', 'CC-400'), ('PRODUCT', 'Product', 'CC-500')`, `INSERT OR IGNORE INTO gl_accounts (code, description, type, favour_high) VALUES ('4000', 'Subscription Revenue', 'revenue', 1), ('4100', 'Professional Services', 'revenue', 1), ('5000', 'Cloud Infrastructure', 'cogs', 0), ('5100', 'Third-party Licenses', 'cogs', 0), ('6100', 'Salaries & Wages', 'headcount', 0), ('6110', 'Employer Payroll Tax', 'headcount', 0), ('6120', 'Employee Benefits', 'headcount', 0), ('6300', 'Software Subscriptions', 'opex', 0), ('6310', 'Travel & Entertainment', 'opex', 0), ('6400', 'Marketing & Advertising', 'opex', 0), ('6500', 'Consulting & Contractors', 'opex', 0), ('6600', 'Office & Facilities', 'opex', 0)`, } for _, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { return fmt.Errorf("migrate: %w", err) } } return nil }