On this page
Core lifecycle — happy pathsThe locking guarantee — highest-value area to testPermission-scoping edgesCounting-input edge casesExcel upload edge casesCross-cutting / regression riskFor Quality
Core lifecycle — happy paths
- Open a count against a single storage → confirm "counted" state of every location with
amount > 0is copied, and locations atamount = 0are correctly excluded. - Open a count with the storage field empty → verify every storage in the company gets
lockedByStockCountIdset, not just the ones with stock. - Count a line in items, then switch the toggle to boxes and re-view it (and vice versa) — the stored
countedAmount/countedAmountBoxpair should stay mathematically consistent with the line's frozenboxsize. - Confirm with "Confirm without changing stock" → count becomes
CONFIRMED, no inventory order is created,stockUpdatedstaysfalse, storage lock is released. - Confirm with "Confirm and update stock" with at least one variance → an
ADJUSTMENTorder is created,stockUpdatedbecomestrue,inventoryOrderIdis set, and the resulting location amounts matchexpected + variance(i.e. counted). - Confirm with "Confirm and update stock" where nothing varied (all counted lines matched expected exactly) → no order should be created at all (
variances.lengthcheck) — worth an explicit regression test since it's an easy off-by-one to create an empty order. - Cancel an open count → storages unlock, status becomes
CANCELED, no inventory order, count can no longer be edited/confirmed/deleted. - Delete an open count → storages unlock,
StockCountand all itsStockCountItemrows are gone (cascade).
The locking guarantee — highest-value area to test
This is the part of the PR with clinic-wide blast radius if wrong, so it deserves the most adversarial testing:
- With a count open on storage A, attempt a purchase/usage/transfer/sale/disposal/adjustment into or out of storage A from another session — every one should be refused with
"{storage name} Is Being Counted In Stock Count #{number}". - Same, but the transaction touches a different, uncounted storage — should succeed normally. (Confirms the lock is per-storage, not global, for a single-storage count.)
- With an all-storages count open, attempt any inventory movement against any storage, including one created after the count opened — should be refused with
"Every Storage Is Being Counted In Stock Count #{number}". This second case specifically exercises the fallback check (inventoryStorageId: nulllookup) rather than the per-storage pointer, since a storage created after the count opened never got a lock pointer written to it. - Race: attempt to open two counts against the same storage (or overlapping all-storages counts) as close to simultaneously as your test tooling allows — exactly one should succeed; the other should fail with
"A Stock Count Is Already Open For One Of These Storages", never both succeeding or both silently locking the same storage. - Confirm a count with
updateStock: trueand verify the resulting adjustment order is not itself blocked by the count's own lock (it shouldn't be, since the lock is released before the order is created — but this ordering is exactly the kind of thing that regresses silently if refactored). - Transfers and distributions reference two storages (
fromStorage/toStorage) — verify a lock on either side blocks the transaction, not just the source.
Permission-scoping edges
- A user with only
VIEW_CREATED_STOCK_COUNT(noVIEW_STOCK_COUNT): the counts list should show only counts they created — verify against a fixture with counts from multiple users. - Same user, attempting to hit
stockCountDetails/editStockCountItems/confirmStockCount/ etc. directly with the id of a count they didn't create — should be denied byisSameCompanyAsStockCount, not just filtered out of the list. This is the more important test: list-only filtering would be insufficient, and the rule closes that gap independently — verify it actually does. - A user with
EDIT_STOCK_COUNTbut notADD_INVENTORY_ADJUSTMENT/DO_ALL: can confirm a count, but theupdateStock: truerequest should be rejected server-side (Not Authorised) even if the disabled frontend button were bypassed (e.g. a direct API call) — worth testing the API directly, not just the disabled button. - Cross-company access: a
stockCountid belonging to a different company, hit from any query/mutation — should be denied, not leak a 404-vs-403 distinction that reveals existence.
Counting-input edge cases
- Save a count with
amount: nullon an already-counted line → line reverts fully to uncounted (countedAmount,countedValue,varianceAmount,countedAt,countedByIdallnull), and the parent count'scountedItemsdecrements accordingly. - Count a line, then recount it with a different value — verify
varianceAmount/varianceValueupdate, and the parent count's rolled-up totals reflect only the latest value, not a sum of both attempts. - An item whose
boxsize is1— the box/item toggle should be disabled (nothing to convert), confirm this in both web and mobile. - Negative counted amounts — should be rejected client- and server-side (
amount < 0check).
Excel upload edge cases
- A row whose
Codedoesn't match anything in this count — should be skipped and reported, not silently ignored or matched to the wrong line. - A row matched via the
SKU+Storagefallback where the same SKU exists in two different storages within the same count (all-storages count) — verify it matches the row's storage column correctly and doesn't cross-contaminate the wrong storage's line. - A row with a blank counted-amount cell — should leave that line untouched (still "not counted"), not be treated as counting it as zero.
- More than 5000 rows in the uploaded file — confirm the stated row cap is actually enforced (client-side drag-drop copy says "Max 5000 Row").
- Uploading against a count that has since been confirmed/cancelled between file selection and submit — should fail cleanly (
This stock count is already closed), not partially apply. - A count with duplicate
codevalues across rows in the sheet (shouldn't be possible given@@unique([stockCountId, code]), but worth confirming the upload handles a sheet with accidental duplicate rows for the same code — later row should just overwrite, or be reported).
Cross-cutting / regression risk
- Every non-count inventory mutation type (
PURCHASE,USAGE,TRANSFER,DISTRIBUTION,RETURN,SALE,REFUND,DISPOSAL,PROFIT, andADJUSTMENTitself) routes through the same sharedassertStoragesAreNotBeingCountedcheck — a regression suite should exercise the lock against each type individually, not just one representative type, since this is exactly the kind of shared-path change where one type could be missed in a future refactor. mergeInventoryStorage.jspicked up 134 new/changed lines in this PR alongside the lock check — worth a focused diff review/regression pass on non-count-related inventory behavior in that file specifically, since it's the highest-traffic file this PR touches outside the new count code itself.- Value/price consistency: a count's
expectedValue/countedValueare computed against the price frozen at count-open time — if an item's price changes while a count is open, verify the count's figures do not silently follow the new price (they shouldn't; this is a specific design guarantee worth a regression test).