Files
RustyRPN/Documentation/cli.md
T
jakob 8e3207b139 Document v1 domain schema; clarify transaction read and import verify
Move the untracked 'schema draft.sql' to Documentation/schema.sql: the
agreed v1 fuel-domain schema (files, customers, batches, cards,
transactions, invoices, invoice_items), design decisions recorded in the
header: NULL-able cleartext card PINs, slim ledger projection (10 of 16
source fields, the files table is the canonical archive), string customer
business key, unified DECIMAL SEK money, full invoice traceability,
cli.md status values.

cli.md: transaction read takes <date> <receipt> -- the register's
receipt counter repeats across days (9,990 distinct receipts in the
138k-row sample), the day+receipt pair is the ledger dedup key, verified
unique in all samples. File import step 5 clarified as an internal
consistency check, since source files carry no totals of their own.
Invoice business key corrected to invoice number.

readme.md: point the database bullet at Documentation/schema.sql.
2026-09-02 16:38:07 +02:00

9.7 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: the
│   │                #      file carries no totals of its own, so this is an
│   │                #      internal check -- recompute each batch touched by
│   │                #      the import from the transactions table and compare
│   │                #      with the stored values
│   │                #   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 by <date> <receipt>:
                     #   the register's receipt counter repeats across days,
                     #   so the day + receipt pair is the business key
    └── 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 number, or filename, depending on the entity. The exception is transactions, whose business key is the pair day + receipt (transaction read <date> <receipt>): the register's receipt counter repeats across days, and the pair is the ledger's dedup key (verified unique in all samples).

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.
  • Db backup / restore: Take an optional positional file path; default to the configured backup directory (the latest dump for restore).
  • 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.