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
- Add `GET /api/invoices/:id` for the dashboard's invoice page: it responds `200 { invoice, lines }`, where `invoice` has the same fields as an item of the existing list (`id`, `number`, `status`, `currency`, `totalCents`, `issuedAt`) and `lines` holds the invoice's line items (`description`, `quantity`, `unitPriceCents`) in their `position` order.
- Invoices belong to one organisation. A signed-in member may see every invoice of their own organisation and none of another organisation's, exactly like the existing `GET /api/invoices` list.
- An id that is not a positive integer written in ASCII digits without leading zeros, does not exist, or belongs to another organisation gets `404 { error: 'not_found' }`, the same answer in all three cases, so the endpoint does not confirm which ids exist elsewhere.
- Database errors go to the app's error handler (`next(err)`), as in the list route.
Files touched
- src/routes/invoices.js
- src/repos/invoiceRepo.js
--- 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);
+ }