Files
boc/aamos-billing/schema.sql
T
Bernt 05ed037fe8 pilot.landvex.com: HTTPS + Full Stack Verified
- DNS: pilot.landvex.com -> 16.170.83.169
- TLS: Let's Encrypt certificate (expires 2026-09-30)
- Nginx: reverse proxy with SSL termination
- API: https://pilot.landvex.com/api/v1/missions
- UI: https://pilot.landvex.com/
- Upload: POST /api/v1/missions/import (multipart/form-data)

Verified:
 https://pilot.landvex.com/health
 https://pilot.landvex.com/version
 https://pilot.landvex.com/api/v1/missions (list)
 https://pilot.landvex.com/api/v1/missions/:id (get)
 POST /api/v1/missions/import (video upload)
 UI loads with title 'LandveX Intelligence Lab'

Next: Pilot 001 — Break the system!
2026-07-02 17:34:19 +00:00

371 lines
17 KiB
SQL

-- ═══════════════════════════════════════════════════════════════════════════
-- 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'))
);