InvoicePdf.render(invoice) returns A4 PDF bytes. Layout: title + number,
horizontal rule, two-column FROM/BILL TO, meta strip (issued/due/currency/
status) bracketed by rules, line items table, right-aligned totals with
optional GEL equivalent, payment block (Bank of Georgia IBAN), optional
notes, repeating footer with issuer ID and page number.
Both party columns capture pdf.y after rendering and the global cursor jumps
to whichever ran longer — avoids the bbox cursor-restore footgun that was
collapsing the meta strip into the FROM column.
Noto Sans Georgian is registered as a fallback family so the Georgian legal
name renders without needing a separate text run.
Issuer details (legal name, tax ID, registration, address, bank) are
hardcoded as a constant — these are public on the rendered site already and
change rarely enough that a config file would be overkill.
Sequel::Model wrapper over the invoices table. Three responsibilities:
- Invoice.build takes form params (client, currency, GEL rate, items array)
and returns an unsaved Invoice with a freshly-allocated number, normalized
line items, and computed subtotal.
- Invoice.allocate_number scans for the highest INV-<year>-NNNN and
increments. Single-process deploy means no contention; if that changes
this needs a DB-side sequence per year.
- Instance methods: paid?, status, total, total_gel for the PDF renderer
and the public landing page.
Items are stored as JSONB; qty and unit_price are kept as strings on the
way in/out so BigDecimal arithmetic stays exact through the JSON round-trip.
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.