fix(bokslut): remove uppskjuten skatt on obeskattade reserver in juridisk person (K3 29.37) (#1421)

* fix(bokslut): remove uppskjuten skatt on obeskattade reserver in juridisk person (K3 29.37) and confirm K3 to K2 reversion

In juridisk person K3 29.37 keeps obeskattade reserver at gross; the 79.4/20.6 split belongs to koncernredovisning. The old disposition double-counted the tax portion (result charged twice, 2240 overstated on top of gross 21xx). Removes the proposal step, POST kind, UI case, K2-to-K3 account seeding and the interim framework gate; keeps LATENT_TAX_DEFAULT_RATE for analytical soliditet presentation. Also adds the K3-to-K2 consequence confirmation dialog in settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(decisions): scope the batch log to shipped code and record the 29.37 election nuance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(settings): stop promising deferred-tax accounting the engine no longer does

The framework help text and the K2-to-K3 confirmation both told the user that switching to K3 means uppskjuten skatt is recognised separately on 2240/8940 with a 79.4/20.6 split. This PR removes exactly that behaviour, so the copy would have promised something the product does not do, which is the defect class this batch exists to remove. Both now describe what actually happens: kassaflodesanalys, komponentavskrivning and a wider note set, with obeskattade reserver carried gross per K3 29.37.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-06 10:05:15 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 8f38baca05
commit 12c59399ee
12 changed files with 132 additions and 523 deletions
@@ -10,7 +10,6 @@ import { createMockRequest, parseJsonResponse } from '@/tests/helpers'
vi.mock('@/lib/bokslut/dispositions-proposal-builder', () => ({
buildDispositionsProposal: vi.fn(),
buildLatentTaxProposal: vi.fn(),
}))
vi.mock('@/lib/bokslut/tax-provision/tax-adjustment-service', () => ({
@@ -107,6 +106,44 @@ function periodClient(period: unknown, error: unknown = null) {
return { from: vi.fn().mockReturnValue(builder) }
}
/** Like periodClient but also answers a companies.accounting_framework
* lookup. Pass null to simulate a missing company row. */
function frameworkClient(period: unknown, framework: string | null) {
const periodBuilder = {
select: vi.fn(),
eq: vi.fn(),
single: vi.fn().mockResolvedValue({ data: period, error: null }),
}
periodBuilder.select.mockReturnValue(periodBuilder)
periodBuilder.eq.mockReturnValue(periodBuilder)
const companyBuilder = {
select: vi.fn(),
eq: vi.fn(),
maybeSingle: vi.fn().mockResolvedValue({
data: framework === null ? null : { accounting_framework: framework },
error: null,
}),
}
companyBuilder.select.mockReturnValue(companyBuilder)
companyBuilder.eq.mockReturnValue(companyBuilder)
return {
from: vi.fn((table: string) =>
table === 'companies' ? companyBuilder : periodBuilder,
),
}
}
const openPeriod = {
id: 'period-1',
name: '2025',
period_start: '2025-01-01',
period_end: '2025-12-31',
opening_balance_entry_id: null,
is_closed: false,
locked_at: null,
closing_entry_id: null,
}
beforeEach(() => {
vi.clearAllMocks()
requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase: {}, error: null })
@@ -625,4 +662,22 @@ describe('POST /api/bookkeeping/fiscal-periods/[id]/bokslutsdispositioner', () =
expect(body.error.details).toEqual({ bookedAmount: 123_181, expectedAmount: 123_180 })
expect(createJournalEntry).not.toHaveBeenCalled()
})
it('rejects the removed uppskjuten_skatt kind as a validation error (K3 29.37)', async () => {
// The kind was removed 2026-08-05: in juridisk person obeskattade
// reserver stay at gross (K3 29.37), so no deferred-tax disposition
// exists for ANY framework. Old clients sending it get schema 400.
requireAuthMock.mockResolvedValue({
user: { id: 'user-1' },
supabase: frameworkClient(openPeriod, 'k3'),
error: null,
})
const { status } = await parseJsonResponse<{ error: { code: string } }>(
await post({ items: [{ kind: 'uppskjuten_skatt' }] }),
)
expect(status).toBe(400)
expect(createJournalEntry).not.toHaveBeenCalled()
})
})
@@ -26,10 +26,7 @@ import { proposeOveravskrivningar } from '@/lib/bokslut/reserves/overavskrivning
import { calculateOveravskrivningar } from '@/lib/bokslut/reserves/overavskrivningar-calculator'
import { generateIncomeStatement } from '@/lib/reports/income-statement'
import { roundOre } from '@/lib/money'
import {
buildDispositionsProposal,
buildLatentTaxProposal,
} from '@/lib/bokslut/dispositions-proposal-builder'
import { buildDispositionsProposal } from '@/lib/bokslut/dispositions-proposal-builder'
import type { ProposedDisposition } from '@/lib/bokslut/types'
import type { JournalEntry } from '@/types'
@@ -55,9 +52,6 @@ const DISPOSITION_ORDER: Record<string, number> = {
sarskild_loneskatt: 2,
periodiseringsfond_avsattning: 3,
bolagsskatt: 4,
// K3 only: posts last because it depends on the closing 21xx balance,
// which only stabilises once avsättning / återföring have been applied.
uppskjuten_skatt: 5,
}
// ============================================================
@@ -174,11 +168,6 @@ const ItemSchema = z.discriminatedUnion('kind', [
.enum(['machinery_equipment', 'building', 'immaterial', 'group'])
.optional(),
}),
// K3 only: uppskjuten skatt provision. Server recomputes the amount from
// current 2240 + 21xx state so the client cannot override it.
z.object({
kind: z.literal('uppskjuten_skatt'),
}),
])
const PostBodySchema = z.object({
@@ -498,15 +487,6 @@ async function computeProposal(
computation: calculation.proposal.computation,
})
}
case 'uppskjuten_skatt':
// Server-only: recompute from current TB (which already reflects any
// 21xx postings that committed earlier in this batch). The client
// sends no amount: the calculator owns the K3 split.
return buildLatentTaxProposal({
supabase,
companyId,
fiscalPeriodId,
})
}
}