-- ═══════════════════════════════════════════════════════════════════════════ -- AAMOS Billing Engine — PostgreSQL Schema -- LandveX Enterprise Platform — Faktureringsmodul -- Principer: Audit First · Tenant Isolation · Immutable Journal -- ═══════════════════════════════════════════════════════════════════════════ -- ── Agreements (Avtal) ────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_agreements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, agreement_number TEXT NOT NULL, agreement_type TEXT NOT NULL, -- 'kommunalt'|'kommersiellt'|'internt' status TEXT NOT NULL DEFAULT 'draft', -- 'draft'|'active'|'suspended'|'terminated'|'expired' customer_type TEXT NOT NULL, -- 'municipality'|'company'|'individual' customer_org_nr TEXT, customer_name TEXT NOT NULL, customer_address TEXT, customer_postal TEXT, customer_city TEXT, customer_contact TEXT, customer_email TEXT, customer_phone TEXT, customer_reference TEXT, title TEXT NOT NULL, description TEXT, start_date DATE NOT NULL, end_date DATE, auto_renew BOOLEAN NOT NULL DEFAULT FALSE, renewal_period_months INTEGER, termination_notice_days INTEGER DEFAULT 90, currency TEXT NOT NULL DEFAULT 'SEK', billing_frequency TEXT NOT NULL, -- 'monthly'|'quarterly'|'semi_annual'|'annual'|'one_time' billing_day INTEGER NOT NULL DEFAULT 1, payment_terms_days INTEGER NOT NULL DEFAULT 30, late_fee_percent NUMERIC(5,2) DEFAULT 8.0, vat_rate NUMERIC(5,2) NOT NULL DEFAULT 25.0, revenue_account TEXT NOT NULL DEFAULT '3000', receivable_account TEXT NOT NULL DEFAULT '1510', vat_account TEXT NOT NULL DEFAULT '2610', external_system_id TEXT, external_system TEXT, -- 'visma'|'agresso'|'fortnox'|'custom' metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by TEXT NOT NULL, updated_by TEXT NOT NULL, trace_id TEXT NOT NULL, UNIQUE (tenant_id, agreement_number), CONSTRAINT chk_agreement_status CHECK (status IN ('draft','active','suspended','terminated','expired')), CONSTRAINT chk_agreement_type CHECK (agreement_type IN ('kommunalt','kommersiellt','internt')), CONSTRAINT chk_customer_type CHECK (customer_type IN ('municipality','company','individual')), CONSTRAINT chk_billing_freq CHECK (billing_frequency IN ('monthly','quarterly','semi_annual','annual','one_time')) ); CREATE INDEX IF NOT EXISTS idx_agreements_tenant_status ON billing_agreements (tenant_id, status); CREATE INDEX IF NOT EXISTS idx_agreements_customer ON billing_agreements (tenant_id, customer_org_nr); CREATE INDEX IF NOT EXISTS idx_agreements_dates ON billing_agreements (tenant_id, start_date, end_date); -- ── Agreement Lines (Avtalsrader) ─────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_agreement_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agreement_id UUID NOT NULL REFERENCES billing_agreements(id) ON DELETE CASCADE, tenant_id TEXT NOT NULL, line_number INTEGER NOT NULL, description TEXT NOT NULL, quantity NUMERIC(12,2) NOT NULL DEFAULT 1, unit TEXT, -- 'st'|'tim'|'mån'|'år'|'m²' unit_price NUMERIC(18,2) NOT NULL, vat_rate NUMERIC(5,2), discount_percent NUMERIC(5,2) DEFAULT 0, discount_amount NUMERIC(18,2), revenue_account TEXT, cost_center TEXT, project_code TEXT, active_from DATE NOT NULL DEFAULT CURRENT_DATE, active_until DATE, is_active BOOLEAN NOT NULL DEFAULT TRUE, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (agreement_id, line_number) ); CREATE INDEX IF NOT EXISTS idx_agreement_lines_agreement ON billing_agreement_lines (agreement_id); CREATE INDEX IF NOT EXISTS idx_agreement_lines_active ON billing_agreement_lines (agreement_id, is_active, active_from, active_until); -- ── Invoices (Fakturor) ───────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, invoice_number TEXT NOT NULL, agreement_id UUID REFERENCES billing_agreements(id), invoice_type TEXT NOT NULL DEFAULT 'standard', -- 'standard'|'credit'|'proforma'|'reminder' status TEXT NOT NULL DEFAULT 'draft', -- 'draft'|'sent'|'paid'|'partially_paid'|'overdue'|'cancelled'|'credited' customer_org_nr TEXT, customer_name TEXT NOT NULL, customer_address TEXT, customer_postal TEXT, customer_city TEXT, customer_contact TEXT, customer_email TEXT, customer_reference TEXT, invoice_date DATE NOT NULL, due_date DATE NOT NULL, delivery_date DATE, sent_at TIMESTAMPTZ, paid_at TIMESTAMPTZ, credited_at TIMESTAMPTZ, currency TEXT NOT NULL DEFAULT 'SEK', subtotal NUMERIC(18,2) NOT NULL DEFAULT 0, vat_total NUMERIC(18,2) NOT NULL DEFAULT 0, total NUMERIC(18,2) NOT NULL DEFAULT 0, amount_paid NUMERIC(18,2) NOT NULL DEFAULT 0, amount_due NUMERIC(18,2) NOT NULL DEFAULT 0, vat_specification JSONB NOT NULL DEFAULT '[]', journal_entry_id UUID, ledger_posted_at TIMESTAMPTZ, credited_by_invoice_id UUID, original_invoice_id UUID, reminder_count INTEGER NOT NULL DEFAULT 0, last_reminder_at TIMESTAMPTZ, reminder_fee NUMERIC(18,2) DEFAULT 0, interest_fee NUMERIC(18,2) DEFAULT 0, pdf_generated_at TIMESTAMPTZ, pdf_url TEXT, external_system_id TEXT, external_system TEXT, exported_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}', notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by TEXT NOT NULL, updated_by TEXT NOT NULL, trace_id TEXT NOT NULL, correlation_id TEXT NOT NULL, UNIQUE (tenant_id, invoice_number), CONSTRAINT chk_invoice_status CHECK (status IN ('draft','sent','paid','partially_paid','overdue','cancelled','credited')), CONSTRAINT chk_invoice_type CHECK (invoice_type IN ('standard','credit','proforma','reminder')) ); CREATE INDEX IF NOT EXISTS idx_invoices_tenant_status ON billing_invoices (tenant_id, status); CREATE INDEX IF NOT EXISTS idx_invoices_agreement ON billing_invoices (agreement_id); CREATE INDEX IF NOT EXISTS idx_invoices_dates ON billing_invoices (tenant_id, invoice_date, due_date); CREATE INDEX IF NOT EXISTS idx_invoices_overdue ON billing_invoices (tenant_id, status, due_date) WHERE status IN ('sent', 'partially_paid'); CREATE INDEX IF NOT EXISTS idx_invoices_journal ON billing_invoices (journal_entry_id); CREATE INDEX IF NOT EXISTS idx_invoices_external ON billing_invoices (tenant_id, external_system, external_system_id); -- ── Invoice Lines (Fakturarader) ──────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_invoice_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID NOT NULL REFERENCES billing_invoices(id) ON DELETE CASCADE, tenant_id TEXT NOT NULL, line_number INTEGER NOT NULL, description TEXT NOT NULL, quantity NUMERIC(12,2) NOT NULL DEFAULT 1, unit TEXT, unit_price NUMERIC(18,2) NOT NULL, line_total NUMERIC(18,2) NOT NULL, vat_rate NUMERIC(5,2) NOT NULL, vat_amount NUMERIC(18,2) NOT NULL, line_total_incl_vat NUMERIC(18,2) NOT NULL, agreement_line_id UUID REFERENCES billing_agreement_lines(id), revenue_account TEXT NOT NULL, cost_center TEXT, project_code TEXT, metadata JSONB NOT NULL DEFAULT '{}', UNIQUE (invoice_id, line_number) ); CREATE INDEX IF NOT EXISTS idx_invoice_lines_invoice ON billing_invoice_lines (invoice_id); CREATE INDEX IF NOT EXISTS idx_invoice_lines_account ON billing_invoice_lines (tenant_id, revenue_account); -- ── Payments (Inbetalningar) ──────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, invoice_id UUID NOT NULL REFERENCES billing_invoices(id), payment_date DATE NOT NULL, amount NUMERIC(18,2) NOT NULL, currency TEXT NOT NULL DEFAULT 'SEK', payment_method TEXT NOT NULL, -- 'bank_transfer'|'swish'|'card'|'cash'|'autogiro'|'other' payment_reference TEXT, bank_transaction_id TEXT, reconciliation_status TEXT NOT NULL DEFAULT 'unreconciled', -- 'unreconciled'|'matched'|'reconciled' reconciled_at TIMESTAMPTZ, reconciled_by TEXT, journal_entry_id UUID, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by TEXT NOT NULL, trace_id TEXT NOT NULL, UNIQUE (tenant_id, bank_transaction_id), CONSTRAINT chk_payment_method CHECK (payment_method IN ('bank_transfer','swish','card','cash','autogiro','other')), CONSTRAINT chk_recon_status CHECK (reconciliation_status IN ('unreconciled','matched','reconciled')) ); CREATE INDEX IF NOT EXISTS idx_payments_invoice ON billing_payments (invoice_id); CREATE INDEX IF NOT EXISTS idx_payments_reconciliation ON billing_payments (tenant_id, reconciliation_status); CREATE INDEX IF NOT EXISTS idx_payments_date ON billing_payments (tenant_id, payment_date); -- ── Reminders (Påminnelser / Inkasso) ─────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_reminders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, invoice_id UUID NOT NULL REFERENCES billing_invoices(id), reminder_number INTEGER NOT NULL, reminder_type TEXT NOT NULL, -- 'reminder'|'debt_collection'|'legal' sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), due_date DATE NOT NULL, reminder_fee NUMERIC(18,2) NOT NULL DEFAULT 0, interest_fee NUMERIC(18,2) NOT NULL DEFAULT 0, total_demand NUMERIC(18,2) NOT NULL, status TEXT NOT NULL DEFAULT 'sent', -- 'sent'|'paid'|'escalated'|'cancelled' sent_via TEXT, -- 'email'|'post'|'sms' email_address TEXT, pdf_url TEXT, metadata JSONB NOT NULL DEFAULT '{}', created_by TEXT NOT NULL, trace_id TEXT NOT NULL, CONSTRAINT chk_reminder_type CHECK (reminder_type IN ('reminder','debt_collection','legal')), CONSTRAINT chk_reminder_status CHECK (status IN ('sent','paid','escalated','cancelled')) ); CREATE INDEX IF NOT EXISTS idx_reminders_invoice ON billing_reminders (invoice_id); CREATE INDEX IF NOT EXISTS idx_reminders_status ON billing_reminders (tenant_id, status); -- ── Schedules (Schemaläggning) ────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_schedules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, agreement_id UUID NOT NULL REFERENCES billing_agreements(id) ON DELETE CASCADE, schedule_type TEXT NOT NULL, -- 'recurring'|'one_time' frequency TEXT NOT NULL, -- 'monthly'|'quarterly'|'semi_annual'|'annual' day_of_month INTEGER NOT NULL DEFAULT 1, next_run_date DATE NOT NULL, last_run_date DATE, last_invoice_id UUID REFERENCES billing_invoices(id), is_active BOOLEAN NOT NULL DEFAULT TRUE, paused_until DATE, runs_count INTEGER NOT NULL DEFAULT 0, failures_count INTEGER NOT NULL DEFAULT 0, last_error TEXT, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, agreement_id, schedule_type), CONSTRAINT chk_schedule_type CHECK (schedule_type IN ('recurring','one_time')), CONSTRAINT chk_schedule_freq CHECK (frequency IN ('monthly','quarterly','semi_annual','annual')) ); CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON billing_schedules (next_run_date, is_active) WHERE is_active = TRUE; -- ── Audit Log ─────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, trace_id TEXT NOT NULL, correlation_id TEXT NOT NULL, user_id TEXT NOT NULL, entity_type TEXT NOT NULL, -- 'agreement'|'invoice'|'payment'|'reminder' entity_id TEXT NOT NULL, action TEXT NOT NULL, -- 'created'|'updated'|'sent'|'paid'|'cancelled'|'credited'|'reminded' decision_source TEXT NOT NULL DEFAULT 'user', before_state JSONB, after_state JSONB, ip_address TEXT, session_id TEXT, ts TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_billing_audit_entity ON billing_audit_log (tenant_id, entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_billing_audit_trace ON billing_audit_log (trace_id); CREATE INDEX IF NOT EXISTS idx_billing_audit_ts ON billing_audit_log (tenant_id, ts); -- ── Export Jobs ───────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_export_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, job_type TEXT NOT NULL, -- 'sie4'|'pdf'|'csv'|'visma'|'agresso' job_status TEXT NOT NULL DEFAULT 'pending', -- 'pending'|'running'|'completed'|'failed' period_from TEXT, period_to TEXT, fiscal_year INTEGER, invoice_ids UUID[], agreement_ids UUID[], file_url TEXT, file_size_bytes INTEGER, record_count INTEGER, error_message TEXT, requested_by TEXT NOT NULL, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), trace_id TEXT NOT NULL, CONSTRAINT chk_export_type CHECK (job_type IN ('sie4','pdf','csv','visma','agresso')), CONSTRAINT chk_export_status CHECK (job_status IN ('pending','running','completed','failed')) ); CREATE INDEX IF NOT EXISTS idx_export_jobs_tenant ON billing_export_jobs (tenant_id, job_status); -- ── Integration Configs ───────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS billing_integration_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, system_name TEXT NOT NULL, -- 'visma'|'agresso'|'fortnox'|'custom' is_active BOOLEAN NOT NULL DEFAULT FALSE, config JSONB NOT NULL, last_sync_at TIMESTAMPTZ, last_sync_status TEXT, last_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, system_name), CONSTRAINT chk_integration_system CHECK (system_name IN ('visma','agresso','fortnox','custom')) );