Supp/04 27 (#361)

* fix: update fiscal period validation to account for locked periods

* fix: restrict receipt alerts and visibility to development environment
This commit is contained in:
Mattsson
2026-04-27 12:12:33 +02:00
committed by GitHub
parent 1af977950b
commit cc41ae0f1d
4 changed files with 47 additions and 10 deletions
@@ -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 {
+6 -2
View File
@@ -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 }
)
}
+1 -1
View File
@@ -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(
<Link key="receipts" href="/receipts" className="group">
<Card className="h-full border-primary/30 hover:bg-primary/[0.03] transition-colors">
+1 -1
View File
@@ -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' },