- owner_id (FK users, ON DELETE RESTRICT) + index on (owner_id, created_at DESC)
- client_id (nullable FK clients, ON DELETE RESTRICT) for client back-references
- issuer_snapshot jsonb + snapshot_version: invoices freeze the issuer's
legal-entity fields at issue time so historical PDFs render unchanged when
the user later edits their profile
- voided_at + voided_reason: soft-void instead of delete, since invoices
are retained indefinitely
- swap unique(number) for unique(owner_id, number): numbers are now per-user,
not global
DROP CONSTRAINT uses IF EXISTS so the migration is resilient to the
auto-generated constraint name differing across Postgres versions.
issuer_profiles is one row per user with all the legal-entity fields
that previously lived in the ISSUER constant — legal name, tax id,
reg number, bank details — plus per-user defaults (invoice prefix,
currency, due days, notes). On invoice creation these get snapshotted
onto the invoice row so historical PDFs render unchanged when the
user later edits their profile.
clients is per-user, case-insensitive unique on (owner_id, email).
Partial index on active clients for the dropdown. Soft-archive only;
ON DELETE RESTRICT to honor the indefinite-retention rule.
Adds users (citext email, nullable password_hash so a user can be
invited before claiming) and password_tokens (hashed one-shot tokens
for initial-set and reset, with purpose + TTL + used_at).
Token raw values live only in the email body; the DB stores SHA256
of the token so a DB leak doesn't grant account access.
DB.connect! opens a single Sequel connection from DATABASE_URL; DB.migrate!
runs Sequel migrations from db/migrations/.
The first migration creates the invoices table: uuid PK, unique invoice
number, client + currency fields, JSONB for line items, GEL conversion rate
captured at issue time, paid_at for the status toggle, pdf_key for the
MinIO object.