Files
boc/docs/arc/ARC-001-canonical-domain-model.md
T

430 lines
16 KiB
Markdown
Raw Normal View History

# ARC-001 — Canonical Domain Model
**Status:** Accepted
**Datum:** 2026-06-01
**Beslutsfattare:** AAMOS Architecture Team
---
## Kontext
AAMOS är ett verksamhetsoperativsystem vars primära designkrav är att moduler (Ekonomi, CRM, HR, Projekt, Kvalitet, Inköp, Kontrakt) kan samexistera, utbyta data och bygga på varandras entiteter utan att skapa n×m-mappningsproblem.
Utan ett kanoniskt objektsystem uppstår ofrånkomligen:
- **Semantisk drift** — samma begrepp (t.ex. "Person") modelleras på 4 olika sätt i 4 moduler, vilket omöjliggör korsmodulärsökning och AI-kontext
- **Integrationsskuld** — varje ny modul måste skriva egna adaptrar mot alla befintliga moduler
- **Auditbrist** — när ett objekt existerar på flera ställen blir spårbarhet svårt att garantera
Ekonomimodulen är referensimplementationen. Alla val i detta ADR speglas redan (eller ska speglas) i Ekonomimodulens databasschema.
---
## Beslut
AAMOS definierar tolv universella kanoniska objekt. Varje modul **MÅSTE** använda dessa objekt som primärreferenser och får inte definiera alternativa entiteter för samma semantiska koncept.
### Fundamentala fält (alla objekt)
Varje kanoniskt objekt bär alltid dessa fält:
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `id` | `UUID v7` | Globalt unikt, tidsordnat |
| `tenant_id` | `UUID` | Isoleringsdomän (se ARC-003) |
| `created_at` | `timestamptz` | ISO 8601, UTC |
| `updated_at` | `timestamptz` | ISO 8601, UTC, auto-update |
| `created_by` | `UUID` | Referens till Person.id eller system-ID |
| `deleted_at` | `timestamptz\|null` | Soft-delete, null = aktiv |
| `metadata` | `jsonb` | Fri nyckel-värde-extension (modul-specifika fält) |
| `version` | `integer` | Optimistic locking, inkrementeras vid varje UPDATE |
> **UUID v7** används för att primary key-index förblir klusterordnade i PostgreSQL och förhindrar page splits vid hög insertfrekvens.
---
### Objekt 1 — Entity
> Abstrakt basobjekt. Alla övriga kanoniska objekt **ärver** Entity-kontraktet (de fundamentala fälten ovan). Entity instansieras aldrig direkt — det är ett kontrakt, inte en tabell.
**Syfte:** Definiera det minsta kontraktet som gör ett objekt spårbart, isolerat och versionerat.
**Relationer:**
- Varje konkret objekt implementerar Entity-kontraktet
- Entity-kontraktet gör det möjligt för generiska tjänster (audit, sök, event-routing) att hantera alla objekt uniformt
---
### Objekt 2 — Organization
> Bolag, juridisk person, koncern, avdelning, team, kostnadsställe.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `name` | `text` | Officiellt namn |
| `org_number` | `text\|null` | Organisationsnummer (landsspecifikt) |
| `org_type` | `enum` | `company`, `department`, `team`, `group`, `subsidiary` |
| `parent_id` | `UUID\|null` | Självrefererande hierarki |
| `country_code` | `char(2)` | ISO 3166-1 alpha-2 |
| `tax_id` | `text\|null` | Momsregistreringsnummer |
| `currency_code` | `char(3)` | ISO 4217 (primärvaluta) |
| `status` | `enum` | `active`, `inactive`, `merged`, `dissolved` |
| `external_refs` | `jsonb` | `{erp_id, crm_id, ...}` |
**Relationer:**
- Organization → Organization (parent_id, hierarkisk)
- Organization ↔ Person (many-to-many via `memberships`)
- Organization → Location (many)
- Organization → Contract (many)
**Ekonomimodulen-mappning:**
`accounts.owner_org_id → Organization.id`
`journal_entries.entity_id → Organization.id` (när motpart är bolag)
---
### Objekt 3 — Person
> Anställd, kontaktperson, ägare, kund, leverantörskontakt, systemanvändare.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `first_name` | `text` | |
| `last_name` | `text` | |
| `display_name` | `text` | Beräknat eller angivet |
| `email` | `text\|null` | Primär e-post |
| `phone` | `text\|null` | E.164-format |
| `national_id` | `encrypted_text\|null` | Personnummer — krypterat i vila |
| `person_type` | `enum` | `employee`, `contact`, `customer`, `owner`, `system` |
| `primary_org_id` | `UUID\|null` | → Organization.id |
| `user_account_id` | `UUID\|null` | → auth-systemets User.id |
| `gdpr_consent` | `jsonb` | Samtyckesregister per ändamål |
**Relationer:**
- Person → Organization (many-to-many via memberships)
- Person → Location (many, adress-koppling)
- Person → Task (assignee)
- Person → Contract (signatory)
**Ekonomimodulen-mappning:**
`suppliers.contact_person_id → Person.id`
`expense_reports.submitted_by → Person.id`
---
### Objekt 4 — Asset
> Anläggningstillgång, resursobjekt, produkt, inventariepost, licenspost.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `name` | `text` | |
| `asset_type` | `enum` | `fixed_asset`, `inventory`, `product`, `license`, `resource`, `intangible` |
| `serial_number` | `text\|null` | |
| `acquisition_date` | `date\|null` | |
| `acquisition_cost` | `numeric(18,4)\|null` | |
| `currency_code` | `char(3)\|null` | |
| `owner_org_id` | `UUID\|null` | → Organization.id |
| `location_id` | `UUID\|null` | → Location.id |
| `status` | `enum` | `active`, `disposed`, `under_maintenance`, `transferred` |
| `depreciation_policy` | `jsonb\|null` | Avskrivningsregler (Ekonomi) |
**Relationer:**
- Asset → Organization (owner)
- Asset → Location (plats)
- Asset → Transaction (inköp, avskrivning)
- Asset → Contract (leasing, garanti)
**Ekonomimodulen-mappning:**
`fixed_assets` tabell är en direkt implementation av Asset med utökad `depreciation_policy`.
---
### Objekt 5 — Location
> Adress, arbetsplats, lagerplats, land, leveransadress.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `name` | `text\|null` | T.ex. "Huvudkontor Stockholm" |
| `location_type` | `enum` | `address`, `workplace`, `warehouse`, `country`, `virtual` |
| `street` | `text\|null` | |
| `city` | `text\|null` | |
| `postal_code` | `text\|null` | |
| `country_code` | `char(2)` | ISO 3166-1 |
| `region` | `text\|null` | Stat/landsdel |
| `coordinates` | `point\|null` | PostGIS `POINT(lon lat)` |
| `parent_id` | `UUID\|null` | Hierarki (land → stad → adress) |
**Relationer:**
- Location → Organization (many)
- Location → Person (hemadress, arbetsplats)
- Location → Asset (plats)
---
### Objekt 6 — Project
> Projekt, uppdrag, kostnadsbärare, kampanj, budgetpost.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `name` | `text` | |
| `code` | `text\|null` | Projektnamn/nummer för referens |
| `project_type` | `enum` | `internal`, `client`, `rnd`, `infrastructure`, `overhead` |
| `status` | `enum` | `planning`, `active`, `on_hold`, `completed`, `cancelled` |
| `owner_id` | `UUID` | → Person.id |
| `org_id` | `UUID` | → Organization.id |
| `budget` | `numeric(18,4)\|null` | |
| `currency_code` | `char(3)\|null` | |
| `start_date` | `date\|null` | |
| `end_date` | `date\|null` | |
| `parent_project_id` | `UUID\|null` | Delprojekt-hierarki |
**Relationer:**
- Project → Organization (ägande)
- Project → Person (projektledare, team)
- Project → Task (nedbrytning)
- Project → Transaction (kostnader, intäkter)
- Project → Contract (kontrakt kopplade till projekt)
**Ekonomimodulen-mappning:**
`cost_centers.project_id → Project.id`
`budget_lines.project_id → Project.id`
---
### Objekt 7 — Task
> Arbetsuppgift, aktivitet, ärende, batchjobb, processuppgift.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `title` | `text` | |
| `description` | `text\|null` | |
| `task_type` | `enum` | `manual`, `automated`, `approval`, `review`, `notification` |
| `status` | `enum` | `pending`, `in_progress`, `blocked`, `completed`, `cancelled` |
| `priority` | `enum` | `low`, `medium`, `high`, `critical` |
| `assignee_id` | `UUID\|null` | → Person.id |
| `project_id` | `UUID\|null` | → Project.id |
| `parent_task_id` | `UUID\|null` | Subtask-hierarki |
| `due_date` | `timestamptz\|null` | |
| `completed_at` | `timestamptz\|null` | |
| `workflow_instance_id` | `UUID\|null` | → Workflow.id |
**Relationer:**
- Task → Person (assignee, creator)
- Task → Project (tillhörighet)
- Task → Workflow (processdrivet)
- Task → Document (bilagor)
---
### Objekt 8 — Workflow
> Processinstans. Kopplas till Workflow Engine (framtida).
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `workflow_definition_id` | `text` | Referens till process-definition |
| `workflow_version` | `text` | Versionerad processdefinition |
| `status` | `enum` | `pending`, `running`, `waiting`, `completed`, `failed`, `cancelled` |
| `context` | `jsonb` | Runtime-kontext för processinstansen |
| `started_at` | `timestamptz\|null` | |
| `completed_at` | `timestamptz\|null` | |
| `initiator_id` | `UUID` | → Person.id eller system |
| `current_step` | `text\|null` | Stegidentifierare |
**Relationer:**
- Workflow → Task (genererade uppgifter)
- Workflow → Document (processade dokument)
- Workflow → Transaction (utlösta transaktioner)
**Ekonomimodulen-mappning:**
Fakturaflöde (inkommen faktura → godkännande → betalning) är en Workflow-instans.
---
### Objekt 9 — Document
> Faktura, kvitto, kontrakt, rapport, fil, attest.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `title` | `text` | |
| `document_type` | `enum` | `invoice`, `receipt`, `contract`, `report`, `attachment`, `specification`, `certificate` |
| `mime_type` | `text\|null` | `application/pdf`, `image/jpeg`, etc. |
| `storage_key` | `text\|null` | S3/blob-sökväg |
| `checksum` | `text\|null` | SHA-256 av filinnehållet |
| `size_bytes` | `bigint\|null` | |
| `source_system` | `text\|null` | Ursprungssystem |
| `related_entity_id` | `UUID\|null` | Polymorf referens |
| `related_entity_type` | `text\|null` | T.ex. `Transaction`, `Contract` |
| `classification` | `enum` | `public`, `internal`, `confidential`, `restricted` (se ARC-003) |
| `extracted_data` | `jsonb\|null` | AI-extraherade fält (se ARC-002) |
**Relationer:**
- Document → Transaction (underlag)
- Document → Contract (kontraktsdokument)
- Document → Workflow (processunderlag)
**Ekonomimodulen-mappning:**
`supplier_invoices.document_id → Document.id`
`expense_reports.receipt_document_ids → Document.id[]`
---
### Objekt 10 — Transaction
> Ekonomisk händelse. `JournalEntry` är en Transaction med `transaction_type = journal_entry`.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `transaction_type` | `enum` | `journal_entry`, `payment`, `invoice`, `credit_note`, `accrual`, `depreciation`, `transfer` |
| `amount` | `numeric(18,4)` | Absolutbelopp |
| `currency_code` | `char(3)` | ISO 4217 |
| `base_currency_amount` | `numeric(18,4)\|null` | Omräknat till baskvaluta |
| `exchange_rate` | `numeric(12,8)\|null` | |
| `status` | `enum` | `draft`, `posted`, `voided`, `pending_approval` |
| `transaction_date` | `date` | Transaktionsdatum (affärsdatum) |
| `value_date` | `date\|null` | Valuteringsdatum |
| `reference` | `text\|null` | Externt referensnummer |
| `counterparty_id` | `UUID\|null` | → Organization.id eller Person.id |
| `project_id` | `UUID\|null` | → Project.id |
| `document_id` | `UUID\|null` | → Document.id (underlag) |
| `lines` | `jsonb` | Transaktionsrader med konton och belopp |
**Relationer:**
- Transaction → Organization (motpart)
- Transaction → Person (utförare)
- Transaction → Document (verifikat)
- Transaction → Project (kostnadsbärare)
- Transaction → Asset (anläggningstillgångsrörelser)
**Ekonomimodulen-mappning:**
`journal_entries` är Transactions med `transaction_type = journal_entry`.
Ledger Engine håller transaktionsrader i `lines`-jsonb eller separata rader beroende på volym.
---
### Objekt 11 — Event
> Systemevent, domänevent. Detta är Hermes Event Fabric-payloaden.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `event_type` | `text` | Namnrymd: `aamos.ekonomi.transaction.posted` |
| `event_version` | `text` | Semver av event-schemat |
| `source_service` | `text` | Originerande tjänst |
| `correlation_id` | `UUID` | Spårnings-ID genom systemet |
| `causation_id` | `UUID\|null` | ID på det event som orsakade detta |
| `subject_type` | `text` | Typ av ämnesobjekt |
| `subject_id` | `UUID` | ID på ämnesobjektet |
| `payload` | `jsonb` | Eventnyttolast |
| `published_at` | `timestamptz` | Publiceringstidpunkt |
**Namnrymdskonvention:**
`aamos.<modul>.<objekt>.<verb>` — t.ex. `aamos.ekonomi.transaction.posted`, `aamos.crm.contact.created`
**Relationer:**
- Event refererar till vilket som helst kanoniskt objekt via `subject_id`
- Events persisteras i JSONL-fallback för replay
---
### Objekt 12 — Contract
> Avtal, SLA, leverantörsavtal, anställningsavtal, ramavtal, licens.
| Fält | Typ | Beskrivning |
|------|-----|-------------|
| `title` | `text` | |
| `contract_type` | `enum` | `supplier`, `customer`, `employment`, `sla`, `license`, `framework`, `nda` |
| `status` | `enum` | `draft`, `pending_signature`, `active`, `expired`, `terminated`, `renewed` |
| `counterparty_org_id` | `UUID\|null` | → Organization.id |
| `counterparty_person_id` | `UUID\|null` | → Person.id |
| `owner_org_id` | `UUID` | → Organization.id (intern part) |
| `start_date` | `date\|null` | |
| `end_date` | `date\|null` | |
| `auto_renewal` | `boolean` | |
| `value` | `numeric(18,4)\|null` | Kontraktsvärde |
| `currency_code` | `char(3)\|null` | |
| `document_id` | `UUID\|null` | → Document.id (signerat PDF) |
| `terms` | `jsonb\|null` | Strukturerade avtalsvillkor |
**Relationer:**
- Contract → Organization (parter)
- Contract → Person (signatärer)
- Contract → Document (kontraktsdokument)
- Contract → Transaction (betalningsplan, fakturor)
- Contract → Asset (leasade tillgångar)
---
## Ekonomimodulens fullständiga mappning
```
Ekonomimodul → Kanonisk typ
──────────────────────────────────────────
JournalEntry → Transaction (type=journal_entry)
Supplier → Organization (org_type=company)
Customer → Organization (org_type=company)
SupplierInvoice → Document (type=invoice) + Transaction
ExpenseReport → Document (type=report) + Transaction[]
FixedAsset → Asset (type=fixed_asset)
CostCenter → Project (type=overhead/internal)
Employee → Person (type=employee)
ApprovalFlow → Workflow
Receipt → Document (type=receipt)
SupplierContract → Contract (type=supplier)
```
---
## Konsekvenser
### Positiva
- Ny modul definierar bara sina egna **extensions** i `metadata`, inte nya bastyper
- AI-kontextfönster kan laddas med enhetliga objektrepresentationer
- Korsmodulär sökning och audit fungerar utan adaptrar
- Event-routing är självbeskrivande via `subject_type`
### Negativa / risker
- **Migrationsarbete:** Ekonomimodulen måste verifiera att befintliga tabeller uppfyller kontraktet
- **UUID v7 kräver** att databasen kan generera dem (`pgcrypto` eller app-sida)
- `metadata jsonb` kan bli en dumpingsplats — kräver disciplin och review
---
## Implementation
### Fas 1 (Ekonomimodulen, nu)
1. Verifiera att alla tabeller har `tenant_id`, `version`, `deleted_at`
2. Lägg till `metadata jsonb DEFAULT '{}'` på tabeller som saknar det
3. Dokumentera mappning i modulens README
### Fas 2 (inför CRM)
1. Skapa dedikerade tabeller för Organization och Person i `core`-schema
2. Ekonomimodulen FK:ar till `core.organizations` och `core.persons`
### Fas 3 (plattformsnivå)
1. GraphQL/REST-lager för kanoniska objekt
2. Unified search-index över alla objekt
3. AI-kontext-builder som hämtar kanonisk representation
---
## Öppna frågor
1. **Ska kanoniska objekt bo i ett delat `core`-schema eller replikeras per modul?**
Förslag: Shared schema för Person, Organization, Location. Replika med CDC för moduler som behöver hög läsprestanda.
2. **UUID v7 vs ULID?**
UUID v7 är nu RFC-standard och stöds bättre i PostgreSQL-ekosystemet. ULID är alternativ om befintlig kod är svår att migrera.
3. **`metadata`-schemavalidering?**
Modul-specifika JSON Schema-fragment bör definieras och valideras vid INSERT/UPDATE.
4. **Event-schema-registret?**
Hermes bör ha ett schema-register som validerar event-payloads mot `event_version`. Se ARC-002.