Download invoices as PDF

Go · Go · intermediate · modification

Adds the PDF download behind the new Download button on the invoices page. The handler parses the id from the path, loads the invoice through a new `Store.Get` that reuses the list's column set and scanner, and renders it with the existing `RenderPDF`, so the file is the same document customers get by email. Rendering goes to a buffer first, so a failure still answers a clean 500 instead of a half-written PDF; the response is marked `no-store`, and every download is logged for finance's audit trail. Tested locally from the invoices page: the downloads match the emailed copies.

Billing service on Go 1.22 and the standard library (`net/http` method-pattern `ServeMux`, `log/slog`, `database/sql` with the pgx stdlib driver, PostgreSQL 16). `main` wraps the mux in the auth middleware, so every `/api/` handler runs for a signed-in user; `auth.FromContext(ctx)` returns an `auth.Member` with `UserID`, `OrgID` and `Role`, and a user belongs to exactly one org. Package `invoices` already has the `Handler` type with the `List` handler (`GET /api/invoices`, which calls `Store.ListForOrg` with the caller's `OrgID`), `writeJSON` and `writeError(w, status, code)` in `respond.go`, and `RenderPDF(w io.Writer, inv Invoice) error` in `render.go`, used by the monthly invoice email. `invoices.id` is a `bigserial`, so ids are sequential across all orgs, and they appear in the web app's URLs (`/invoices/1042`). An invoice shows the customer's name and the amounts billed.

Requirements

Files touched

--- internal/invoices/routes.go
 	h := &Handler{Store: &Store{DB: db}}
 	mux.HandleFunc("GET /api/invoices", h.List)
+	mux.HandleFunc("GET /api/invoices/{id}/pdf", h.PDF)
 }
 
--- internal/invoices/store.go
 	"context"
 	"database/sql"
+	"errors"
 	"time"
 )
+
+var ErrNotFound = errors.New("invoice not found")
 
 type Invoice struct {
 }
 
+// Get returns one invoice by id, or ErrNotFound.
+func (s *Store) Get(ctx context.Context, id int64) (Invoice, error) {
+	inv, err := scanInvoice(s.DB.QueryRowContext(ctx,
+		`SELECT `+invoiceColumns+` FROM invoices WHERE id = $1`, id))
+	if errors.Is(err, sql.ErrNoRows) {
+		return Invoice{}, ErrNotFound
+	}
+	return inv, err
+}
+
--- internal/invoices/pdf.go
+package invoices

Review this PR

Go practice