# 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...` — 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.