Reframe the implementation notes as design decisions (no code exists yet) and resolve the open design questions: - card: contract fuel card only; retail rows carry no card - customer delete also guards against invoices and cards - invoice: outgoing fuel only; add explicit send (draft -> sent); --all fans out to one invoice per customer; document statuses - VAT: amounts stored inclusive, 25% base/VAT split at invoice time - file import: v1 = epsilon TSV only; one DB transaction per import; batch-total mismatch aborts with exit code 1 - file export --format raw is source-faithful; raw errors elsewhere - batch: document derived nature; update = manual reconciliation; list gains --from/--to/--year filters - daemon status is local-only (no external check); pidfile/logs under a configured state directory - db: --force on reset/restore; timestamped backups in configured dir; status reports migration version - add config show, --version, --config precedence, exit-code table, business-key ID semantics, and status values
8.9 KiB
8.9 KiB
Command structure
rpnc
├── batch # derived entity: created and maintained by "file import"
│ ├── update # 1. Recalculate all values of batch using data from transactions table
│ │ # 2. Update batch values
│ │ # Manual reconciliation: transactions are immutable, so this
│ │ # exists to recompute stored values after migration/correction
│ └── list # list all batches in DB
│ # optional filters --from, --to, --year
│
├── card # contract fuel card only (epsilon row with a non-empty
│ # customer number); retail transactions carry no card
│ ├── create # add card to DB
│ │ # - require customer
│ │ # - require status
│ │ # - require description
│ │ # - require pin
│ ├── read # fetch card details
│ ├── update # modify card
│ ├── delete # delete card
│ │ # only allowed on cards not referenced by any transactions
│ └── list # list all cards
│ # optional filters --customer, --status
│
├── config # effective configuration
│ └── show # print effective config, secrets masked
│
├── customer # contract customer; identity is the register customer number
│ ├── create # add customer
│ │ # - require customer number (ID) and name
│ ├── read # fetch customer details
│ ├── update # modify customer
│ ├── delete # delete customer
│ │ # only allowed on customers not referenced by any
│ │ # transaction, invoice, or card
│ └── list # list all customers
│
├── daemon # web server daemon (axum API + Leptos SPA)
│ ├── start # start the web server daemon (detached; pidfile and logs
│ │ # under the state directory from config)
│ ├── status # check status of daemon (local checks only, no external call):
│ │ # - running? (pid alive)
│ │ # - port? (listening)
│ │ # - db access? (connection ok)
│ │ # - fqdn? (reported from config, not verified externally)
│ └── stop # stop daemon
│
├── db
│ ├── setup # create database and schema (embedded migrations)
│ ├── reset # drop and recreate database (requires --force)
│ ├── status # check if connection to db is ok; reports migration version
│ ├── backup # create a backup of database (timestamped file in the
│ │ # configured backup directory)
│ └── restore # recreate database from backup file (requires --force)
│
├── file # imported source files; the filename is the natural key,
│ # so ingesting the same file twice must not duplicate data
│ ├── import # read source file into DB (v1: epsilon TSV only):
│ │ # 1. create any missing customers (placeholder name:
│ │ # "Customer <number>"; the file carries no name)
│ │ # 2. create any missing cards (pin/description nullable)
│ │ # 3. create transactions
│ │ # 4. create any missing batches
│ │ # 5. verify batch values match calculated value
│ │ # one import is one DB transaction: a step 5 mismatch
│ │ # aborts and persists nothing (exit code 1)
│ ├── list # list all files stored in DB
│ │ # (name, format, batch, row count, imported-at)
│ └── export # export a stored file to the specified format
│ # --format raw = byte-faithful copy of the source
│
├── invoice # outgoing fuel invoices only (a Swedish invoice names
│ # one buyer); tsdrms/subfranchise data is never invoiced
│ ├── create # add invoice to DB:
│ │ # - require batch OR date range
│ │ # - require "all" or specific customer
│ │ # - "all" fans out: one invoice per customer in the period
│ │ # - amounts stored VAT-inclusive; the 25% base/VAT split
│ │ # is computed at creation
│ ├── read # fetch invoice details
│ ├── update # modify invoice
│ │ # - only allowed on invoices with "draft" status
│ ├── send # transition invoice from "draft" to "sent"
│ ├── delete # delete invoice
│ │ # - only allowed on invoice with the highest ID number
│ │ # - only allowed if invoice status is "draft"
│ │ # - known limitation: a still-draft credit invoice can no
│ │ # longer be deleted once a newer invoice exists; fix it
│ │ # with a newer credit
│ ├── credit # create a credit invoice
│ │ # - required if an error needs to be fixed on a sent invoice
│ ├── export # write HTML files to disk
│ │ # - pure file writer; does not change invoice status
│ └── list # list all invoices
│ # optional filters --customer, --from, --to
│
└── transaction # immutable; created only via "file import"; no create/update/delete
├── read # fetch transaction details
└── list # list transactions
# optional filters --customer, --card, --batch, --from, --to
Global flags
--config=<path> # explicit config path; takes precedence over --env
--env=[dev,test] # select config.<env>.toml; production (config.toml) assumed
--quiet # suppress all output; the exit code is authoritative
--format=[raw,json,csv,columns] # columns assumed; raw is only honored by `file export`
--help, --version # generated by clap, per subcommand
Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | domain error (validation, import mismatch, constraint violation, ...) |
| 2 | usage error (clap argument parsing) |
| 3 | entity not found |
ID semantics
Positional id arguments take the business key of the entity, never a
surrogate key: customer number, card number, batch number, invoice id, or
filename, depending on the entity.
Status values
- card: active / suspended / cancelled
- invoice: draft / sent
Sent invoices are immutable; corrections go through
credit. (A "paid" state is a future concern, out of scope for v1.)
Design decisions (added during CLI design)
The following decisions were made while designing the CLI command structure:
- Positional IDs:
read,update,delete,send, andcreditsubcommands that operate on a single entity take a positionalidargument (e.g.,card read <id>,invoice credit <id>). - Card: only contract fuel cards exist in the DB; a card always belongs to a customer.
- Card create: Uses flags
--customer,--status,--description,--pin. - Card list: Optional filters
--customerand--status. - Customer create: Uses flags
--idand--name. - File import: Takes a positional path to the source file; v1 accepts epsilon TSV files only (tsdrms xlsx and subfranchise PDF are future work).
- File export: Takes a positional filename;
--formatselects the target export format,rawbeing a byte-faithful copy of the original source. - Invoice scope: outgoing fuel invoices only; tsdrms/subfranchise data feeds the bookkeeping/voucher feature, never invoicing.
- Invoice create: Uses flags
--batch,--from,--to,--customer, and--all. The caller must supply either--batchor a--from/--todate range, and either--customeror--all.--allfans out to one invoice per customer found in the period. - Invoice send: Takes a positional
id; the only status transition in v1. - Invoice list: Optional filters
--customerand--from/--to. - Transaction list: Optional filters
--customer,--card,--batch,--from,--to. - VAT: transaction amounts are stored VAT-inclusive as delivered by the register; the 25% base/VAT split is computed at invoice creation.
- Global
--env: Acceptsdevortest; omitted means production. An explicit--configpath takes precedence. - Global
--format: Defaults tocolumns;rawis only honored byfile exportand is a usage error (exit code 2) on any other command.