# Part 2: Period Management & Year-End Closing ## Overview Part 2 implements **year-end closing (årsbokslut)** — the process that legally closes a fiscal year per Bokföringslagen. This includes period locking, closing entry generation, opening balance propagation, and the API surface to drive the workflow. **Depends on Part 1:** immutable ledger, audit trail, tax codes, document archive, period lock enforcement, retention protection. --- ## What Was Built ### Migration 19: Period Closing Metadata **File:** `supabase/migrations/20240101000019_period_closing.sql` Three new columns on `fiscal_periods`: | Column | Type | Purpose | |--------|------|---------| | `closing_entry_id` | `uuid FK → journal_entries` | Links to the year-end closing journal entry | | `opening_balance_entry_id` | `uuid FK → journal_entries` | Links to the opening balance entry in this period | | `previous_period_id` | `uuid FK → fiscal_periods` | Chain link to the prior period for validation | One new trigger: - **`enforce_opening_balance_immutability`** — Once `opening_balance_entry_id` or `closing_entry_id` are set, they cannot be changed. This prevents tampering with the closing chain after the fact. --- ### TypeScript Types **File:** `types/index.ts` Extended `FiscalPeriod` with the three new nullable fields. New interfaces: | Interface | Purpose | |-----------|---------| | `YearEndValidation` | Result of readiness check: `ready`, `errors[]`, `warnings[]`, `draftCount`, `voucherGaps[]`, `trialBalanceBalanced` | | `YearEndPreview` | Preview of closing: `netResult`, `closingAccount` (2099/2010), `closingLines[]`, `resultAccountSummary[]` | | `YearEndResult` | Result of execution: `closingEntry`, `nextPeriod`, `openingBalanceEntry` | | `PeriodStatus` | Status summary: lock/close/draft/opening state | --- ### Period Service **File:** `lib/core/bookkeeping/period-service.ts` | Function | What it does | |----------|-------------| | `lockPeriod(userId, fiscalPeriodId)` | Sets `locked_at = now()`. Validates period exists, belongs to user, isn't already locked/closed. After locking, the `enforce_period_lock` trigger (from Part 1) blocks new journal entries. | | `closePeriod(userId, fiscalPeriodId)` | Sets `is_closed = true, closed_at = now()`. Requires: already locked AND `closing_entry_id` is set. This is the final, permanent state. | | `createNextPeriod(userId, currentPeriodId)` | Creates the next fiscal year. Computes dates from the current period's length to handle **brutet räkenskapsår** (broken fiscal years, e.g. Jul–Jun). Sets `previous_period_id` for chain validation. Auto-generates name like "FY 2025" or "FY 2025/2026". | | `getPeriodStatus(userId, fiscalPeriodId)` | Returns a summary: `is_locked`, `is_closed`, `has_closing_entry`, `has_opening_balances`, `draft_count`, `next_period_exists`. | --- ### Year-End Service **File:** `lib/core/bookkeeping/year-end-service.ts` This is the core new logic. #### `validateYearEndReadiness(userId, fiscalPeriodId)` → `YearEndValidation` Checks preconditions before allowing year-end closing: - **Blocking errors** (prevent closing): - Period already closed - Closing entry already exists - Draft journal entries exist (must be posted or deleted) - Trial balance is not balanced - **Warnings** (informational): - Voucher number gaps detected (via `detect_voucher_gaps()` SQL function) - No posted entries in the period #### `previewYearEndClosing(userId, fiscalPeriodId)` → `YearEndPreview` Generates a preview without persisting anything: 1. Looks up `entity_type` from `company_settings` → determines closing account: - **Aktiebolag (AB):** account `2099` (Årets resultat) - **Enskild firma (EF):** account `2010` (Eget kapital) 2. Runs income statement to get `net_result` 3. Gets trial balance, filters to class 3–8 accounts 4. For each account with a non-zero balance: creates a line that zeros it 5. Adds a final balancing line to the closing account (2099/2010) 6. Returns the preview with all lines and a summary of result accounts #### `executeYearEndClosing(userId, fiscalPeriodId)` → `YearEndResult` Full orchestration (the main entry point): ``` 1. validateYearEndReadiness() → abort if errors 2. previewYearEndClosing() → get closing lines 3. createJournalEntry() → create closing entry (source_type: 'year_end') 4. UPDATE fiscal_periods → set closing_entry_id 5. lockPeriod() → lock the period 6. closePeriod() → permanently close 7. createNextPeriod() → create next fiscal year 8. generateOpeningBalances() → carry forward class 1-2 balances 9. Return { closingEntry, nextPeriod, openingBalanceEntry } ``` #### `generateOpeningBalances(userId, closedPeriodId, nextPeriodId)` → `JournalEntry` Creates opening balance entries in the new period: 1. Gets trial balance of the closed period (after closing entry) 2. Filters to balance sheet accounts (class 1–2) with non-zero closing balance 3. Creates a journal entry with `source_type: 'opening_balance'`: - Debit accounts get debit opening, credit accounts get credit opening 4. Verifies the entry is balanced (total debit = total credit) 5. Sets `opening_balance_entry_id` and `opening_balances_set = true` on the next period **Key invariant:** UB (utgående balans) of year N == IB (ingående balans) of year N+1. --- ### API Routes All follow the existing pattern: authenticate via Supabase, delegate to service, return JSON. | Method | Path | Handler | |--------|------|---------| | `POST` | `/api/bookkeeping/fiscal-periods/[id]/lock` | `lockPeriod()` | | `GET` | `/api/bookkeeping/fiscal-periods/[id]/year-end` | `validateYearEndReadiness()` + `previewYearEndClosing()` | | `POST` | `/api/bookkeeping/fiscal-periods/[id]/year-end` | `executeYearEndClosing()` | | `POST` | `/api/bookkeeping/fiscal-periods/[id]/close` | `closePeriod()` | --- ## Reused Components | Component | From | Used by | |-----------|------|---------| | `generateTrialBalance()` | `lib/reports/trial-balance.ts` | Balance aggregation for closing + opening entries | | `generateIncomeStatement()` | `lib/reports/income-statement.ts` | Net result calculation | | `createJournalEntry()` | `lib/bookkeeping/engine.ts` | Creating closing + opening entries (auto-posts) | | `validateBalance()` | `lib/bookkeeping/engine.ts` | Pre-flight balance check | | `detect_voucher_gaps()` | Migration 16 SQL function | Gap validation during readiness check | | `enforce_period_lock` trigger | Migration 17 | Blocks writes after locking | | `enforce_journal_entry_immutability` trigger | Migration 17 | Protects closing/opening entries after posting | --- ## Period Lifecycle Diagram ``` ┌─────────┐ │ OPEN │ ← Journal entries can be posted └────┬────┘ │ lockPeriod() ▼ ┌─────────┐ │ LOCKED │ ← No new entries (enforce_period_lock trigger) └────┬────┘ │ closePeriod() (requires closing_entry_id) ▼ ┌─────────┐ │ CLOSED │ ← Permanent, immutable └─────────┘ ``` The `executeYearEndClosing()` function drives the full flow from OPEN → CLOSED in one call, including creating the closing entry, locking, closing, creating the next period, and generating opening balances. --- ## Verification Checklist - [x] `npx tsc --noEmit` — zero TypeScript errors - [ ] Migration 19 applies cleanly (`\d fiscal_periods` shows new columns) - [ ] `GET /api/bookkeeping/fiscal-periods/[id]/year-end` returns preview with net result - [ ] `POST /api/bookkeeping/fiscal-periods/[id]/year-end` creates closing entry, locks, closes, creates next period, generates opening balances - [ ] Closing entry zeros all class 3–8 accounts - [ ] Opening balance entry in next period matches UB of closed period (class 1–2 only) - [ ] Closed period rejects new journal entries (period lock trigger) - [ ] Period with draft entries → validation fails with blocking error - [ ] Period already closed → validation fails - [ ] EF entity type → closing goes to 2010 (not 2099) --- ## Files Changed/Created | File | Action | |------|--------| | `supabase/migrations/20240101000019_period_closing.sql` | **Created** — 3 ALTER columns + 1 trigger | | `types/index.ts` | **Modified** — extended FiscalPeriod, added 4 new interfaces | | `lib/core/bookkeeping/period-service.ts` | **Created** — lockPeriod, closePeriod, createNextPeriod, getPeriodStatus | | `lib/core/bookkeeping/year-end-service.ts` | **Created** — validateYearEndReadiness, previewYearEndClosing, executeYearEndClosing, generateOpeningBalances | | `app/api/bookkeeping/fiscal-periods/[id]/lock/route.ts` | **Created** — POST lock endpoint | | `app/api/bookkeeping/fiscal-periods/[id]/year-end/route.ts` | **Created** — GET preview + POST execute | | `app/api/bookkeeping/fiscal-periods/[id]/close/route.ts` | **Created** — POST close endpoint |