Supp/verifikationer inconsitency (#369)
* feat(bookkeeping): implement reset bookkeeping functionality with safeguards * feat(migrations): restore relaxed trigger for retroactive first fiscal year
This commit is contained in:
@@ -4,6 +4,7 @@ import { requireCompanyId } from '@/lib/company/context'
|
||||
import { requireWritePermission } from '@/lib/auth/require-write'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import { eventBus } from '@/lib/events/bus'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
@@ -59,7 +60,7 @@ export async function DELETE(
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ error: getErrorMessage(error, { context: 'journal_entry', statusCode: 400 }) },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,6 +88,30 @@ const ERROR_PATTERN_MAP: [RegExp, string | null][] = [
|
||||
/Entry date .+ is outside fiscal period/i,
|
||||
'Datumet ligger utanför det valda räkenskapsåret.',
|
||||
],
|
||||
[
|
||||
/Only company owners and admins can delete vouchers/i,
|
||||
'Endast ägare och administratörer kan radera verifikationer.',
|
||||
],
|
||||
[
|
||||
/Journal entry not found/i,
|
||||
'Verifikationen kunde inte hittas.',
|
||||
],
|
||||
[
|
||||
/Only posted entries can be deleted/i,
|
||||
'Endast bokförda verifikationer kan raderas.',
|
||||
],
|
||||
[
|
||||
/Cannot delete voucher in a closed fiscal period/i,
|
||||
'Verifikationen kan inte raderas — räkenskapsåret är stängt.',
|
||||
],
|
||||
[
|
||||
/Cannot delete voucher in a locked fiscal period/i,
|
||||
'Verifikationen kan inte raderas — perioden är låst.',
|
||||
],
|
||||
[
|
||||
/Cannot delete: other entries reference this voucher/i,
|
||||
'Verifikationen kan inte raderas eftersom andra verifikationer (t.ex. storno eller rättelse) refererar till den.',
|
||||
],
|
||||
[
|
||||
/timed out after \d+m?s/i,
|
||||
'Anslutningen mot tjänsten tog för lång tid. Försök igen.',
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getPool } from '@/tests/pg/setup'
|
||||
import { seedCompany } from '@/tests/pg/fixtures'
|
||||
|
||||
// Covers enforce_first_of_month_for_subsequent_periods (migration
|
||||
// 20260418120000). The trigger only blocks mid-month period_start when a
|
||||
// strictly earlier period exists — so importing a company's chronologically
|
||||
// first fiscal year (förlängt första räkenskapsår) via SIE must succeed even
|
||||
// after a later period was created during onboarding.
|
||||
describe('fiscal_periods: subsequent-period start-day trigger', () => {
|
||||
async function insertPeriod(
|
||||
companyId: string,
|
||||
name: string,
|
||||
periodStart: string,
|
||||
periodEnd: string,
|
||||
) {
|
||||
return getPool().query(
|
||||
`INSERT INTO public.fiscal_periods
|
||||
(company_id, name, period_start, period_end, is_closed, opening_balances_set)
|
||||
VALUES ($1, $2, $3, $4, false, false)
|
||||
RETURNING id`,
|
||||
[companyId, name, periodStart, periodEnd],
|
||||
)
|
||||
}
|
||||
|
||||
it('allows a mid-month start when no earlier period exists', async () => {
|
||||
const { companyId } = await seedCompany()
|
||||
|
||||
const { rows } = await insertPeriod(
|
||||
companyId,
|
||||
'Räkenskapsår 2025',
|
||||
'2025-06-15',
|
||||
'2026-06-30',
|
||||
)
|
||||
expect(rows[0]!.id).toBeTruthy()
|
||||
})
|
||||
|
||||
it('allows importing an earlier mid-month period after a later day-1 period exists', async () => {
|
||||
const { companyId } = await seedCompany()
|
||||
|
||||
// Onboarding-created period (day 1, year N).
|
||||
await insertPeriod(companyId, 'Räkenskapsår 2025', '2025-01-01', '2025-12-31')
|
||||
|
||||
// SIE import of förlängt första räkenskapsår — earlier in time,
|
||||
// mid-month start. This used to fail with the old trigger.
|
||||
const { rows } = await insertPeriod(
|
||||
companyId,
|
||||
'Räkenskapsår 2023/2024',
|
||||
'2023-06-15',
|
||||
'2024-12-31',
|
||||
)
|
||||
expect(rows[0]!.id).toBeTruthy()
|
||||
})
|
||||
|
||||
it('still rejects mid-month start when a strictly earlier period already exists', async () => {
|
||||
const { companyId } = await seedCompany()
|
||||
|
||||
await insertPeriod(companyId, 'Räkenskapsår 2024', '2024-01-01', '2024-12-31')
|
||||
|
||||
await expect(
|
||||
insertPeriod(companyId, 'Räkenskapsår 2025 (bad)', '2025-06-15', '2026-06-30'),
|
||||
).rejects.toThrow(/Non-first fiscal period must start on the 1st of a month/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Restore retroactive first fiscal year fix.
|
||||
--
|
||||
-- Migration 20260418120000 (applied to prod as 20260420092940
|
||||
-- "allow_retroactive_first_fiscal_year") relaxed the trigger so that a
|
||||
-- mid-month period_start is allowed when no existing period starts earlier.
|
||||
--
|
||||
-- However, on prod the migration "sie_files_and_fiscal_period_sync" was
|
||||
-- recorded with version 20260421194554 — *after* the fix — and its body
|
||||
-- contains a CREATE OR REPLACE FUNCTION with the strict pre-fix logic,
|
||||
-- which clobbered the relaxed trigger. As a result the SIE import flow
|
||||
-- for förlängt första räkenskapsår started failing again on prod.
|
||||
--
|
||||
-- This migration re-applies the relaxed trigger so that mid-month start
|
||||
-- is allowed iff no existing period starts earlier.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.enforce_first_of_month_for_subsequent_periods()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXTRACT(DAY FROM NEW.period_start) = 1 THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public.fiscal_periods
|
||||
WHERE company_id = NEW.company_id
|
||||
AND id IS DISTINCT FROM NEW.id
|
||||
AND period_start < NEW.period_start
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Non-first fiscal period must start on the 1st of a month';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user