-- Multi-Tenant Ledger Schema -- Supports SE (BAS/SIE4), US-DE (GAAP), US-TX (GAAP) -- Company registry CREATE TABLE IF NOT EXISTS boc_companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, legal_name TEXT NOT NULL, org_number TEXT, -- Swedish org nr or US EIN tax_id TEXT, -- EIN for US companies jurisdiction TEXT NOT NULL, -- SE, US-DE, US-TX company_type TEXT NOT NULL, -- AB, Inc, LLC, etc address JSONB, currency TEXT NOT NULL DEFAULT 'SEK', fiscal_year_end DATE NOT NULL DEFAULT '12-31-2026', accounting_std TEXT NOT NULL DEFAULT 'BAS', -- BAS, GAAP, IFRS vat_registered BOOLEAN DEFAULT FALSE, vat_number TEXT, settings JSONB DEFAULT '{}', status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Chart of accounts per company (BAS for SE, GAAP for US) CREATE TABLE IF NOT EXISTS boc_chart_of_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, account_code TEXT NOT NULL, name TEXT NOT NULL, name_en TEXT, -- English name for US entities account_type TEXT NOT NULL, -- asset, liability, equity, revenue, expense account_subtype TEXT, -- current_asset, fixed_asset, current_liability, etc parent_code TEXT, vat_code TEXT, -- SE: 25, 12, 6, 0 | US: exempt is_bank_account BOOLEAN DEFAULT FALSE, is_active BOOLEAN DEFAULT TRUE, sort_order INTEGER, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(company_id, account_code) ); -- Standard BAS kontoplan (Sweden) INSERT INTO boc_chart_of_accounts (company_id, account_code, name, name_en, account_type, account_subtype, vat_code, sort_order) SELECT c.id, a.account_code, a.name_sv, a.name_en, a.account_type, a.account_subtype, a.vat_code, a.sort_order FROM boc_companies c CROSS JOIN (VALUES ('1000', 'Tillgångar', 'Assets', 'asset', 'header', NULL, 100), ('1221', 'Datorer och kringutrustning', 'Computer equipment', 'asset', 'fixed_asset', NULL, 1221), ('1227', 'Personbilar', 'Vehicles', 'asset', 'fixed_asset', NULL, 1227), ('1930', 'Företagskonto / affärskonto', 'Business account', 'asset', 'current_asset', NULL, 1930), ('2000', 'Skulder', 'Liabilities', 'liability', 'header', NULL, 2000), ('2510', 'Skatteskulder', 'Tax liabilities', 'liability', 'current_liability', NULL, 2510), ('2611', 'Utgående moms 25%', 'Output VAT 25%', 'liability', 'current_liability', '25', 2611), ('2640', 'Ingående moms', 'Input VAT', 'asset', 'current_asset', NULL, 2640), ('2990', 'Övriga skulder till närstående / ägaruttag', 'Related party debt / owner drawings', 'liability', 'current_liability', NULL, 2990), ('3000', 'Intäkter', 'Revenue', 'revenue', 'header', NULL, 3000), ('3001', 'Försäljning av tjänster', 'Service revenue', 'revenue', 'operating_revenue', '25', 3001), ('3010', 'Konsultarvode', 'Consulting fees', 'revenue', 'operating_revenue', '25', 3010), ('3020', 'Hyresintäkter', 'Rental income', 'revenue', 'operating_revenue', '25', 3020), ('3900', 'Övriga rörelseintäkter', 'Other operating income', 'revenue', 'other_revenue', '25', 3900), ('5000', 'Kostnader', 'Expenses', 'expense', 'header', NULL, 5000), ('5420', 'Personalrepresentation', 'Staff entertainment', 'expense', 'operating_expense', NULL, 5420), ('5460', 'Arbetskläder och skyddsmaterial', 'Work clothes and safety', 'expense', 'operating_expense', NULL, 5460), ('5612', 'Fordonsskatt', 'Vehicle tax', 'expense', 'operating_expense', NULL, 5612), ('5614', 'Bilförsäkring', 'Vehicle insurance', 'expense', 'operating_expense', NULL, 5614), ('5810', 'Resekostnader', 'Travel expenses', 'expense', 'operating_expense', NULL, 5810), ('5820', 'Biljettkostnader', 'Ticket expenses', 'expense', 'operating_expense', NULL, 5820), ('5900', 'Reklam och marknadsföring', 'Advertising and marketing', 'expense', 'operating_expense', '25', 5900), ('6071', 'Representation avdragsgill', 'Deductible entertainment', 'expense', 'operating_expense', NULL, 6071), ('6540', 'IT-tjänster, köpta', 'IT services purchased', 'expense', 'operating_expense', '25', 6540), ('6550', 'Programvarulicenser', 'Software licenses', 'expense', 'operating_expense', '25', 6550), ('7630', 'Friskvård', 'Wellness', 'expense', 'personnel_expense', NULL, 7630), ('8000', 'Finansiella poster', 'Financial items', 'expense', 'header', NULL, 8000), ('8910', 'Skatt på årets resultat', 'Income tax', 'expense', 'tax_expense', NULL, 8910) ) AS a(account_code, name_sv, name_en, account_type, account_subtype, vat_code, sort_order) WHERE c.jurisdiction = 'SE'; -- Standard GAAP chart (US) INSERT INTO boc_chart_of_accounts (company_id, account_code, name, name_en, account_type, account_subtype, sort_order) SELECT c.id, a.account_code, a.name_en, a.name_en, a.account_type, a.account_subtype, a.sort_order FROM boc_companies c CROSS JOIN (VALUES ('1000', 'Assets', 'asset', 'header', 100), ('1100', 'Cash and equivalents', 'asset', 'current_asset', 1100), ('1200', 'Accounts receivable', 'asset', 'current_asset', 1200), ('1500', 'Computer equipment', 'asset', 'fixed_asset', 1500), ('1600', 'Vehicles', 'asset', 'fixed_asset', 1600), ('2000', 'Liabilities', 'liability', 'header', 2000), ('2100', 'Accounts payable', 'liability', 'current_liability', 2100), ('2200', 'Accrued expenses', 'liability', 'current_liability', 2200), ('2300', 'Taxes payable', 'liability', 'current_liability', 2300), ('2500', 'Related party debt', 'liability', 'current_liability', 2500), ('3000', 'Equity', 'equity', 'header', 3000), ('3100', 'Common stock', 'equity', 'equity', 3100), ('3200', 'Retained earnings', 'equity', 'equity', 3200), ('3500', 'Owner drawings', 'equity', 'equity', 3500), ('4000', 'Revenue', 'revenue', 'header', 4000), ('4100', 'Service revenue', 'revenue', 'operating_revenue', 4100), ('4200', 'Consulting revenue', 'revenue', 'operating_revenue', 4200), ('4300', 'Rental income', 'revenue', 'operating_revenue', 4300), ('4900', 'Other income', 'revenue', 'other_revenue', 4900), ('5000', 'Expenses', 'expense', 'header', 5000), ('5100', 'Advertising', 'expense', 'operating_expense', 5100), ('5200', 'Travel and meals', 'expense', 'operating_expense', 5200), ('5300', 'IT services', 'expense', 'operating_expense', 5300), ('5400', 'Software licenses', 'expense', 'operating_expense', 5400), ('5500', 'Vehicle expenses', 'expense', 'operating_expense', 5500), ('5600', 'Insurance', 'expense', 'operating_expense', 5600), ('5700', 'Professional fees', 'expense', 'operating_expense', 5700), ('6000', 'Personnel', 'expense', 'header', 6000), ('6100', 'Salaries and wages', 'expense', 'personnel_expense', 6100), ('6200', 'Payroll taxes', 'expense', 'personnel_expense', 6200), ('6300', 'Benefits', 'expense', 'personnel_expense', 6300), ('7000', 'Taxes', 'expense', 'header', 7000), ('7100', 'Federal income tax', 'expense', 'tax_expense', 7100), ('7200', 'State income tax', 'expense', 'tax_expense', 7200) ) AS a(account_code, name_en, account_type, account_subtype, sort_order) WHERE c.jurisdiction LIKE 'US-%'; -- Journal entries (universal, works for both BAS and GAAP) CREATE TABLE IF NOT EXISTS boc_journal_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, entry_number TEXT NOT NULL, entry_date DATE NOT NULL, description TEXT NOT NULL, reference TEXT, source TEXT DEFAULT 'manual', -- manual, import, bank, payroll source_id UUID, attachments JSONB DEFAULT '[]', is_reversed BOOLEAN DEFAULT FALSE, reversed_by UUID REFERENCES boc_journal_entries(id), status TEXT NOT NULL DEFAULT 'posted', -- draft, posted, reversed posted_at TIMESTAMPTZ, posted_by UUID REFERENCES boc_users(id), created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(company_id, entry_number) ); -- Journal entry lines CREATE TABLE IF NOT EXISTS boc_journal_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, entry_id UUID REFERENCES boc_journal_entries(id) ON DELETE CASCADE, account_id UUID REFERENCES boc_chart_of_accounts(id) ON DELETE RESTRICT, debit DECIMAL(15,2) NOT NULL DEFAULT 0, credit DECIMAL(15,2) NOT NULL DEFAULT 0, description TEXT, project TEXT, department TEXT, vat_amount DECIMAL(15,2) DEFAULT 0, vat_rate DECIMAL(5,2) DEFAULT 0, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Account balances per period CREATE TABLE IF NOT EXISTS boc_period_balances ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, account_id UUID REFERENCES boc_chart_of_accounts(id) ON DELETE CASCADE, fiscal_year INTEGER NOT NULL, period INTEGER NOT NULL, -- 1-12 for month, 0 for year opening_balance DECIMAL(15,2) NOT NULL DEFAULT 0, closing_balance DECIMAL(15,2) NOT NULL DEFAULT 0, total_debit DECIMAL(15,2) NOT NULL DEFAULT 0, total_credit DECIMAL(15,2) NOT NULL DEFAULT 0, currency TEXT NOT NULL DEFAULT 'SEK', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(company_id, account_id, fiscal_year, period) ); -- VAT/GST tracking (SE: moms, US: sales tax if applicable) CREATE TABLE IF NOT EXISTS boc_tax_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, tax_type TEXT NOT NULL, -- VAT, GST, SalesTax period_start DATE NOT NULL, period_end DATE NOT NULL, tax_in DECIMAL(15,2) NOT NULL DEFAULT 0, tax_out DECIMAL(15,2) NOT NULL DEFAULT 0, tax_payable DECIMAL(15,2) NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'draft', filed_at TIMESTAMPTZ, filed_with TEXT, paid_at TIMESTAMPTZ, metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Payroll (works for both SE and US) CREATE TABLE IF NOT EXISTS boc_payroll_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, employee_id UUID REFERENCES boc_employees(id), period DATE NOT NULL, gross_pay DECIMAL(15,2) NOT NULL, net_pay DECIMAL(15,2) NOT NULL, -- SE specific tax_deduction DECIMAL(15,2) DEFAULT 0, pension_contribution DECIMAL(15,2) DEFAULT 0, -- US specific federal_tax DECIMAL(15,2) DEFAULT 0, state_tax DECIMAL(15,2) DEFAULT 0, social_security DECIMAL(15,2) DEFAULT 0, medicare DECIMAL(15,2) DEFAULT 0, -- Employer contributions employer_contribution DECIMAL(15,2) DEFAULT 0, -- SE: arbetsgivaravgift, US: FUTA + SUTA benefits JSONB DEFAULT '{}', status TEXT NOT NULL DEFAULT 'draft', paid_at TIMESTAMPTZ, journal_entry_id UUID REFERENCES boc_journal_entries(id), metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Intercompany transactions CREATE TABLE IF NOT EXISTS boc_intercompany ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, to_company_id UUID REFERENCES boc_companies(id) ON DELETE CASCADE, entry_id UUID REFERENCES boc_journal_entries(id), amount DECIMAL(15,2) NOT NULL, currency TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'open', reconciled_at TIMESTAMPTZ, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Currency exchange rates CREATE TABLE IF NOT EXISTS boc_exchange_rates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_currency TEXT NOT NULL, to_currency TEXT NOT NULL, rate DECIMAL(15,6) NOT NULL, date DATE NOT NULL, source TEXT DEFAULT 'manual', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(from_currency, to_currency, date) ); -- Indexes CREATE INDEX IF NOT EXISTS idx_companies_tenant ON boc_companies(tenant_id); CREATE INDEX IF NOT EXISTS idx_coa_company ON boc_chart_of_accounts(company_id, account_code); CREATE INDEX IF NOT EXISTS idx_journal_company_date ON boc_journal_entries(company_id, entry_date DESC); CREATE INDEX IF NOT EXISTS idx_journal_lines_entry ON boc_journal_lines(entry_id); CREATE INDEX IF NOT EXISTS idx_journal_lines_account ON boc_journal_lines(account_id); CREATE INDEX IF NOT EXISTS idx_balances_company_period ON boc_period_balances(company_id, fiscal_year, period); CREATE INDEX IF NOT EXISTS idx_tax_reports_company ON boc_tax_reports(company_id, period_start, period_end); CREATE INDEX IF NOT EXISTS idx_payroll_company_period ON boc_payroll_entries(company_id, period); CREATE INDEX IF NOT EXISTS idx_intercompany_from ON boc_intercompany(from_company_id, status); CREATE INDEX IF NOT EXISTS idx_intercompany_to ON boc_intercompany(to_company_id, status);