Add the invoice detail endpoint

Node · Node · beginner · modification

Adds the backend half of the invoice page: `GET /api/invoices/:id` returns one invoice with its line items, so the dashboard can open an invoice from the list instead of showing only the summary row. The repo gets `findById` (via the shared `toInvoice`) and `linesFor`; the route answers `404 not_found` per the spec and sends query errors through `next(err)` like the list route. Clicked through list to detail locally for a few invoices.

Express 4 multi-tenant billing API on PostgreSQL via `pg`; `src/db.js` (unchanged) exports `query(text, params)`. `src/app.js` (unchanged) mounts `app.use('/api/invoices', requireAuth, invoicesRouter)` and ends with a JSON 404 handler and an error handler that logs the error and answers `500 { error: 'internal' }`. `requireAuth` (unchanged) answers `401` without a valid session and otherwise sets `req.user = { id, orgId }`. Every member of an organisation may view all of its invoices. Tables: `invoices (id serial primary key, org_id integer not null, number, status, currency, total_cents integer, issued_at timestamptz)` and `invoice_lines (invoice_id integer references invoices, position integer, description, quantity integer, unit_price_cents integer, unique (invoice_id, position))`. An invoice and its lines are written in one transaction when it is issued and never change afterwards. Invoice ids come from one sequence shared by all organisations. A path that fails URL decoding is rejected by Express before routing and is out of scope for this PR.

Requirements

Files touched

--- src/routes/invoices.js
 
 const router = express.Router();
+
+// Invoice ids are positive serial integers; anything else cannot exist.
+function parseId(raw) {
+  if (!/^[1-9][0-9]*$/.test(raw)) return null;
+  const id = Number(raw);
+  return id <= 2147483647 ? id : null;
+}
 
 // GET /api/invoices - the caller's organisation's invoices, newest first.
 });
 
+// GET /api/invoices/:id - one invoice with its line items.
+router.get('/:id', async (req, res, next) => {
+  const id = parseId(req.params.id);
+  if (id === null) {
+    return res.status(404).json({ error: 'not_found' });
+  }
+  try {
+    const invoice = await invoiceRepo.findById(id);
+    if (!invoice) {
+      return res.status(404).json({ error: 'not_found' });
+    }
+    const lines = await invoiceRepo.linesFor(invoice.id);
+    res.json({ invoice, lines });
+  } catch (err) {
+    next(err);
+  }

Review this PR

Node practice