# 12.3.15 — Concurrency & data-integrity fixes (found by the new QA / production-load suite)

Server-side only (`db.js`, `server.js`, new `bcryptPool.js`). **No screens changed, no schema changes to existing tables, no data migration.** One new small table (`op_locks`) is created automatically at start-up.

## Stock & costing — what was wrong
Several inventory actions read an item, worked on that copy, then saved it back. When two things happened at the same moment (two restocks; a restock during sales; an admin editing an item during a sale) one change silently overwrote the other, or a sale invented "phantom" cost layers. Nothing showed an error.

- **Quick restock lost units under concurrency.** 15 simultaneous restocks of 5 units left the item at 1,010 instead of 1,075 (cost batches said 1,075). Quick restock is now **one locked transaction**: opening layer, quantity (`quantity = quantity + n`), new layer, re-averaged unit cost and purchase log commit together.
- **Phantom cost layers.** A POS "Add stock" (or any restock) overlapping with sales made the stock ledger think units were missing and create extra "opening" layers (8 add-stocks + 8 sales: 120 phantom units). `reconcileStockBatches` and `refreshItemAvgCost` now read the **current** data under the item lock instead of the transaction's older snapshot.
- **FIFO/LIFO cost wrong after "restock before first issue".** An item created with opening stock and restocked before anything was issued had its opening layer priced at the *blended* cost and dated today (100 @ Rs10 + 50 @ Rs20, issue 120: charged Rs1,933, correct Rs1,400). The opening layer is now created at its **original cost** first. Fixed for quick restock **and** procurement restock-receive.
- **Editing an item's quantity or cost while it was selling** (found in the follow-up run: 8 of 8 attempts left quantity ≠ batches). `adjustStockBatches` now updates the item row in the same commit as the layers, and a generic save **no longer overwrites `quantity` / `purchase_cost` of an existing stock item** (new rows and asset items are unaffected). Even renaming an item can no longer undo a concurrent sale.

## Approvals & receiving — double-processing
- **Procurement receive could create duplicate stock** (20 simultaneous receives of a 25-unit order → 20 items, 500 units) and **transfer approval could move stock more than once** (30-unit transfer, 90 moved). Approve/receive actions now take a short **claim on the record** (`op_locks`, database-backed so it also works across several app instances); a second simultaneous request gets **HTTP 409 "Someone else is processing this request"**. Covers admin transfer decision, admin and manager procurement decisions, procurement receive, and the manager-approval route. A claim left by a crashed process expires after 120 s.

## Availability under load
- **Logins no longer freeze the server.** bcrypt (pure-JS, ~⅓ s of CPU per hash) ran on the main thread, so 10 simultaneous logins stalled every till for ~3.4 s. Hashing now runs in **worker threads** (`bcryptPool.js`; no native module; automatic fallback to the old behaviour if workers are unavailable). Other requests now wait ≤ 50 ms during a login storm.
- **Bursts are queued, not rejected.** The database wait-queue default rose from 30 to **500** (`DB_QUEUE_LIMIT`); the number of MySQL connections is unchanged (`DB_CONNECTION_LIMIT` = 8). Before: 200 simultaneous sales → only 38 succeeded, the rest got 503. Now all are served (exactly 50 of 50 units sold, no oversell).
- **Rate limit is per signed-in user, not per IP.** Every till in a school normally reaches the server from one public IP; a shared 600/min bucket made busy tills start getting 429. Signed-in traffic is now counted per user (600/min each); anonymous traffic and the login/OTP limiters stay per IP.

## Smaller fixes
- **Repair status** is validated (`reported`, `in_review`, `in_repair`, `repaired`, `not_repairable`, `cancelled`); anything else returns 400 instead of being stored.
- **Fresh-install warning** `could not create idx_pe_linked_pr on petty_expenses` fixed (index was created before its column existed).
- Item edit now answers with the stored quantity/cost, not the copy read at the start.
- `package.json` version corrected (it still said 12.3.8).

## Behaviour changes to be aware of
- A second simultaneous approve/receive returns **409** instead of silently running twice.
- After a quick restock, the item's unit cost is the weighted average of its cost layers (identical to the old figure whenever layers were consistent).
- A generic item save can no longer change `quantity`/`purchase_cost` of an existing **stock** item. Quantity changes must go through restock, consume, transfer, POS, or the Edit-item screen (which already does it correctly). Seed into an **empty** database (re-seeding over existing data is not a supported path and was not tested with this change).
- Up to two bcrypt worker threads are started per app process.

## Tests added (`tests/qa/`, run with `npm run test:qa`)
78 checks (inventory 34, POS 28, load/races 14, item-edit & manager races 2) plus a 7-scenario × 6-run concurrency matrix and a phantom-layer probe, covering inventory, POS, inter-module information flow, races, and a production-load run (realistic peak + 60-till stress) with SQL audits. See `tests/qa/README.md` and `docs/QA_REPORT_12.3.15.md`. **Run them on a test database only.**

## Not changed / known limits
- bcrypt logins still take ~0.4 s of CPU each; 10 at once take ~4 s *in total* on a single core, but nobody else is blocked.
- Beyond roughly 500 simultaneously queued database operations the server still answers 503 (by design, to protect the database).
- Tested as a single Node process on one CPU core against MariaDB 10.11; multi-instance behaviour is designed for (database-level claims) but was not exercised. Browser UI, printing, email delivery, file uploads and CSV import were not part of this test pass.
