Some formats predate the idea that a computer would ever read them back to you kindly. Enrolling customers in Danish Betalingsservice means producing a fixed-width file where every single line is exactly 128 characters — right fields, right padding — or the bank quietly rejects the whole batch. Doing that by hand in a text editor is how you lose an afternoon hunting one misplaced space.
The problem
Two things go wrong, and both fail silently downstream. A line that's 127 characters looks fine to a human and is garbage to the bank. And the CPR number you typed — the Danish personal ID at the heart of each record — has real structure most tools never check, so a transposed digit sails straight through to a rejected submission days later.
The format is unforgiving, so the tool has to be — before the file ever leaves your machine.
How it works
It's a small menu-driven CLI — one operator, one file at a time, no surprises:
=== Main Menu - PBS Enrollment === 1. Create enrollment file 2. Verify the file 3. Send file to PBS 4. Reset and archive 5. Setup SFTP connection 6. Exit
The most interesting part is the CPR validator. It runs the full spec — date check, the century table, and a modulus-11 checksum — but it's careful about a real-world wrinkle: mod-11 was abandoned for numbers issued after 2007, so a structurally valid number that fails the checksum isn't wrong, it's just unverifiable. So it reports three states, not two:
# validate a CPR against the full Danish spec, standalone python3 cpr_validator.py 010190-XXXX # → VALID passes every check, modulus-11 included # → UNVERIFIABLE well-formed but fails mod-11 (likely a post-2007 number) # → INVALID definitively wrong: bad date or century/sequence combo
Generation builds the record, verify asserts every line hits 128 characters, and send uploads over SFTP with an SSH key via paramiko. Old batches get archived and the working directory swept clean, so the next run starts from zero.
What I'd do differently
- The 128-character rule is checked after generation; building each field to its fixed width as it's written would catch a short line at the source instead of one step later.
- It leans on an
id_rsakey file and a.envsitting next to the code. An SSH agent or the OS keychain would keep the secret off disk entirely — the right move for anything that touches a bank. - There's no dry-run. A preview of exactly what's about to hit the bank's SFTP, before it does, would take the fear out of pressing 3.
Roadmap
It's a deliberately small tool that does its one job. If it grows, it's a batch validation report over a folder of files and moving credentials into the keychain — not features, just fewer sharp edges.
Thanks for reading. Found a bug or have a sharper idea? get in touch — or run cat 0x0104 above for another small CLI.