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' },