Files
RustyRPN/Documentation/cli.md
T
jakob 2b1cbb2500 Redesign CLI command structure after design review
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
2026-09-02 11:37:04 +02:00

151 lines
8.9 KiB
Markdown

## 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`, and `credit` subcommands that operate on a single entity take a positional `id` argument (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 `--customer` and `--status`.
- **Customer create**: Uses flags `--id` and `--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; `--format` selects the target export format, `raw` being 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 `--batch` or a `--from`/`--to` date range, and either `--customer` or `--all`. `--all` fans 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 `--customer` and `--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`**: Accepts `dev` or `test`; omitted means production. An explicit `--config` path takes precedence.
- **Global `--format`**: Defaults to `columns`; `raw` is only honored by `file export` and is a usage error (exit code 2) on any other command.