87 lines
3.6 KiB
Go
87 lines
3.6 KiB
Go
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
|
|
}
|