* feat: separate AR/AP/accounting into distinct nav groups (#92) Split the flat "Finans" sidebar group into three visually distinct sections — Försäljning (AR), Inköp (AP), and Redovisning — so users coming from Fortnox immediately find customer invoicing and supplier invoices as top-level concepts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: journal entry detail view, correction chain, and account name display - Add journal entry detail page at /bookkeeping/[id] with full entry view - Add correction chain API and component showing storno relationships - Add JournalEntryStatusBadge component for entry status display - Show debit/credit account names in template picker and review dialogs - Expand client-side BAS account name mapping with additional accounts - Show account codes on transaction inbox suggestion buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — N+1 query, duplicate name, nav dedup - Batch reverse-lookup into single query per BFS iteration (was N+1) - Differentiate account 2393 from 2893 in display names - Extract shared loop for desktop/mobile nav group rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: counterparty templates, Skatteverket extension, VAT form completeness, and UI cleanup - Add counterparty-based categorization templates (learned from user approvals and auto-ingestion) with fuzzy matching in the mapping engine - Add Skatteverket extension for direct VAT declaration submission via API - Complete VAT declaration form with all 30 SKV 4700 boxes (ruta 08, 35-42, 50, 60-62) - Fix ruta 49 formula to include import VAT (ruta 60+61+62) - Simplify dashboard UI: remove redundant icons from stat cards, customer cards, invoice list, supplier invoices; use Badge variants consistently - Add SkatteverketPanel component to reports page - Add categorization_templates and skatteverket_tokens migrations - Update tests and helpers for new types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback — VAT detection, migration timestamps, dedup - Fix detectVatTreatment to derive actual rate (12%/6%) from VAT line description instead of hardcoding standard_25 - Rename skatteverket_tokens migration to 20260324120001 to avoid duplicate timestamp with categorization_templates (fixes Supabase deployment failure) - Make refreshAccessToken accept previousRefreshCount param to enforce refresh limit contract at the type level - Fix rate limiter TOCTOU by claiming slot before await - Extract formatRedovisare/formatRedovisningsperiod to shared lib/skatteverket/format.ts — eliminates duplication between mappers.ts and SkatteverketPanel.tsx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { rutorToMomsuppgift, formatRedovisare, formatRedovisningsperiod } from '../lib/mappers'
|
|
import type { VatDeclarationRutor } from '@/types'
|
|
|
|
const emptyRutor: VatDeclarationRutor = {
|
|
ruta05: 0, ruta06: 0, ruta07: 0, ruta08: 0,
|
|
ruta10: 0, ruta11: 0, ruta12: 0,
|
|
ruta20: 0, ruta21: 0, ruta22: 0, ruta23: 0, ruta24: 0,
|
|
ruta30: 0, ruta31: 0, ruta32: 0,
|
|
ruta35: 0, ruta36: 0, ruta37: 0, ruta38: 0,
|
|
ruta39: 0, ruta40: 0, ruta41: 0, ruta42: 0,
|
|
ruta48: 0, ruta49: 0,
|
|
ruta50: 0, ruta60: 0, ruta61: 0, ruta62: 0,
|
|
}
|
|
|
|
describe('rutorToMomsuppgift', () => {
|
|
it('converts typical EF VAT declaration', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta05: 10000,
|
|
ruta10: 2500,
|
|
ruta48: 350,
|
|
ruta49: 2150,
|
|
}
|
|
|
|
const result = rutorToMomsuppgift(rutor)
|
|
|
|
expect(result.momspliktigForsaljning).toBe(10000)
|
|
expect(result.momsForsaljningUtgaendeHog).toBe(2500)
|
|
expect(result.ingaendeMomsAvdrag).toBe(350)
|
|
expect(result.summaMoms).toBe(2150)
|
|
})
|
|
|
|
it('omits zero-value fields except summaMoms', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta05: 5000,
|
|
ruta10: 1250,
|
|
ruta49: 1250,
|
|
}
|
|
|
|
const result = rutorToMomsuppgift(rutor)
|
|
|
|
expect(result.momspliktigForsaljning).toBe(5000)
|
|
expect(result.momsForsaljningUtgaendeHog).toBe(1250)
|
|
expect(result.summaMoms).toBe(1250)
|
|
// Zero fields should not be present
|
|
expect(result.momspliktigaUttag).toBeUndefined()
|
|
expect(result.momsForsaljningUtgaendeMedel).toBeUndefined()
|
|
expect(result.momsForsaljningUtgaendeLag).toBeUndefined()
|
|
expect(result.ingaendeMomsAvdrag).toBeUndefined()
|
|
})
|
|
|
|
it('always includes summaMoms even when zero', () => {
|
|
const result = rutorToMomsuppgift(emptyRutor)
|
|
expect(result.summaMoms).toBe(0)
|
|
})
|
|
|
|
it('maps reverse charge fields correctly', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta21: 5000,
|
|
ruta30: 1250,
|
|
ruta48: 1250,
|
|
ruta49: 0,
|
|
}
|
|
|
|
const result = rutorToMomsuppgift(rutor)
|
|
|
|
expect(result.inkopTjansterEU).toBe(5000)
|
|
expect(result.momsInkopUtgaendeHog).toBe(1250)
|
|
expect(result.ingaendeMomsAvdrag).toBe(1250)
|
|
expect(result.summaMoms).toBe(0)
|
|
})
|
|
|
|
it('maps EU/export and import fields', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta35: 8000,
|
|
ruta36: 12000,
|
|
ruta39: 3000,
|
|
ruta40: 5000,
|
|
ruta50: 2000,
|
|
ruta60: 500,
|
|
ruta49: 500,
|
|
}
|
|
|
|
const result = rutorToMomsuppgift(rutor)
|
|
|
|
expect(result.forsaljningVarorEU).toBe(8000)
|
|
expect(result.forsaljningVarorUtanforEU).toBe(12000)
|
|
expect(result.forsaljningTjansterEU).toBe(3000)
|
|
expect(result.ovrigForsaljningTjansterUtanforSE).toBe(5000)
|
|
expect(result.import).toBe(2000)
|
|
expect(result.momsImportUtgaendeHog).toBe(500)
|
|
})
|
|
})
|
|
|
|
describe('formatRedovisare', () => {
|
|
it('formats aktiebolag org number with 16 prefix', () => {
|
|
expect(formatRedovisare('5020000013', 'aktiebolag')).toBe('165020000013')
|
|
})
|
|
|
|
it('formats aktiebolag org number with hyphen', () => {
|
|
expect(formatRedovisare('502000-0013', 'aktiebolag')).toBe('165020000013')
|
|
})
|
|
|
|
it('formats enskild firma personnummer with 19 prefix for older', () => {
|
|
// Person born in 1985 — 85 > 26 (current year), so prefix 19
|
|
expect(formatRedovisare('8501011234', 'enskild_firma')).toBe('198501011234')
|
|
})
|
|
|
|
it('formats enskild firma personnummer with 20 prefix for younger', () => {
|
|
// Person born in 2005 — 05 < 26 (current year), so prefix 20
|
|
expect(formatRedovisare('0501011234', 'enskild_firma')).toBe('200501011234')
|
|
})
|
|
|
|
it('passes through 12-digit numbers unchanged', () => {
|
|
expect(formatRedovisare('165020000013', 'aktiebolag')).toBe('165020000013')
|
|
})
|
|
|
|
it('throws for invalid length', () => {
|
|
expect(() => formatRedovisare('12345', 'aktiebolag')).toThrow('Ogiltigt organisationsnummer')
|
|
})
|
|
})
|
|
|
|
describe('formatRedovisningsperiod', () => {
|
|
it('formats monthly period as YYYYMM', () => {
|
|
expect(formatRedovisningsperiod('monthly', 2025, 1)).toBe('202501')
|
|
expect(formatRedovisningsperiod('monthly', 2025, 12)).toBe('202512')
|
|
})
|
|
|
|
it('formats quarterly period as last month of quarter', () => {
|
|
expect(formatRedovisningsperiod('quarterly', 2025, 1)).toBe('202503')
|
|
expect(formatRedovisningsperiod('quarterly', 2025, 2)).toBe('202506')
|
|
expect(formatRedovisningsperiod('quarterly', 2025, 3)).toBe('202509')
|
|
expect(formatRedovisningsperiod('quarterly', 2025, 4)).toBe('202512')
|
|
})
|
|
|
|
it('formats yearly period as December', () => {
|
|
expect(formatRedovisningsperiod('yearly', 2025, 1)).toBe('202512')
|
|
})
|
|
})
|