Phase 2a quarantined four defects rather than guessing at Swedish accounting. Each is now resolved against a domain source. KNOWN_BROKEN is empty. Löneutbetalning could never post. It debited 2710 @0.3 + 2920 @0.12 + 7010 @1.0 against a single 1.0 credit, totalling 1.42x the amount, so the balance trigger would reject every entry built from it. Rebuilt per the swedish-payroll skill: Debit 7010 gross, Credit 2710 tax, Credit 1930 net. The 2920 semesterlöneskuld line is gone because vacation accrual is its own verifikat (7290/2920), and a legal_note now says the 30% split is schablon and must be adjusted to the actual skatteavdrag. Periodiseringsfond avsättning/återföring referenced account 2113. Per swedish-year-end-closing the year-tagged block is 2120-2129 (2126 = tax year 2026), so 2113 was the fund for tax year 2013: long since reversed and absent from BAS 2026. Both now use 2110 Periodiseringsfonder, which does not rot annually, with a legal_note pointing at the year-tagged accounts for a company that tracks funds per year. Preliminär F-skatt (EF) turned out to be RIGHT, and the reference was wrong. Account 2012 "Avräkning för skatter och avgifter" was simply missing from lib/bookkeeping/bas-data (the file jumps 2011 -> 2013), while the swedish-year-end-closing skill uses it in two places as an enskild firma equity sub-account. That is not cosmetic: account-backfill.ts only seeds accounts present in BAS_REFERENCE, so any entry touching 2012 failed with AccountsNotInChartError. Added it with the equity SRU code its siblings share, and a description separating it from 1630, which carries a confusingly similar name on the asset side. The port test now distinguishes deliberate divergence from accidental drift: a pack not listed in INTENTIONAL_DIVERGENCES must still match the seeded JSONB exactly, and a listed pack must actually differ, so neither an unnoticed edit nor a stale entry can survive. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Konteringspaket
Reusable bookkeeping patterns, as data. One YAML file per pattern.
These are the templates a user picks in the app when booking something common: representation, EU-handel, periodiseringsfond, löneutbetalning. They used to be rows frozen inside a database migration. They are files now, so correcting one is a one-line edit and a green CI run instead of a new migration.
Anatomy
meta:
slug: representation-avdragsgill-25-moms # filename must match, this is the public key
order: 13 # display order, unique across the catalogue
name: 'Representation (avdragsgill, 25% moms)'
category: representation # eu_trade | tax_account | private_transfer |
# salary | representation | year_end | vat |
# financial | other
entity_type: all # all | enskild_firma | aktiebolag
description: >-
Extern representation med avdragsgill moms. Max 300 kr/person exkl. moms.
lines:
- account: '6072' # BAS account, ALWAYS quoted (it is a string)
label: 'Representation avdragsgill'
side: debit
type: business
ratio: 0.8
- account: '2641'
label: 'Ingående moms'
side: debit
type: vat
vat_rate: 0.25
- account: '1930'
label: 'Företagskonto'
side: credit
type: settlement
ratio: 1.0
The three line types
The user types one total amount. The type decides how each line's amount is
derived from it (applyTemplate() in lib/bookkeeping/template-library.ts):
| Type | Amount | Carries |
|---|---|---|
vat |
total * vat_rate / (1 + vat_rate) |
vat_rate, never ratio |
business |
total * ratio |
ratio, never vat_rate |
settlement |
total * ratio |
ratio, never vat_rate |
settlement is the money leg (the bank account, the reskontra). business is
the cost or revenue. Putting a ratio on a vat line silently computes the
wrong amount, so the schema rejects it rather than trusting you to remember.
Rules the CI gate enforces
Run npm run validate:packs before pushing. It checks:
- The schema, including the
vat_rate/ratiosplit above. - Filename equals
meta.slug. meta.slugandmeta.orderare unique across the catalogue.- Every account exists in the BAS 2026 chart. A pack may only reference standard accounts, because a non-standard one cannot be seeded into a company's chart and the template will fail to apply.
- The pack balances at five probe amounts, applied through the real
applyTemplate(). Debits must equal credits or the verifikat cannot post. - Both a debit and a credit line are present.
Account numbers are strings
account: '1930', never account: 1930. YAML would read the unquoted form as
a number, and a BAS account is an identifier, not a quantity. The schema
rejects it, but quote it anyway so the file reads correctly.
Swedish stays Swedish
name, description and legal_note are user-facing Swedish and are not
translated, in either locale. They are statutory content, per
.claude/rules/i18n.md.
Known-broken templates
Four packs ported out of the original migration have pre-existing problems
(an unbalanced salary template, and accounts that no longer exist in BAS 2026).
They are listed in KNOWN_BROKEN in scripts/validate-packs.ts with the reason
for each. They are quarantined, not accepted: the list may only shrink, and
fixing one means deleting its entry. Each needs a Swedish accounting decision
rather than a code change, which is why they were not fixed during the port.
Adding a pack
- Copy the closest existing file, rename it to your slug.
- Set
meta.orderto one past the current highest. - Run
npm run validate:packs. - New user-facing strings go in the YAML, not in
messages/*.json: a pack carries its own Swedish.