05ed037fe8
- 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!
371 lines
17 KiB
SQL
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'))
|
|
);
|