- In development: the CLI command structure is specified in Documentation/cli.md - Config: daemon state and database backups default to ~/.config/rpn (backups in its backups/ subdirectory), overridable in config
295 lines
15 KiB
Markdown
295 lines
15 KiB
Markdown
# 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:
|
||
1. Selling gasoline and diesel at the local airport both to retail, as well as to the car rental firms located at the airport.
|
||
2. 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.8x (TBD).
|
||
Crate versions are decided at `cargo add` time 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).
|
||
- 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 the `Batch number` field 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) or `YYYY-MM-DD - YYYY-MM-DD.xlsx`
|
||
(period), e.g. `2026-01.xlsx`, `2026-01-01 - 2026-07-31.xlsx`.
|
||
One sheet, named `Sheet`.
|
||
- 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: `<received YYYY-MM-DD> -- <Invoice #> - Subfranchise Statement
|
||
- <settlement YYYY-MM>.pdf`, e.g.
|
||
`2026-03-02 -- 821100844 - Subfranchise Statement - 2026-01.pdf`.
|
||
Invoice numbers are sequential (821100844, 821100853, …).
|
||
- 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:
|
||
1. Create a named git branch for a new feature, improvement, or bugfix
|
||
2. 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
|
||
3. Write a failing test
|
||
4. Commit test to git
|
||
5. 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
|
||
6. Verify everything on the checklist has been completed
|
||
7. Update roadmap
|
||
- this file is the source of truth
|
||
- any task that changes a decision (stack, format, workflow) updates it in the same commit
|
||
8. 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
|
||
* 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
|