* fix(copy): make K3, leasing and year-end claims match what the code does
Follow-up to the batch that removed the uppskjuten-skatt posting on
obeskattade reserver (K3 29.37 gross in juridisk person) and added the K2
asset-account gate. Six user-facing strings still described the old
behaviour or made claims the code cannot support.
1. Arsredovisning page: the K3 explainer promised an uppskjuten skatt-not
and a materiella anlaggningstillgangar-not in every K3 document. Both
are conditional (a 2240/8940 balance, assets in the register) and the
first is now absent in the normal case. The kassaflodesanalys is
dropped with a warning when it cannot be generated, so it is named
only when the document actually carries one.
2. Regelverk settings: kassaflodesanalys was presented as following from
K3. It follows from being ett storre foretag
(swedish-year-end-closing/references/reporting-and-filing.md:10,
legal-framework.md:42); the copy now says the product includes one and
states the storre-foretag rule separately. Komponentavskrivning was
presented as optional under K3; it is mandatory where component useful
lives differ materially (k2-vs-k3.md:5, asset-accounting
references/depreciation.md:33).
3. Note 1 and the Uppskjutna skatter-not no longer claim the 2240 balance
is hanforlig till obeskattade reserver. deriveLatentTaxMovement reads
the 2240/8940 balances only, and under K3 that account carries deferred
tax on all temporary differences (k2-vs-k3.md:11-13).
4. The deferredTax 'unknown' branch emitted the gross-reserve statement,
which is the denial phrased positively: the same affirmative claim
about books that could not be read. It now emits no deferred-tax
paragraph at all; build-data already warns on that path.
5. Capitalized-lease detection looked at 1260/1269 only. On the shipped
BAS 2026 chart 1260 is a free inventarier account and 1269 is ack.
avskrivningar pa datorer, so owned computers were reported as leased,
while 1217/1227 (finansiellt leasade) were missed. Detection now reads
the company's own account names in kontogrupp 12, which is where BAS
keeps capitalized leases (leasing-and-disposal.md:28) and which owned
inventarier on 1220 never matches. 1720 forutbetalda leasingavgifter
stays out: that is the operational treatment.
6a. gnubok_year_end_readiness listed FX revaluation as a blocker (it is a
warning) and omitted UNBOOKED_TRANSACTIONS, the common one. The
description now names every actionable blocker kind, within the
280-char budget, and a test pins it against YEAR_END_BLOCKER_KIND.
6b. companies.accounting_framework defaults to 'k2', so every enskild
firma hit the K2 asset gate and was handed a BFNAR 2016:10 punkt 10.4
citation plus a K3 remedy it cannot take: a sole trader prepares ett
forenklat arsbokslut, not an arsredovisning (legal-framework.md:29,
:48). entity_type now rides along on the companies read the routes
already do, and non-AB entities get wording with no citation and no
K3, keeping the 1090 remedy. The K1 counterpart of punkt 10.4 is not
sourced in the repo skills, so nothing was invented in its place.
* fix(copy): close the review findings on the copy-truth sweep
Three follow-ups from the source and code reviews. (1) The K2/K3 help text had upgraded a vague sentence into a definite boundary claim ('gransen gar vid <trosklar>'), which excludes the other routes into mandatory K3 that are live right now for this control's audience: noterade vardepapper, and from fiscal years starting after 2025-12-31 also utlandsk filial, kryptotillgangar, aktierelaterade ersattningar and fastighetsbolag. An AB in one of those categories would have read the sentence and stayed on a regelverk it may no longer use. (2) hasCapitalizedLeaseAsset compared per-side cumulative totals, so a lease acquired earlier and disposed this year still claimed the balance sheet carries a leased asset; it now compares the net balance. (3) The K3 warning enumerated a kassaflodesanalys the document may not contain, contradicting the newly conditional page copy on the same screen.
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>
188 lines
8.8 KiB
TypeScript
188 lines
8.8 KiB
TypeScript
/**
|
|
* The K2 gate fires on ANY account the BAS chart flags k2_excluded ("Ej K2"),
|
|
* but those accounts are excluded for different reasons. These tests pin that
|
|
* the rejection text cites BFNAR 2016:10 punkt 10.4 only for the
|
|
* egenupparbetade immateriella group and stays generic everywhere else, so a
|
|
* deferred-tax or fair-value account never gets a wrong legal citation.
|
|
*
|
|
* The second axis is entity_type. companies.accounting_framework is NOT NULL
|
|
* DEFAULT 'k2', so an enskild firma hits the same gate although K2 is the
|
|
* regelverk for an ÅRSREDOVISNING, which it does not prepare
|
|
* (legal-framework.md:29, :48). It may therefore get neither the citation nor
|
|
* a K3 remedy.
|
|
*/
|
|
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
findK2ExcludedAccount,
|
|
k2ExcludedAccountMessages,
|
|
} from '@/lib/bokslut/assets/k2-account-guard'
|
|
import { BAS_REFERENCE, getBASReference } from '@/lib/bookkeeping/bas-reference'
|
|
import type { EntityType } from '@/types'
|
|
|
|
function messagesFor(accountNumber: string, entityType: EntityType | null = 'aktiebolag') {
|
|
const account = getBASReference(accountNumber)
|
|
expect(account, `${accountNumber} missing from the BAS reference`).toBeTruthy()
|
|
return k2ExcludedAccountMessages(account!, entityType)
|
|
}
|
|
|
|
describe('findK2ExcludedAccount', () => {
|
|
it('returns the first Ej K2 account in the list', () => {
|
|
expect(findK2ExcludedAccount(['1030', '1010'])?.account_number).toBe('1010')
|
|
expect(findK2ExcludedAccount(['1010', '1370'])?.account_number).toBe('1010')
|
|
})
|
|
|
|
it('returns null when every account is allowed under K2', () => {
|
|
expect(findK2ExcludedAccount(['1030', '1039'])).toBeNull()
|
|
expect(findK2ExcludedAccount([undefined, '1220', '1229'])).toBeNull()
|
|
})
|
|
|
|
it('treats unknown account numbers as allowed', () => {
|
|
expect(findK2ExcludedAccount(['9999'])).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('k2ExcludedAccountMessages', () => {
|
|
// The boundary is derived from the chart (kontogrupp 10 + k2_excluded), not
|
|
// from a literal list. This asserts the derivation still selects exactly the
|
|
// egenupparbetade accounts, so a future flag change surfaces here.
|
|
const intangibleGroup = BAS_REFERENCE.filter(
|
|
(a) => a.k2_excluded && a.account_class === 1 && a.account_group === '10',
|
|
).map((a) => a.account_number)
|
|
|
|
it('covers exactly the egenupparbetade immateriella accounts', () => {
|
|
expect(intangibleGroup).toEqual(['1010', '1011', '1012', '1018', '1019', '1081'])
|
|
})
|
|
|
|
it.each(['1010', '1011', '1012', '1018', '1019', '1081'])(
|
|
'cites BFNAR 2016:10 punkt 10.4 for %s',
|
|
(accountNumber) => {
|
|
const { message_sv, message_en } = messagesFor(accountNumber)
|
|
expect(message_sv).toContain(accountNumber)
|
|
expect(message_sv).toContain('egenupparbetade utvecklingsutgifter')
|
|
expect(message_sv).toContain('BFNAR 2016:10 punkt 10.4')
|
|
expect(message_en).toContain('internally generated development expenditure')
|
|
expect(message_en).toContain('BFNAR 2016:10 paragraph 10.4')
|
|
},
|
|
)
|
|
|
|
// K2 forbids only EGENUPPARBETADE immateriella tillgångar; an ACQUIRED one
|
|
// is lawful and belongs on 1090 (k2-vs-k3.md:24, "Only acquired intangibles
|
|
// may be recognized"). The rejection has to point there, otherwise the only
|
|
// way out it offers is the one that is wrong.
|
|
it.each(['1010', '1011', '1012', '1018', '1019', '1081'])(
|
|
'points %s at the acquired-intangible account 1090 instead',
|
|
(accountNumber) => {
|
|
const { message_sv, message_en } = messagesFor(accountNumber)
|
|
expect(message_sv).toContain('1090')
|
|
expect(message_sv).toContain('förvärvad immateriell tillgång')
|
|
expect(message_en).toContain('1090')
|
|
expect(message_en).toContain('acquired intangible asset')
|
|
},
|
|
)
|
|
|
|
// Switching regelverk pulls in komponentavskrivning and uppskjuten skatt and
|
|
// rewrites the whole årsredovisning: it is never the remedy for one
|
|
// misdirected account. No message may suggest it, on any Ej K2 account.
|
|
it('never tells the user to change the accounting framework', () => {
|
|
for (const entityType of ['aktiebolag', 'enskild_firma', null] as const) {
|
|
for (const account of BAS_REFERENCE.filter((a) => a.k2_excluded)) {
|
|
const { message_sv, message_en } = k2ExcludedAccountMessages(account, entityType)
|
|
expect(message_sv).not.toContain('Byt regelverk')
|
|
expect(message_sv).not.toContain('Inställningar')
|
|
expect(message_en).not.toContain('Switch the accounting framework')
|
|
expect(message_en).not.toContain('Settings')
|
|
}
|
|
}
|
|
})
|
|
|
|
// The routes resolve accounting_framework from a read whose error they
|
|
// discard, so a transient failure looks like "not K3" even for a K3
|
|
// company. The text must therefore describe the ACCOUNT, never claim which
|
|
// framework this company applies.
|
|
it('never asserts which framework the company applies', () => {
|
|
for (const entityType of ['aktiebolag', 'enskild_firma', null] as const) {
|
|
for (const account of BAS_REFERENCE.filter((a) => a.k2_excluded)) {
|
|
const { message_sv, message_en } = k2ExcludedAccountMessages(account, entityType)
|
|
expect(message_sv).not.toContain('företaget tillämpar')
|
|
expect(message_sv).not.toContain('ert företag')
|
|
expect(message_en).not.toContain('the company applies')
|
|
expect(message_en).not.toContain('your company')
|
|
}
|
|
}
|
|
})
|
|
|
|
it('never cites punkt 10.4 for an Ej K2 account outside kontogrupp 10', () => {
|
|
const others = BAS_REFERENCE.filter((a) => a.k2_excluded).filter(
|
|
(a) => !intangibleGroup.includes(a.account_number),
|
|
)
|
|
// 1370, 1518, 2089, 2092, 2096, 2240, 2448, 3940, 7940, 82xx-84xx, 8940.
|
|
expect(others.length).toBeGreaterThan(0)
|
|
for (const account of others) {
|
|
const { message_sv, message_en } = k2ExcludedAccountMessages(account, 'aktiebolag')
|
|
expect(message_sv).toContain(account.account_number)
|
|
expect(message_sv).toContain('Ej K2')
|
|
expect(message_sv).toContain('K3')
|
|
expect(message_sv).not.toContain('10.4')
|
|
expect(message_sv).not.toContain('egenupparbetade')
|
|
// 1090 is the answer for a misdirected intangible only: proposing it for
|
|
// a deferred-tax or fair-value account would be its own wrong advice.
|
|
expect(message_sv).not.toContain('1090')
|
|
expect(message_en).toContain('Ej K2')
|
|
expect(message_en).not.toContain('10.4')
|
|
expect(message_en).not.toContain('intangible')
|
|
expect(message_en).not.toContain('1090')
|
|
}
|
|
})
|
|
|
|
it('names the account so the user can see what was rejected', () => {
|
|
const { message_sv } = messagesFor('1370')
|
|
expect(message_sv).toContain('1370 (Uppskjuten skattefordran)')
|
|
})
|
|
})
|
|
|
|
describe('k2ExcludedAccountMessages: enskild firma', () => {
|
|
// An enskild firma prepares ett förenklat årsbokslut (K1) or ett årsbokslut
|
|
// (BFNAR 2017:3), never an årsredovisning under BFNAR 2016:10
|
|
// (.claude/skills/swedish-year-end-closing/references/legal-framework.md:29,
|
|
// :48). Citing the K2 regelverk at it is a wrong legal claim, and "apply K3
|
|
// instead" points at a document it does not produce. The K1 counterpart of
|
|
// punkt 10.4 is not sourced in the repo skills, so nothing replaces it: the
|
|
// message states the chart flag, what the system did, and the way forward.
|
|
it.each(['1010', '1011', '1012', '1018', '1019', '1081'])(
|
|
'drops the BFNAR 2016:10 citation on %s but keeps the 1090 remedy',
|
|
(accountNumber) => {
|
|
const { message_sv, message_en } = messagesFor(accountNumber, 'enskild_firma')
|
|
expect(message_sv).toContain(accountNumber)
|
|
expect(message_sv).not.toContain('BFNAR 2016:10')
|
|
expect(message_sv).not.toContain('10.4')
|
|
expect(message_sv).not.toContain('K3')
|
|
expect(message_sv).toContain('Ej K2')
|
|
expect(message_sv).toContain('anläggningsregistret')
|
|
expect(message_sv).toContain('1090')
|
|
expect(message_en).not.toContain('BFNAR 2016:10')
|
|
expect(message_en).not.toContain('K3')
|
|
expect(message_en).toContain('1090')
|
|
},
|
|
)
|
|
|
|
it('drops the "presumes K3" line for the other Ej K2 accounts', () => {
|
|
const { message_sv, message_en } = messagesFor('1370', 'enskild_firma')
|
|
expect(message_sv).toContain('1370 (Uppskjuten skattefordran)')
|
|
expect(message_sv).toContain('Ej K2')
|
|
expect(message_sv).not.toContain('K3')
|
|
// No 1090: that answer belongs to a misdirected intangible only.
|
|
expect(message_sv).not.toContain('1090')
|
|
expect(message_en).not.toContain('K3')
|
|
expect(message_en).not.toContain('1090')
|
|
})
|
|
|
|
// A failed companies read leaves entity_type undefined. An unknown entity
|
|
// may not be handed a legal citation on a guess, so it gets the same
|
|
// neutral wording as an enskild firma.
|
|
it('treats an unknown entity type like an enskild firma', () => {
|
|
const known = messagesFor('1010', 'enskild_firma')
|
|
const unknown = k2ExcludedAccountMessages(getBASReference('1010')!)
|
|
expect(unknown).toEqual(known)
|
|
})
|
|
})
|