books/backend/BACKEND_REQUIREMENTS.md
Nicolaj Hartmann 66f6fa138d Initial commit: Books accounting system with EventFlow CQRS
Backend (.NET 10):
- EventFlow CQRS/Event Sourcing with PostgreSQL
- GraphQL.NET API with mutations and queries
- Custom ReadModelSqlGenerator for snake_case PostgreSQL columns
- Hangfire for background job processing
- Integration tests with isolated test databases

Frontend (React/Vite):
- Initial project structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 02:52:30 +01:00

9.6 KiB

Backend Krav - Dansk Bogføringssystem

Overblik

Dette dokument beskriver backend-kravene for det danske bogføringssystem. Frontend er implementeret i React/TypeScript med Ant Design og Zustand state management.


1. Regnskabsår (Fiscal Years)

1.1 Data Model

interface FiscalYear {
  id: string;
  companyId: string;
  name: string;                    // "2025" eller "2024/2025"
  startDate: string;               // ISO date "YYYY-MM-DD"
  endDate: string;                 // ISO date "YYYY-MM-DD"
  status: 'open' | 'closed' | 'locked';
  openingBalancePosted: boolean;
  closingDate?: string;            // Når året blev lukket
  closedBy?: string;               // Bruger ID
  createdAt: string;
  updatedAt: string;
}

1.2 API Endpoints

Endpoint Metode Beskrivelse
/api/fiscal-years GET Hent alle regnskabsår for aktiv virksomhed
/api/fiscal-years POST Opret nyt regnskabsår
/api/fiscal-years/:id GET Hent specifikt regnskabsår
/api/fiscal-years/:id PATCH Opdater regnskabsår
/api/fiscal-years/:id/close POST Luk regnskabsår (årsafslutning)
/api/fiscal-years/:id/reopen POST Genåbn lukket regnskabsår
/api/fiscal-years/:id/lock POST Lås regnskabsår permanent

1.3 Forretningsregler

Oprettelse

  • Nye regnskabsår må IKKE overlappe med eksisterende
  • Regnskabsår der "rører" ved grænsen (slutter/starter samme dag) er TILLADT
  • Understøt både kalenderår (jan-dec) og skæve regnskabsår (fx jul-jun)
  • Valider at regnskabsåret er mellem 300-400 dage (advarsel, ikke fejl)

Årsafslutning (Year-End Closing)

KRITISK: Ved årsafslutning skal backend:

  1. Validere at regnskabsåret kan lukkes:

    • Ikke allerede låst
    • Advar om åbne perioder (men tillad lukning)
    • Advar om ikke-afstemte transaktioner
  2. Oprette lukkeposter (closing entries):

    • Luk alle indtægtskonti til resultatoverførselskonto
    • Luk alle udgiftskonti til resultatoverførselskonto
    • Gem som normale transaktioner med særlig markering (isClosingEntry: true)
  3. Generere åbningsbalancer til næste regnskabsår:

    • Beregn slutsaldo for alle balancekonti (aktiver, passiver, egenkapital)
    • Nulstil resultatkonti (indtægter, udgifter, vareforbrug, personale, finansielle)
    • Opret åbningsposteringer i næste regnskabsår (isOpeningBalance: true)
  4. Opdatere status:

    • Sæt regnskabsår status til 'closed' eller 'locked'
    • Luk alle tilhørende regnskabsperioder
    • Sæt openingBalancePosted: true på næste regnskabsår

Genåbning

  • Kun 'closed' regnskabsår kan genåbnes (ikke 'locked')
  • Ved genåbning skal åbningsbalancer i næste år opdateres automatisk
  • Log hvem der genåbnede og hvornår

1.4 Dynamiske Åbningsbalancer

Frontend forventer at åbningsbalancer opdateres automatisk når der bogføres i et tidligere år (som Dinero):

Scenarie:
1. Regnskabsår 2024 er lukket, 2025 er åbent
2. Revisor finder fejl og bogfører korrektion i 2024
3. Backend skal automatisk genberegne åbningsbalancen for 2025

2. Regnskabsperioder (Accounting Periods)

2.1 Data Model

interface AccountingPeriod {
  id: string;
  fiscalYearId: string;
  companyId: string;
  name: string;                    // "Januar 2025", "Q1 2025"
  periodNumber: number;            // 1-12 for månedlig
  startDate: string;
  endDate: string;
  status: 'future' | 'open' | 'closed' | 'locked';
  closedAt?: string;
  closedBy?: string;
  reopenedAt?: string;
  reopenedBy?: string;
  lockedAt?: string;
  lockedBy?: string;
  createdAt: string;
  updatedAt: string;
}

type PeriodFrequency = 'monthly' | 'quarterly' | 'half-yearly' | 'yearly';

2.2 API Endpoints

Endpoint Metode Beskrivelse
/api/periods GET Hent perioder (filtrer på fiscalYearId)
/api/periods/:id PATCH Opdater periode
/api/periods/:id/close POST Luk periode
/api/periods/:id/reopen POST Genåbn periode
/api/periods/:id/lock POST Lås periode

2.3 Forretningsregler

  • Perioder genereres automatisk ved oprettelse af regnskabsår
  • Understøt: månedlig (12), kvartalsvis (4), halvårlig (2), årlig (1)
  • Perioder kan kun lukkes i rækkefølge (periode 2 kræver periode 1 lukket)
  • Låste perioder kan IKKE genåbnes

3. Transaktioner med Periode-Reference

3.1 Udvidet Transaction Model

interface Transaction {
  id: string;
  companyId: string;
  fiscalYearId: string;           // PÅKRÆVET - hvilket regnskabsår
  periodId: string;               // PÅKRÆVET - hvilken periode
  journalEntryNumber: number;
  date: string;
  description: string;
  reference?: string;
  lines: TransactionLine[];
  isVoided: boolean;
  isReconciled: boolean;
  isClosingEntry?: boolean;       // NY - markering for lukkepost
  isOpeningBalance?: boolean;     // NY - markering for åbningsbalance
  createdAt: string;
  updatedAt: string;
  createdBy: string;
}

3.2 Forretningsregler for Bogføring

interface PostingValidation {
  allowed: boolean;
  reason?: string;
  reasonDanish?: string;
}

Backend skal validere ved bogføring:

  1. Find periode for transaktionsdato
  2. Tjek om perioden er åben
  3. Tjek om regnskabsåret er åbent
  4. Returner fejl med dansk besked hvis ikke tilladt

4. Kontoplan (Chart of Accounts)

4.1 Data Model

interface Account {
  id: string;
  companyId: string;
  accountNumber: string;          // "1000", "3900"
  name: string;
  type: AccountType;
  parentId?: string;
  description?: string;
  vatCodeId?: string;
  isActive: boolean;
  isSystemAccount: boolean;       // Kan ikke slettes
  balance: number;                // Beregnet felt
  createdAt: string;
  updatedAt: string;
}

type AccountType =
  | 'asset'        // Aktiver
  | 'liability'    // Passiver
  | 'equity'       // Egenkapital
  | 'revenue'      // Indtægter
  | 'cogs'         // Vareforbrug
  | 'expense'      // Udgifter
  | 'personnel'    // Personaleomkostninger
  | 'financial'    // Finansielle poster
  | 'extraordinary'; // Ekstraordinære poster

4.2 Systemkonti

Følgende konti skal oprettes automatisk:

  • 3900 - Overført resultat (equity) - bruges til årsafslutning
  • Åbningsbalancekonto for primo-posteringer

5. Moms (VAT)

5.1 Data Model

interface VATCode {
  id: string;
  companyId: string;
  code: string;                   // "S25", "K25"
  name: string;
  rate: number;                   // 0.25 for 25%
  type: 'sales' | 'purchase' | 'eu_sales' | 'eu_purchase' | 'reverse_charge';
  accountId: string;              // Momskonto
  isActive: boolean;
}

interface VATPeriod {
  id: string;
  companyId: string;
  startDate: string;
  endDate: string;
  dueDate: string;                // Indberetningsfrist
  status: 'open' | 'submitted' | 'paid';
  salesVAT: number;               // Beregnet
  purchaseVAT: number;            // Beregnet
  netVAT: number;                 // salesVAT - purchaseVAT
  submittedAt?: string;
  paidAt?: string;
}

5.2 SKAT Integration (Fremtidig)

  • Momsindberetning til SKAT via NemVirksomhed API
  • Kvartalsvise eller månedlige momsperioder baseret på virksomhedsstørrelse

6. Virksomhed (Company)

6.1 Data Model

interface Company {
  id: string;
  name: string;
  cvr: string;                    // Dansk CVR-nummer
  address?: string;
  postalCode?: string;
  city?: string;
  country: string;                // Default "DK"
  fiscalYearStart: number;        // Måned 1-12 (default 1 for januar)
  currency: string;               // Default "DKK"
  vatRegistered: boolean;
  vatPeriodFrequency: 'monthly' | 'quarterly' | 'half-yearly';
  createdAt: string;
  updatedAt: string;
}

7. Fejlhåndtering

Alle fejlbeskeder skal være tilgængelige på både engelsk og dansk:

interface APIError {
  code: string;
  message: string;
  messageDanish: string;
  details?: Record<string, unknown>;
}

Eksempler på fejlkoder

Kode Engelsk Dansk
PERIOD_LOCKED Period is locked Perioden er låst
FISCAL_YEAR_LOCKED Fiscal year is locked Regnskabsåret er låst
OVERLAP_EXISTS Overlaps with existing fiscal year Overlapper med eksisterende regnskabsår
UNBALANCED_ENTRY Debit and credit must be equal Debet og kredit skal være ens
NO_COMPANY_SELECTED No company selected Ingen virksomhed valgt

8. Prioriteret Implementeringsrækkefølge

Fase 1: Grundlæggende (MVP)

  1. Company CRUD
  2. Account CRUD med standard kontoplan
  3. Transaction CRUD med validering
  4. FiscalYear CRUD

Fase 2: Periode-management

  1. AccountingPeriod generering og CRUD
  2. Periode-validering ved bogføring
  3. Periode lukning/åbning

Fase 3: Årsafslutning

  1. Closing entries generering og bogføring
  2. Opening balance beregning og bogføring
  3. Dynamisk åbningsbalance-opdatering

Fase 4: Moms

  1. VATCode CRUD
  2. VATPeriod generering
  3. Momsberegning og -rapportering

9. Tekniske Krav

Authentication

  • JWT-baseret authentication
  • Multi-tenant support (bruger kan have adgang til flere virksomheder)

Database

  • PostgreSQL anbefales
  • Soft delete på alle entiteter
  • Audit trail (createdBy, updatedBy, deletedBy)

API Format

  • RESTful JSON API
  • GraphQL som alternativ (valgfrit)
  • Pagination på liste-endpoints
  • Filtering og sorting support

Valuta

  • Alle beløb i øre/cents (integers) for at undgå floating point problemer
  • Frontend konverterer til/fra DKK ved visning

Sidst opdateret: 17. januar 2026