From cc41ae0f1d7d64a8e778777cea61fc00cfc9cab7 Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:12:33 +0200 Subject: [PATCH] Supp/04 27 (#361) * fix: update fiscal period validation to account for locked periods * fix: restrict receipt alerts and visibility to development environment --- .../fiscal-periods/__tests__/route.test.ts | 45 ++++++++++++++++--- app/api/bookkeeping/fiscal-periods/route.ts | 8 +++- components/dashboard/DashboardContent.tsx | 2 +- components/dashboard/DashboardNav.tsx | 2 +- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/app/api/bookkeeping/fiscal-periods/__tests__/route.test.ts b/app/api/bookkeeping/fiscal-periods/__tests__/route.test.ts index 6a3ec131..7e44f4c6 100644 --- a/app/api/bookkeeping/fiscal-periods/__tests__/route.test.ts +++ b/app/api/bookkeeping/fiscal-periods/__tests__/route.test.ts @@ -65,10 +65,12 @@ function buildMockSupabase(options: { chainable.select = vi.fn().mockImplementation((_sel: string, opts?: { count?: string }) => { if (opts?.count === 'exact') { - // openCount query + // openCount query: .eq(company_id).eq(is_closed=false).is(locked_at, null) return { eq: vi.fn().mockReturnValue({ - eq: vi.fn().mockResolvedValue({ count: openCount }), + eq: vi.fn().mockReturnValue({ + is: vi.fn().mockResolvedValue({ count: openCount }), + }), }), } } @@ -160,7 +162,7 @@ describe('POST /api/bookkeeping/fiscal-periods', () => { expect(body.error).toMatch(/must start on 2026-01-01/) }) - it('rejects forward period when unclosed period exists', async () => { + it('rejects forward period when an unlocked open period exists', async () => { buildMockSupabase({ allPeriods: [{ id: 'p1', period_start: '2025-01-01', period_end: '2025-12-31', is_closed: false }], openCount: 1, @@ -169,7 +171,26 @@ describe('POST /api/bookkeeping/fiscal-periods', () => { const res = await POST(req) expect(res.status).toBe(409) const body = await res.json() - expect(body.error).toMatch(/unclosed period/) + expect(body.error).toMatch(/unlocked period/) + }) + + // Regression: BFL 6 kap allows löpande bokföring of the new year in parallel + // with bokslut work on the prior year (6-month deadline for årsbokslut, 7 + // months for AB årsredovisning). A locked-but-not-yet-closed prior period is + // the normal state during that window and must not block creation of the + // next räkenskapsår. The .is('locked_at', null) filter excludes locked + // periods from the openCount, so the mock returns 0 here. + it('allows forward period creation when prior period is locked-but-not-closed', async () => { + buildMockSupabase({ + allPeriods: [{ id: 'p1', period_start: '2024-01-01', period_end: '2024-12-31', is_closed: false }], + openCount: 0, + overlapping: [], + }) + const req = createMockRequest({ name: 'FY 2025', period_start: '2025-01-01', period_end: '2025-12-31' }) + const res = await POST(req) + expect(res.status).toBe(200) + const body = await res.json() + expect(body.data).toBeDefined() }) it('allows backward period creation', async () => { @@ -256,7 +277,13 @@ describe('POST /api/bookkeeping/fiscal-periods', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any select: vi.fn().mockImplementation((_sel: string, opts?: any) => { if (opts?.count === 'exact') { - return { eq: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ count: 0 }) }) } + return { + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + is: vi.fn().mockResolvedValue({ count: 0 }), + }), + }), + } } if (callNum === 1) { return { @@ -313,7 +340,13 @@ describe('POST /api/bookkeeping/fiscal-periods', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any select: vi.fn().mockImplementation((_sel: string, opts?: any) => { if (opts?.count === 'exact') { - return { eq: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ count: 0 }) }) } + return { + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + is: vi.fn().mockResolvedValue({ count: 0 }), + }), + }), + } } if (callNum === 1) { return { diff --git a/app/api/bookkeeping/fiscal-periods/route.ts b/app/api/bookkeeping/fiscal-periods/route.ts index d31254f6..ef3af8c8 100644 --- a/app/api/bookkeeping/fiscal-periods/route.ts +++ b/app/api/bookkeeping/fiscal-periods/route.ts @@ -100,16 +100,20 @@ export async function POST(request: Request) { ) } - // Enforce: max one unclosed period (no skipping ahead) — forward only + // Enforce: max one editable prior period (no skipping ahead) — forward only. + // Locked periods are write-blocked by enforce_period_lock and so don't + // represent skipping ahead; they're the normal state during bokslut work, + // which BFL 6 kap allows in parallel with löpande bokföring of the new year. const { count: openCount } = await supabase .from('fiscal_periods') .select('id', { count: 'exact', head: true }) .eq('company_id', companyId) .eq('is_closed', false) + .is('locked_at', null) if (openCount && openCount > 0) { return NextResponse.json( - { error: 'Cannot create a new period while an unclosed period exists' }, + { error: 'Cannot create a new period while an unlocked period exists' }, { status: 409 } ) } diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx index 28a1cd4d..2cecfd35 100644 --- a/components/dashboard/DashboardContent.tsx +++ b/components/dashboard/DashboardContent.tsx @@ -163,7 +163,7 @@ export default function DashboardContent({ firstName, companyId, settings, summa ) } - if (summary.receiptQueue && (summary.receiptQueue.pending_review_count > 0 || summary.receiptQueue.unmatched_receipts_count > 0)) { + if (process.env.NODE_ENV === 'development' && summary.receiptQueue && (summary.receiptQueue.pending_review_count > 0 || summary.receiptQueue.unmatched_receipts_count > 0)) { alertItems.push( diff --git a/components/dashboard/DashboardNav.tsx b/components/dashboard/DashboardNav.tsx index a4f6fc8f..dbe481f6 100644 --- a/components/dashboard/DashboardNav.tsx +++ b/components/dashboard/DashboardNav.tsx @@ -78,7 +78,7 @@ const navItems: NavItem[] = [ { href: '/supplier-invoices', label: 'Leverantörsfakturor', icon: FileInput, group: 'inköp', hidden: true }, // General accounting { href: '/pending', label: 'Granskning', icon: ClipboardCheck, group: 'redovisning' }, - { href: '/receipts', label: 'Kvitton', icon: Receipt, group: 'redovisning', hidden: !ENABLED_EXTENSION_IDS.has('invoice-inbox'), devBadge: true }, + { href: '/receipts', label: 'Kvitton', icon: Receipt, group: 'redovisning', hidden: !ENABLED_EXTENSION_IDS.has('invoice-inbox') || process.env.NODE_ENV !== 'development', devBadge: true }, { href: '/agent-inbox', label: 'Agent-inkorg', icon: Sparkles, group: 'redovisning', hidden: !ENABLED_EXTENSION_IDS.has('ai-agent') || !isAgentInboxEnabled(), devBadge: true }, { href: '/transactions', label: 'Transaktioner', icon: ArrowLeftRight, group: 'redovisning' }, { href: '/bookkeeping', label: 'Bokföring', icon: BookOpen, group: 'redovisning' },