Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@ jobs:
- name: Validate skr03.json against schema
run: npx ajv validate -s schemas/skr03.schema.json -d src/data/skr03.json --spec=draft2020 -c ajv-formats

- name: Validate skr04.json against schema
run: npx ajv validate -s schemas/skr04.schema.json -d src/data/skr04.json --spec=draft2020 -c ajv-formats

- name: Validate mcc_skr_mapping.json against schema
run: npx ajv validate -s schemas/mcc_skr_mapping.schema.json -d src/data/mcc_skr_mapping.json --spec=draft2020 -c ajv-formats
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ CLAUDE.md
node_modules/
dist/

# DATEV chart PDFs — copyrighted, must never be committed
# ("Nachdruck - auch auszugsweise - nicht gestattet")
datev/

# Internal planning docs
german-accounting-spec.md
TODOS.md
Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const suggestion = suggestAccount("5812"); // restaurant
// }

// SaaS subscriptions (GitHub, Slack, Notion, Figma...)
suggestAccount("5817").primary.konto; // "4969" — Software-Nutzungsrechte
suggestAccount("5817").primary.konto; // "4964" — zeitlich befristete Überlassung von Rechten (Lizenzen)

// Target SKR04 instead of SKR03
suggestAccount("5812", "SKR04").primary.konto; // "6640"
Expand Down Expand Up @@ -63,7 +63,7 @@ SKR03.size // → number
suggestAccount(mcc: string, skr?: "SKR03" | "SKR04"): MCCSuggestion | undefined
```

MCC range entries (airlines `3000–3350`, hotels `3351–3500`, car rentals `3501–3999`) are expanded at load time. Any code in those ranges resolves correctly.
MCC range entries (airlines `3000–3350`, car rentals `3351–3500`, lodging `3501–3999`) are expanded at load time. Any code in those ranges resolves correctly.

When `skr = "SKR04"`, the primary account is the `skr04_primary` value. `name` is `null` and `alternatives` is empty (no SKR04 name data in v0.1).

Expand Down Expand Up @@ -117,6 +117,20 @@ The JSON files are the primary product. Download them from [GitHub Releases](htt

JSON Schemas are in [`schemas/`](schemas/) and validated by CI on every push.

### Verifying against the official DATEV charts

Every account number and designation can be verified deterministically against
the official DATEV chart PDFs (which are copyrighted and therefore not part of
this repository):

```bash
npm run verify:datev -- datev/skr03-2026.pdf datev/skr04-2026.pdf
```

See [`docs/datev-verification.md`](docs/datev-verification.md) for where to
download the charts, what the script checks, and the workflow for upgrading to
a new chart year.

### Data schema — `skr03.json`

```jsonc
Expand Down Expand Up @@ -160,6 +174,12 @@ JSON Schemas are in [`schemas/`](schemas/) and validated by CI on every push.
}
```

### MCC source and currency

The MCC data is a curated set of accounting suggestions, not a complete reproduction of the MCC catalogue. Code definitions are checked against [ISO 18245:2023](https://www.iso.org/standard/79450.html) and the public [Mastercard Quick Reference Booklet — Merchant Edition (25 February 2025)](https://www.mastercard.us/content/dam/public/mastercardcom/na/global-site/documents/mastercard-quick-reference-booklet-merchant.pdf). The exact source URLs and the last verification date are also recorded in [`src/data/mcc_skr_mapping.json`](src/data/mcc_skr_mapping.json).

Card networks can introduce or regionally assign MCCs after the recorded review date. Treat an unknown MCC as unmapped and verify it with the applicable network documentation before adding an accounting suggestion.

---

## Contributing
Expand Down
104 changes: 104 additions & 0 deletions docs/datev-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Verifying the datasets against the official DATEV charts

The SKR03/SKR04 datasets in this package claim to match the official DATEV
charts. `scripts/verify-datev.mjs` makes that claim testable: it parses the
official DATEV chart PDFs and checks every account in
[`src/data/skr03.json`](../src/data/skr03.json) and
[`src/data/skr04.json`](../src/data/skr04.json) against them.

Use it whenever the datasets change, and — most importantly — when a **new
chart year** is published, to find every account whose number, reservation
status, or designation changed.

## 1. Download the charts from DATEV

DATEV publishes the chart PDFs publicly:

| Chart | Document | DATEV Art.-Nr. |
|---|---|---|
| SKR 03 | "DATEV-Kontenrahmen Standardkontenrahmen – Prozessgliederungsprinzip (SKR 03)" | 11174 |
| SKR 04 | "DATEV-Kontenrahmen Standardkontenrahmen – Abschlussgliederungsprinzip (SKR 04)" | 11175 |

Find them via [datev.de](https://www.datev.de) (search for "Kontenrahmen
SKR 03" / "SKR 04" or the Art.-Nr.) and download the PDF for the year you want
to verify against. Save both files into a `datev/` folder in the repository
root, e.g.:

```
datev/skr03-2026.pdf
datev/skr04-2026.pdf
```

> **Never commit these PDFs.** They are DATEV's copyrighted documents
> ("Eigenformular, Nachdruck – auch auszugsweise – nicht gestattet") and must
> stay out of the repository. The `datev/` folder is gitignored for that
> reason. This is also why CI cannot run this check — it is a local,
> deterministic end-to-end test.

## 2. Run the verification

```bash
npm ci
npm run verify:datev -- datev/skr03-2026.pdf datev/skr04-2026.pdf
```

Exit code `0` means both datasets are fully consistent with the charts. A
clean run looks like:

```
SKR03: chart has 2149 named + 1214 reserved accounts; dataset 286 accounts, 286 verified, 0 findings
SKR04: chart has 2329 named + 1451 reserved accounts; dataset 222 accounts, 222 verified, 0 findings
```

For every dataset account the script checks that:

1. the account **number exists** as a named account in the chart,
2. the number is **not marked `R` (reserved)** in the chart, and
3. the dataset **designation matches** the chart designation
(whitespace-, hyphenation- and footnote-normalized, case-insensitive).

It does **not** verify the enriched metadata (`typ`, `gruppe`,
`ust_relevant`, `steuerschluessel`, MCC suggestions) — the chart PDFs do not
carry that information in machine-checkable form. Structural rules for those
fields live in `tests/data_integrity.test.ts`.

## 3. Upgrading to a new chart year

1. Download the new year's PDFs (see above) into `datev/`.
2. Run the script against them. Every finding is an account whose number was
freed/reserved or whose designation changed in the new year.
3. Update `src/data/skr03.json` / `src/data/skr04.json` accordingly
(designations must be copied verbatim from the chart), and review the
enriched metadata of every changed account by hand.
4. Bump `meta.version`, `meta.source`, and `meta.last_updated` in both data
files.
5. Re-run the script until it is clean, then run `npm test`.

## How the parser works (maintenance notes)

The chart PDFs are two-column pages; each half-page column is
`sidebar | function code | account number | designation`. The parser
(`scripts/verify-datev.mjs`, using the [`mupdf`](https://www.npmjs.com/package/mupdf)
WASM bindings as a dev dependency) reconstructs words with x/y positions and
applies the chart's layout grammar:

- **Per-page calibration**: the left number column x is the mode of all
4-digit token positions in a fixed band; the right column sits a constant
offset (`HALF_OFFSET`) to its right. Pages with fewer than 10 number tokens
(title, legend, footnotes) are skipped.
- **Zones**: only words within `ZONE_WIDTH` right of a number column belong
to that column's entries; this excludes the neighbouring sidebar.
- **Entries**: a line starting with a 4-digit number (optionally prefixed by
a DATEV function code such as `AM`, `AV`, `F`, `K`, `R`, `S`) starts an
account; following lines in the zone are designation continuations.
`R`-rows without text are reserved numbers; `-NN` tokens extend an entry to
a number range (e.g. `3300 -09`).
- **Headings**: bold lines that neither start an entry nor continue a bold
entry are section headings and end the current entry. (DATEV prints main
accounts bold too, so boldness alone does not identify a heading.)
- **Cleanup**: footnote markers (`Rücklagen17)`) are stripped and hyphenated
line wraps (`gegen-` + `über`) are rejoined.

If DATEV changes the page geometry in a future year, re-derive the constants
at the top of the script (they are documented inline) by dumping word
positions for one page.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "german-accounting",
"version": "0.1.0",
"version": "0.2.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the lockfile package version in sync

The manifest is bumped to 0.2.0, but both the top-level version and packages[""] .version in package-lock.json remain 0.1.0. This leaves the committed dependency-tree metadata inconsistent with the package being released and causes tooling that reads the lockfile to report the old package version; regenerate or update the lockfile as part of the version bump.

Useful? React with 👍 / 👎.

"description": "Machine-readable SKR03/SKR04 chart of accounts with MCC-to-account mapping for German SME accounting",
"keywords": [
"german-accounting",
Expand Down Expand Up @@ -43,12 +43,15 @@
],
"scripts": {
"build": "tsup",
"prepare": "npm run build",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"verify:datev": "node scripts/verify-datev.mjs",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"mupdf": "^1.28.0",
"tsup": "^8.0.0",
"typescript": "^5.4.0",
"vitest": "^1.6.0"
Expand Down
19 changes: 17 additions & 2 deletions schemas/mcc_skr_mapping.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,27 @@
"properties": {
"meta": {
"type": "object",
"required": ["version", "last_updated"],
"required": ["version", "last_updated", "sources", "source_checked"],
"properties": {
"version": { "type": "string" },
"last_updated": { "type": "string" },
"source_checked": { "type": "string", "format": "date" },
"description": { "type": "string" },
"total_mappings": { "type": "integer" }
"total_mappings": { "type": "integer" },
"sources": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["title", "url", "scope"],
"properties": {
"title": { "type": "string", "minLength": 1 },
"url": { "type": "string", "format": "uri" },
"published": { "type": "string", "format": "date" },
"scope": { "type": "string", "minLength": 1 }
}
}
}
}
},
"mappings": {
Expand Down
76 changes: 76 additions & 0 deletions schemas/skr04.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/german-accounting/skr04.schema.json",
"title": "SKR04 Kontenrahmen",
"description": "German Standard Chart of Accounts (Standardkontenrahmen 04) with enriched metadata",
"type": "object",
"required": ["meta", "klassen", "konten"],
"additionalProperties": false,
"properties": {
"meta": {
"type": "object",
"required": ["version", "source", "last_updated"],
"properties": {
"version": { "type": "string" },
"source": { "type": "string" },
"last_updated": { "type": "string" },
"description": { "type": "string" }
}
},
"klassen": {
"type": "array",
"items": {
"type": "object",
"required": ["klasse", "name"],
"properties": {
"klasse": { "type": "integer", "minimum": 0, "maximum": 9 },
"name": { "type": "string" }
}
}
},
"konten": {
"type": "array",
"items": { "$ref": "#/$defs/Konto" }
}
},
"$defs": {
"Konto": {
"type": "object",
"required": ["konto", "name", "klasse", "typ", "gruppe", "ust_relevant", "steuerschluessel"],
"additionalProperties": false,
"properties": {
"konto": {
"type": "string",
"pattern": "^\\d{4}$",
"description": "4-digit account number, zero-padded"
},
"name": { "type": "string", "minLength": 1 },
"klasse": { "type": "integer", "minimum": 0, "maximum": 9 },
"typ": {
"type": "string",
"enum": ["aufwand", "ertrag", "aktiv", "passiv"]
},
"gruppe": { "type": "string", "minLength": 1 },
"untergruppe": { "type": "string" },
"ust_relevant": { "type": "boolean" },
"steuerschluessel": {
"type": "array",
"items": { "type": "integer" }
},
"skr03": {
"oneOf": [
{ "type": "string", "pattern": "^\\d{4}$" },
{ "type": "null" }
],
"description": "Cross-reference to the corresponding SKR03 account number"
},
"notes": {
"oneOf": [
{ "type": "string" },
{ "type": "null" }
]
}
}
}
}
}
Loading