Dentolize · Stock Counts Walkthrough
On this pageCore lifecycle — happy pathsThe locking guarantee — highest-value area to testPermission-scoping edgesCounting-input edge casesExcel upload edge casesCross-cutting / regression risk

For Quality

Core lifecycle — happy paths

  • Open a count against a single storage → confirm "counted" state of every location with amount > 0 is copied, and locations at amount = 0 are correctly excluded.
  • Open a count with the storage field empty → verify every storage in the company gets lockedByStockCountId set, 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/countedAmountBox pair should stay mathematically consistent with the line's frozen box size.
  • Confirm with "Confirm without changing stock" → count becomes CONFIRMED, no inventory order is created, stockUpdated stays false, storage lock is released.
  • Confirm with "Confirm and update stock" with at least one variance → an ADJUSTMENT order is created, stockUpdated becomes true, inventoryOrderId is set, and the resulting location amounts match expected + 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.length check) — 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, StockCount and all its StockCountItem rows 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: null lookup) 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: true and 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 (no VIEW_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 by isSameCompanyAsStockCount, 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_COUNT but not ADD_INVENTORY_ADJUSTMENT/DO_ALL: can confirm a count, but the updateStock: true request 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 stockCount id 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: null on an already-counted line → line reverts fully to uncounted (countedAmount, countedValue, varianceAmount, countedAt, countedById all null), and the parent count's countedItems decrements accordingly.
  • Count a line, then recount it with a different value — verify varianceAmount/varianceValue update, and the parent count's rolled-up totals reflect only the latest value, not a sum of both attempts.
  • An item whose box size is 1 — 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 < 0 check).

Excel upload edge cases

  • A row whose Code doesn'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 + Storage fallback 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 code values 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, and ADJUSTMENT itself) routes through the same shared assertStoragesAreNotBeingCounted check — 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.js picked 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/countedValue are 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).