# Bug-fix session — 2026-08-24/25

## Why

Asked to read through the whole app (backend + frontend), find bugs, log them
to a file, and fix them one by one. See [BUGS.txt](BUGS.txt) for the full
catalogue — this file is the narrative summary of what was found, why, and
what changed.

## What was found

The single biggest issue was systemic, not a one-off: **27 endpoints looked
up a record by `:id` and then read or mutated it with no check that it
belonged to the caller's own outlet/company.** The README already documented
that this exact class of bug (cross-tenant data leak via an unscoped lookup)
had been found and fixed once before, in `dashboardController`/
`reportController`. What this session found is that the fix was never
propagated to the ~20 other hand-written controllers built the same way —
`middleware/authorize.js` only checks permission *slugs*, never record
*ownership*, so any authenticated user holding the right permission could
read, edit, or transact on another company's data by guessing/incrementing an
id. Concretely this included: resetting another company's user's password
(full account takeover), refunding another company's payment (repeatedly —
no double-refund guard either), checking a guest in/out of another company's
hotel, closing another outlet's cash register, and posting a salary expense
against another company's payroll item.

Two correctness bugs also stood out: a day-end closing report was silently
summing *every* outlet's refunds into whichever outlet ran the close (a
missing `outlet_id` filter that every sibling query in the same function
had), and a checked-in hotel reservation could be "cancelled," snapping the
room back to `available` while the guest was still in it.

On the frontend, the POS screen (the highest money-risk page) had a
double-order race (three independent buttons could each create an order if
tapped in quick succession), a cart that survived an outlet switch (so a
wrong-outlet item could ride into a bill), and a dead end where items added
after "Send to Kitchen" silently vanished with no error and no way to submit
them.

## What changed

- Added two small reusable ownership-check helpers,
  `inOutletScope`/`inCompanyScope`, to `backend/src/utils/outletScope.js` —
  mirroring the pattern `orderController.js`'s `orderInScope()` already
  established — and applied them (or the equivalent inline check where a
  record's scope had to be resolved through an association, e.g. a hotel
  folio via its room) across every controller/route listed in BUGS.txt.
- Added the one missing model association (`Refund.belongsTo(Order)`) needed
  to scope the day-end refund query correctly, since `Refund` has no
  `outlet_id` column of its own.
- Added a production-only fail-fast check on `JWT_SECRET`/
  `JWT_REFRESH_SECRET` so a misconfigured prod deploy can't silently sign
  tokens with the hard-coded dev placeholder.
- POS.jsx: one shared in-flight guard across Bill/KOT/Hold, a cart/order
  reset keyed on outlet switch, the menu grid now disables itself (with an
  explicit message) once an order is open instead of silently dropping taps,
  and the cart's addon/modifier dedupe key is now sorted so identical
  selections always merge into one line regardless of click order.
- PaymentModal.jsx: split-payment lines now key on a stable id instead of
  array index, so removing a non-last line during entry doesn't reassign
  focus across rows.
- OutletContext.jsx: `outletId` (and its localStorage entry) is now
  explicitly cleared when the user's outlet list goes empty (e.g. logout)
  instead of being left stale.

Two extra findings of the same ownership-check class turned up while fixing
the ones above and were fixed alongside them: `inventoryController.js`'s
`updateTransferStatus` (no company check before moving real stock between
outlets) and `employeeRoutes.js`'s attendance auto-mark endpoint (fetched the
employee but never checked it was the caller's own).

## Verification

No MySQL instance or automated test suite was available in this session
(fresh environment, not a git repo), so this was verified statically rather
than end-to-end:

- `node --check` on every edited backend file.
- `node -e "require('./src/app.js')"` — loads the entire route → controller →
  model tree (all 87 models, every route file) with zero errors, confirming
  no broken import/require introduced by the ownership-check edits.
- Every edited frontend file parsed cleanly through the project's own
  `@babel/core` + its configured presets.
- `npm run build` (webpack, production mode) compiled successfully with the
  edited POS.jsx/PaymentModal.jsx/OutletContext.jsx in place — same bar the
  README's own delivery notes hold the frontend to.

What this **doesn't** cover: no live database, so none of the fixed
endpoints were exercised against real data (e.g. actually calling
`resetPassword` cross-company and confirming a 404 now comes back instead of
a 200). If a MySQL instance gets set up, the highest-value manual check would
be re-running the two acceptance flows in `docs/POS-FLOW.md`/
`docs/HOTEL-FLOW.md` as two *different* seeded companies/outlets and
confirming cross-tenant 404s where BUGS.txt says they should now occur.

## Intentionally out of scope

- Extending an already-open POS order with more items (the fix for #22 stops
  the silent data loss by disabling the grid with a clear message, but
  building an actual "add a second round" flow is a feature, not a bug fix).
- The "Known Remaining Work" items already called out in [README.md](README.md)
  §11 (split-bill-by-item, direct inventory deduction for non-recipe items,
  most print documents, printer routing UI, an automated test suite) — none
  of those are bugs, they're already-documented scope gaps.
