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:
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user