Files
accounted/packs/README.md
T
Jakob Wennberg df34cae9bf feat(packs): konteringspaket as validated data files (phase 2a) (#1386)
* feat(packs): konteringspaket as validated data files, ported losslessly

The 26 system booking templates lived inside migration 20260413160000. Under
the never-modify-a-shipped-migration rule that froze them: correcting a wrong
BAS account or a Swedish typo needed a whole new migration, and nothing checked
that a seeded account existed in the chart or that a template balanced. #1321
was exactly that failure with seeded chart names.

They are now one YAML file per pattern under packs/, with a Zod contract and a
CI gate. A correction becomes a one-line edit plus a green run.

The port is proven lossless, not asserted. The test fixture was read out of a
Postgres with all 548 migrations applied, so it is the exact JSONB production
holds; lib/packs/__tests__/port-is-lossless.test.ts asserts the YAML reproduces
it by value. Phase 2b can swap the seeded rows for the loader as a no-op.

The gate checks what makes a pack CORRECT, not just well-formed, because #1321
was structurally valid and still wrong: every account must exist in BAS 2026,
and every pack must balance at five probe amounts through the real
applyTemplate() rather than a reimplementation. Account numbers validate through
lib/invariants, so a pack cannot disagree with the API or the SIE importer about
what an account number is.

Doing that immediately found four pre-existing breakages in the shipped
templates:

  loneutbetalning                    debits total 1.42x the amount against a
                                     1.0 credit: it can never post
  periodiseringsfond-avsattning-ab   account 2113 is not in BAS 2026 and is not
  periodiseringsfond-aterforing-ab   seeded into any company chart
  preliminar-f-skatt-ef              account 2012, same problem

These are quarantined in KNOWN_BROKEN, not fixed and not hidden: a quarantined
pack's findings are warnings, any NEW finding fails the build, and the validator
fails if a quarantined pack turns out to be clean, so the list may only shrink.
Each is a Swedish accounting content change to a user-facing template, which
deserves its own review rather than riding along inside a file-format change.

Five shipped descriptions contain em dashes, preserved verbatim and pinned by a
test: a lossless port must not silently rewrite user-visible strings.

js-yaml is promoted from a transitive dependency to a declared one (MIT, already
in node_modules), so the catalogue does not depend on it by accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(deps): regenerate package-lock.json with npm 10 to match CI

`npm ci` failed on every job with "Missing: @swc/helpers@0.5.23 from lock
file". The lockfile was written by local npm 11.6.0; CI runs npm 10.8.2 on
node 20, and npm 11 emits a tree npm 10 reads as out of sync.

Regenerated with `npx npm@10 install --package-lock-only`, which cuts the diff
from a sprawling rewrite down to the three entries this branch actually adds
(js-yaml, @types/js-yaml, and the @swc/helpers entry npm 11 had dropped).
Verified with `npx npm@10 ci --dry-run`.

This is the documented gotcha for this repo: regenerate lockfiles with
npx npm@10, never with a local npm 11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:05:54 +02:00

98 lines
3.9 KiB
Markdown

# 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
```yaml
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:
1. The schema, including the `vat_rate` / `ratio` split above.
2. Filename equals `meta.slug`.
3. `meta.slug` and `meta.order` are unique across the catalogue.
4. **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.
5. **The pack balances** at five probe amounts, applied through the real
`applyTemplate()`. Debits must equal credits or the verifikat cannot post.
6. 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
1. Copy the closest existing file, rename it to your slug.
2. Set `meta.order` to one past the current highest.
3. Run `npm run validate:packs`.
4. New user-facing strings go in the YAML, not in `messages/*.json`: a pack
carries its own Swedish.