Tests define the db module API: - connection_url() builds the sqlx URL with percent-encoded credentials; with/without the database name - split_statements() splits SQL on semicolons outside strings and line comments (the embedded migration is applied statement by statement, since DDL cannot run in a rollbackable transaction) - the embedded migration is Documentation/schema.sql (version 1), carrying the seven domain tables only; schema_migrations is created by the application - backup_command() shells out to mariadb-dump (mysqldump fallback) with --single-transaction, password via MYSQL_PWD, never argv - restore_command() pipes the dump (which carries CREATE DATABASE / USE) into the mariadb client - backup_filename() is timestamped per cli.md
RustyRPN
RustyRPN is a project to create and maintain a business management application for a small Swedish Aktiebolag.
The business
The company in question is Rent & Petroleum Nordic AB with a yearly turnover of about 20 million SEK. RPNAB has two main revenue generators:
- Selling gasoline and diesel at the local airport both to retail, as well as to the car rental firms located at the airport.
- Acting as a franchisee to Enterprise Rent-A-Car.
RPNAB is historically a family company and is at the moment driven by two cousins; Johan (CEO) and Jakob (chairman of the board) Rönnbäck.
For a few years RPNAB had a daughter company called Recamp Nordic AB that everything related to car rentals was delegated to, but it was recently absorbed back into the main company.
The maintainer
- Official position within the company is board-of-director as well as owner of 60% of the private equity
- Have some minor experience with development (mainly node.js)
- Is using this project as an opportunity to learn Rust, as well as AI-assisted development
- Prefer a TDD approach and functional code style (used ramda library while writing node.js code)
- Uses OmniFocus and a GTD inspired workflow for keeping track of tasks
The application
Code will eventually be kept on a private gitea instance (for issue handling), but everything is to be considered open source. No expectations of assistance with writing code, but if the project and the company is successful a dream of the maintainer is for other companies and developers to make use of it. However, due to the bespoke nature of the functionality provided this seems unlikely.
Technology
The whole stack is Rust: one language, one toolchain, one test runner, from database to browser. Motivation for choosing Rust is maintainability, not speed or efficiency.
- Rust: stable channel, edition 2024. Minimum supported version: 1.85
(the first release with edition 2024).
Crate versions are decided at
cargo addtime and pinned in Cargo.lock; this document records choices, not versions. - Async runtime: tokio.
- CLI: clap (derive API).
- Web server & JSON API: axum.
- SPA: Leptos, served by the same binary as the API.
- Database: MariaDB (protocol-compatible MySQL). Client: sqlx, with mysql_async as fallback. The DB server itself is out of scope; given connection details in config, the application creates its own tables from embedded migrations (schema_migrations table). The v1 domain schema (files, customers, batches, cards, transactions, invoices, invoice_items) is documented in Documentation/schema.sql; it is the initial embedded migration.
- Config: TOML file (see config.template.toml), loaded via --config flag or $RUSTYRPN_CONFIG. Real config files are gitignored. Daemon state (pidfile, logs) and database backups default to ~/.config/rpn (backups in its backups/ subdirectory); both are overridable in config.
- Serialization: serde / serde_json.
- Logging: tracing (level via RUST_LOG).
- CSV/TSV files: csv crate; non-standard raw-export dates are parsed by our own module, test-covered.
- Dates: chrono.
- Errors: thiserror for domain errors, anyhow in binaries.
Repository layout (Cargo workspace, root: Application/):
- src/core – domain logic: ingest, invoices, Fortnox, car registry
- src/cli – command line interface
- src/server – axum server: JSON API + SPA assets
- src/web – Leptos SPA
Build & test:
- cargo build / cargo test at the workspace root
- Tests needing a database use config.test.toml and a scratch schema; tests must not assume pre-existing data
- The binaries are built on FreeBSD (pkg rust); they run inside a jail
Hosting:
- Caddy reverse-proxies the web interface (TLS) from a separate jail (running caddy is out of scope)
- The CLI is used from the LAN only (e.g. via SSH); nothing is exposed for it.
Authentication & Authorization:
- Portal authentication (v1): login form with email + password (argon2id)
- Server-side session in MariaDB behind a HttpOnly/Secure/SameSite=Lax cookie, rate-limited, no user enumeration
- Roles:
- director (Jakob & Johan, full access)
- employee (access to appropriate internal functions)
- customer (own company's invoices only — every query scoped to the session's company)
- OIDC and passkeys are future options, not part of v1.
Data formats
Three document types flow into the application. Samples for all of them live
under Application/data/test_input/ in one directory per source. Samples
contain real customer and card data: they are gitignored, must never be
committed, and their values must never be embedded in committed test files.
Tests may reference sample files by path (the files stay local), or use
synthetic/sanitized values in the repo.
The raw formats are canonical: the application parses what the source systems deliver. Filenames carry metadata (batch / month / invoice number) and are the natural keys for idempotent re-ingestion — ingesting the same file twice must not duplicate data.
Date and number formats differ per source; never assume:
| Source | Date format | Example | Number format |
|---|---|---|---|
| epsilon | US style, no zero padding: M/d/yyyy h:mm:ss AM/PM |
3/16/2026 6:08:43 AM |
561.24 |
| tsdrms | EU style: DD/MM/YYYY |
07/01/2026 |
-540.54 |
| subfranchise PDF | Month name + year in text | January 2026 |
European: 1.387.732,10, (35.070,15) for negatives |
1. Epsilon fuel station exports
Sales transactions from the station's cash register system Epsilon
- Filename:
raw-export-from-epsilon-<batch>.txt, e.g.raw-export-from-epsilon-405.txt. One file = one batch; the filename number equals theBatch numberfield of every row in the file. - Batches are numbered sequentially (two per month in samples) and span several days each (e.g. batch 409: 2/1–2/9). Cumulative exports can contain many batches (sample: batches 1–406, ~138k rows, 2019→2025).
- File format: ASCII, tab-separated, every field double-quoted, CRLF line endings, header row, trailing newline. 16 fields:
| Field | Example | Notes |
|---|---|---|
| Date | 3/16/2026 6:08:43 AM |
US format, see table above |
| Batch number | 405 |
matches filename |
| Amount | 561.24 |
SEK; = Volume × Price |
| Volume | 31.18 |
liters |
| Price | 18.00 |
SEK/liter |
| Quality | 1001 |
code: 1001 = unleaded, 4 = Diesel, 0 = zero-value record |
| QualityName | 95 Oktan |
empty for Quality 0 |
| Card number | 549543******5778 |
consumer cards are masked; contract cards appear unmasked and are exactly the rows with a non-empty Customer number — personal data, store as delivered |
| Card type | 549543******5778 |
equals Card number in all samples |
| Customer number | 1861 |
contract customer id; empty for retail |
| Station | 97254 |
single station in samples |
| Terminal / Pump | 1 / 2 |
small integers |
| Receipt | 004109 |
zero-padded 6 digits |
| Card report group number | 4 |
|
| Control number | 126301 |
alphanumeric; empty on some rows |
Known edge cases (from samples — verify against new real files before changing the parser):
- Quality 0 rows are zero-value records (amount, volume, price all
0.00, empty name)- canceled fuelings and testing events
- No negative amounts in any sample.
2. TSDRMS exports (Enterprise rental system)
- Filename:
YYYY-MM.xlsx(monthly) orYYYY-MM-DD - YYYY-MM-DD.xlsx(period), e.g.2026-01.xlsx,2026-01-01 - 2026-07-31.xlsx. One sheet, namedSheet. - Content: a general ledger journal export (double-entry), not a list of rentals. Each rental appears as paired debit/credit rows. 11 fields:
| Field | Example | Notes |
|---|---|---|
| GL Account # | 2050 |
2050 DEPOSITS RECEIVED, 8000 MASTERCARD, 1030 WIRE TRANSFER PAYMENT, 1511 ACCOUNTS RECEIVABLE, 2611 TAX TYPE 1, … full code list unknown — derive from data |
| Description | DEPOSITS RECEIVED |
GL account name |
| Location | LLAT73 |
Enterprise location code |
| CODE | M, R1, P, D, CDWTPI |
product identifier used to map rows to bookkeeping accounts |
| R/A # | LLAT62-3986i5 |
rental agreement id; format varies (also 345670C) |
| Transaction Date | 07/01/2026 |
DD/MM/YYYY |
| DBR | LLAC61-1260 |
meaning unknown |
| Cutoff Date | 07/01/2026 |
DD/MM/YYYY |
| Debit / Credit | -1.25 / 1.25 |
numeric; stored as IEEE floats in the XML (e.g. -540.53999999999996) — round to 2 decimals on ingest |
| Product Type | VEHICLE |
VEHICLE / NO PRODUCT |
- Partitioning quirk (important): monthly files are partitioned by Cutoff Date, not Transaction Date. All 2797 rows of the January 2026 file have January cutoffs, but 23 of them have December 2025 transaction dates. Never assume transaction month = file month.
- Scale: ~2.8k–3.6k rows per month; ~19.9k rows for a 7-month period file.
3. Subfranchise statements (PDF)
Monthly settlement statements from Enterprise (Shared Mobility Sverige Filial, org.nr 516411-7920) for the sub-franchised business. One PDF per settlement month, received ~1–2 months after the period ends.
- Filename: ` -- <Invoice #> - Subfranchise Statement
- .pdf
, e.g.2026-03-02 -- 821100844 - Subfranchise Statement - 2026-01.pdf`. Invoice numbers are sequential (821100844, 821100853, …).
- .pdf
- 1-page A4, text-based (not scanned) — text extraction works. Choose a Rust PDF text-extraction crate when this feature starts; layout stability is verified for six samples (2026-01 → 2026-06) only, so parse defensively and re-verify against each new statement.
- Header block: sender name/address/org.nr, settlement period (month name +
year), location code (
LLAT), Invoice #, Date, Partner (Recamp Nordic AB), Reference (e.g.Subfranchise Statement - Luleå January 2026). - Body: line-item table with columns
DESCRIPTION | EUR | @daily rate | AMOUNT SEK. Numbers use the European format from the table above; zero cells read€ - 10,66 -. - Known line items (order can vary; some months contain zero rows; names can
embed the month, e.g.
InMoment SQI - January 2026— match by pattern, not exact string): All Revenues, Excluded revenues, Gross revenues less exclusions, Royalty Fee (7%), Central Invoicing Fee (2%), Marketing Fee (1%), reservation-fee line, GF Learning Center, InMoment SQI, Cross Border Debit, Bad Debt Reserve (1%), subtotals, Total fees due, VAT (25%), adjustments, Total amount due to Franchisee.
4. Voucher mapping reference (Fortnox)
Application/data/other_resources/ (gitignored) holds a copy of the Apple
Numbers spreadsheet the maintainer currently uses to turn tsdrms data into
bookkeeping vouchers. It is the reference for the voucher feature: read it
to understand the mapping; never modify it.
It maps tsdrms Description values to Swedish voucher descriptions in these
groups: sales (Fordran - Biluthyrning - Faktura / - Shared Mobility,
Bränsleförsäljning, VAT-liable / VAT-exempt sales), products and fees
(Försäkringsprodukter, parking and traffic-violation fees, airport fee,
road assistance), damage billing (Skadedebitering), write-offs
(Nedskrivning), card-network and Enterprise reconciliations
(Avstämning - American Express / - Windcave / - Enterprise), and
output VAT (Utgående moms, 25%). Output includes Export ID and
Bankkonto per line.
The complete mapping lives in the spreadsheet's formulas and is not in the repo. When the voucher feature starts, its first task is to port the mapping into versioned data in the repo (e.g. a TOML table) before writing code against it.
Development
- Two target interfaces
- Command line: for scripting & automation, and efficiency for advanced users (i.e. myself)
- Web based (SPA) for regular employees and customers
- Code
- Main priority is maintainability and user facing interface stability
- Style should emphasize ease of understanding as well as long term stability
- Keep source code clean by mainly keeping descriptive documentation in the git commit messages rather than inline
- Use generative AI for writing docs, tests, and code
- Use a TDD inspired approach by always writing tests before implementation code
- Liberal use of checklists to keep both human and AI on track
Workflow
Use a test-driven-development inspired approach, specifically the following workflow:
- Create a named git branch for a new feature, improvement, or bugfix
- Create specification for changes to be made 2.1 Discuss the goal; developer and AI-model discuss a new feature, fix, or improvement to be done 2.2 AI-model asks clarifying questions until a clear understanding is shared between human and AI on what the goal is 2.3 Agree on a checklist of items to test against to help keep the AI on track
- Write a failing test
- Commit test to git
- Write code 5.1 Write the minimal code to pass one of the tests 5.2 Run test and verify test passes 5.3 Create a git commit per passing test 5.4 Iterate for each test until they all pass
- Verify everything on the checklist has been completed
- Update roadmap
- this file is the source of truth
- any task that changes a decision (stack, format, workflow) updates it in the same commit
- Provide a short list of suggestions for improvements and include a suggested global next step
General guidelines for AI
- Do not
- commit secrets or private data
- commit config files (except for template)
- commit anything under data/
- add dependencies without asking
- refactor existing code as part of an unrelated task
- put secrets or real customer data in tests
- Git commit messages
- Follow best practices regarding format and context, but also keep in mind the commit messages are the main code documentation
- Always ask for feedback before actually committing
- Be concise
Roadmap and status of functionality
Active
nothing yet…
In development
- cli
- command structure specified in Documentation/cli.md
- ingest tab-separated .txt files containing transactions from automated gas station into a MariaDB
- create invoices from data in database
Future / planned
- cli
- ingest xlsx files containing car rental transactions exported from tsdrms
- prepare bookkeeping voucher to be entered into Fortnox accounting/bookkeeping software suite
- ingest pdf files containing monthly settlement data from Enterprise
- spa
- website for car rental firms to check details regarding invoices
- receipt reissue for retail fuel sales (low priority)
- customer requests a receipt for a past fuel purchase (forgot to take it, or a printing issue)
- identifies the transaction by date + last 4 digits of the card number
- consumer cards are stored masked (e.g.
549543******5778), so the last 4 digits are available for lookup
- consumer cards are stored masked (e.g.
- creates a new receipt from a template
- sends it to the customer by e-mail
- to-do-application for employees
- car registry for RPN
- cars directly owned
- leased cars
- keeps track of costs associated with each car
- keeps track of yearly car inspections (besiktning)
- keeps track of general car status
- winter or summer tyres
- active, broken, being repaired (i.e. general status)
Glossary
- aktiebolag: limited company
- besiktning: yearly car inspection mandated by Swedish law
- Fortnox: suite of accounting and bookkeeping software
- tsdrms: rental management software used by Enterprise and its subfranchisees
- Epsilon: cash register system used to administer fuel sales