An invoice issued by mistake could only be left pending or marked paid.
Add cancel (with an optional reason) and reinstate. Cancelling renders a
third PDF variant with a diagonal CANCELLED stamp and the date/reason,
and it becomes the current one, so the client's link and download show
the invoice as void. Cancelled wins over paid in the status, and marking
a cancelled invoice paid is refused.
Superusers can also hide an invoice: it disappears from the admin list,
dashboard and invoice pages for every other user, while the client's
public link keeps working.
Assisted-by: Claude:opus-5
One asset per invoice from a registry (lib/crypto_asset.rb): BTC, LTC,
ETH, XMR, SOL, ALGO, USDT/USDC on ERC-20/TRC-20/BEP-20/Solana/Algorand.
Migration 003 generalizes the ltc_* columns to crypto_* + crypto_coin
(existing LTC invoices backfilled). QR payload adapts per asset: BIP21
amount URIs where supported, bare address for tokens with a network
hint on the PDF. Default addresses come from the CRYPTO_ADDRESSES
secret (JSON code=>address); LTC_ADDRESS still works as legacy.
Three admin templates:
- index: invoice list with status badge and link to detail.
- new: client + terms fields, dynamic line-item rows backed by
admin-invoice-form.js (add/remove + re-index of items[N][...] names).
- show: invoice detail, public URL to share, toggle-paid button.
One public template, invoice_public.erb, shown at /i/:uuid: client name,
number, total, status, and a single big download button to /i/:uuid/pdf.
All admin pages are noindex; same for the public landing (it's a
client-specific URL, not search-discoverable content).