Raised by the Swedish compliance bot on #1395. The template called itself "Representation (avdragsgill, 25% moms)" and labelled its cost line "Representation avdragsgill", but booked to 6072, which BAS 2026 defines as Representation, EJ avdragsgill. The swedish-vat skill lists that exact confusion under Representation errors: "Not separating avdragsgill/ej avdragsgill on correct accounts (6071 vs 6072)". The account was right and the words were wrong. Meal representation stopped being income-tax deductible in 2017, so 6072 is where the cost belongs; only enklare fortaring (max 60 kr per person) is still deductible, on 6071. Renamed to "Representation, maltid (ej avdragsgill)" and relabelled to match. VAT moves from 25% to the 12% restaurang-och-catering rate our own static representation_external template in booking-templates.ts already used, so the two template systems stop contradicting each other, with the paired net ratio 1/1.12. A legal_note now carries the 300 kr per person VAT cap, which this format cannot compute because a template sees only a total and never a participant count, plus the reminder that the rate follows the underlag rather than the template. Correcting the record on one point: the seeded ratio 0.8 was NOT a phantom 80% deductibility rule, as the bot suggested and as I first repeated. 1/1.25 = 0.8, so it was the net-of-VAT fraction pairing with the 25% rate, and the arithmetic balanced. Only the naming and the missing cap were wrong. The ratio is now commented in the file so the next reader does not make the same misreading. Slug deliberately unchanged though it still reads "avdragsgill": a slug is an identifier, not a label (packs/README.md). Renaming it would retire this template and insert a new one, breaking every company's booking_template_usage history. The drift guard caught the rename, which exposed that the test joined packs to seeded rows by NAME. That is the same fragility that made pack_slug the sync's key, so INTENTIONAL_DIVERGENCES now records each entry's seededName explicitly. 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.