SOP: Shrinkage Investigation
Document ID: WMS-AUD-002 Version: 1.0 Effective date: 04/30/2026 Owner: Warehouse Operations Manager Next review: [six months from effective date] Applies to: Managers and the warehouse operations manager investigating chronic variance, recurring shorts, or unexplained inventory loss
1. Purpose
This procedure governs how to investigate shrinkage — the cumulative gap between system inventory and physical inventory that isn't explained by sales, returns, or documented adjustments. Shrinkage shows up in the WMS as: chronic negative variance on cycle counts, recurring short picks that don't reconcile, locations with persistent under-counts, SKUs that vanish faster than the order book explains.
This SOP picks up where WMS-AUD-001 §5.4 stops. AUD-001 surfaces the patterns; this SOP acts on them. The output is one of: a process fix, a system fix, a training conversation, a vendor follow-up, an HR / loss-prevention escalation, or a documented financial write-off.
Shrinkage investigations are time-bounded and decision-focused. The audit trail is rich enough to localize most issues within a few hours; the discipline is to triage quickly and act, not to chase every variance forever.
2. Scope
In scope:
- Detecting shrinkage signals: chronic SKU variance, location chronic variance, counter-pattern variance, recurring shorts
- Walking the audit trail:
inventory_events,cycle_count_audits,task_events(PICKING + PACKING),audit_logs(receiving),inventory_adjustments,fulfillment_events - Triage by SKU, location, user, and shift
- The four root-cause buckets: process error / system error / vendor error / human factor
- Documenting findings and the resolution decision
- Escalation paths to HR / loss prevention, IT, vendor, or financial write-off
Out of scope:
- The cycle-count approval mechanics themselves — see WMS-INV-002 §4.8 and WMS-AUD-001
- Reactive variance resolution (the "do this immediately after a short pick" procedure) — see WMS-PICK-003
- Receiving-time variance investigation (PO vs. received) — see WMS-AUD-003 (when written), different table, different shape
- Customer-side disputes ("you shipped wrong / short" claims) — see WMS-AUD-004 (when written)
- Cannabis / regulated-product destruction audit — see WMS-INV-005 §6 for that subset
3. Roles & permissions
API enforcement: all the audit-table query endpoints exposed through the WMS UI are auth-only. The bulk of this SOP runs against the database directly via SQL (
#5queries) — that's how investigations work in practice. Database access for the warehouse operations manager is the assumed baseline.
| Role | Pull audit queries | Localize patterns | Decide resolution | Escalate to HR / LP |
|---|---|---|---|---|
| READONLY | ✓ via UI surfaces | — | — | — |
| STAFF | ✓ via UI surfaces | — | — | — |
| MANAGER | ✓ via UI + read-only DB | ✓ | ✓ for routine cases | ✓ |
| ADMIN | ✓ full DB | ✓ | ✓ | ✓ |
| SUPER_ADMIN | ✓ full DB | ✓ | ✓ | ✓ |
Operational expectations:
- Investigations are confidential until closed. Don't discuss a counter or picker who's under investigation with peers. The audit trail is the evidence; conjecture is not.
- Document every decision. Even "decided no further action" is a decision. The case file is the audit of the audit.
- Two-person rule for HR / loss-prevention escalations. The warehouse operations manager + one other manager review the evidence together before any disciplinary action. The system can't catch every bug; people are the last line of defense.
- Time-box investigations. A typical SKU-level shrinkage investigation should reach a decision within 5 business days of opening the case. Longer = stale data + lost institutional memory. If you can't decide in 5 days, write up what you found and escalate to senior leadership for direction.
4. Procedures
4.1 Detect — what triggers an investigation
A shrinkage investigation opens when one or more of these surface:
| Signal | Source | Threshold |
|---|---|---|
| SKU with 3+ cycle-count variance sessions in 90 days, net negative | WMS-AUD-001 §5.4 by-SKU query | Net loss >10% of received quantity for the period |
| Location with chronic variance | WMS-AUD-001 §5.4 by-location query | total_units_off > 50 over 90 days |
| Counter with consistently high variance vs. peers | WMS-AUD-001 §5.4 by-counter query | avg_variance_per_session >2x peer median |
| Recurring shorts on a SKU | WMS-PICK-003 §5.3 weekly shorts query | >5 shorts on same SKU in 30 days |
| Customer "wrong item / missing item" returns clustering | WMS-RET-002 (when written) | >3 returns on same SKU per month coded as wrong-pack |
| Sudden drop in available inventory not explained by sales | manual observation | unexpected gap with no inventory_events correlation |
| Anonymous tip / ethics report | outside the WMS | always investigate, never dismiss |
Open the case file (paper or digital — your operation's choice). Use a template:
Case: SHRK-{YYYY-MM-DD}-{nn}
Opened: {date} by {manager}
Signal: {which trigger above; raw query results attached}
Suspected scope: {SKU(s) / location(s) / user(s) / time window}
Status: OPEN | TRIAGING | RESOLVED | ESCALATED
4.2 Triage — narrow the scope
The first hour is about narrowing. You don't need to know what happened yet — you need to know what slice of the warehouse is involved.
Step 1 — Confirm the signal. Re-run the query that triggered the investigation. Chronic variance from 90 days ago might not be chronic this week; SKU patterns can resolve themselves through normal flow.
Step 2 — Pull inventory_events for the affected SKU and location (last 30 days):
SELECT
e.createdAt,
e.type,
u.name AS who,
fl.name AS from_loc,
tl.name AS to_loc,
iu.quantity AS unit_qty_after,
v.sku,
e.payload
FROM inventory_events e
JOIN inventory_units iu ON e.inventoryUnitId = iu.id
JOIN product_variants v ON iu.productVariantId = v.id
LEFT JOIN locations fl ON e.fromLocationId = fl.id
LEFT JOIN locations tl ON e.toLocationId = tl.id
LEFT JOIN users u ON e.userId = u.id
WHERE v.sku = '{sku}'
AND e.createdAt > NOW() - INTERVAL '30 days'
ORDER BY e.createdAt DESC;
This returns every move, damage, adjustment, and split for the SKU. The seven inventory event types (per packages/pubsub/src/index.ts):
| Event type | Means |
|---|---|
inventory:received | Stock landed via receiving |
inventory:unit_moved | Stock moved between locations (per WMS-INV-001 §4.1) |
inventory:unit_split_source | Source of a split — partial qty moved out |
inventory:unit_split_dest | Destination of a split — new unit at new location |
inventory:unit_adjusted | Lightweight adjust per WMS-INV-001 §4.2 |
inventory:unit_damaged | Full unit marked damaged per WMS-INV-001 §4.3 |
inventory:unit_damaged_source / inventory:unit_damaged_dest | Partial damage split (some units kept, some marked damaged) |
Step 3 — Cross-reference with the action streams. Each variance can usually be traced to one of:
If the SKU's last inventory_events shows... | Look at... |
|---|---|
A pick task (task_events) that completed near the variance time | task_events WHERE eventType = 'ITEM_SHORT' AND ... for the same time window |
| A receiving session in the variance time window | audit_logs WHERE entityType = 'RECEIVING_SESSION' for that session — was it approved correctly? |
| A cycle count adjustment | cycle_count_audits for that session — does the count make sense? |
Nothing in inventory_events for the period | The variance may not be a write — could be a count error, a sale not yet syncing, or shrinkage with no system trace |
Step 4 — Localize by user and shift. If the affected SKU was touched (picked, packed, counted, moved) by a small number of users, that's your candidate set:
SELECT
u.name AS who,
COUNT(*) AS event_count,
MIN(e.createdAt) AS first_touch,
MAX(e.createdAt) AS last_touch
FROM inventory_events e
JOIN inventory_units iu ON e.inventoryUnitId = iu.id
JOIN product_variants v ON iu.productVariantId = v.id
JOIN users u ON e.userId = u.id
WHERE v.sku = '{sku}'
AND e.createdAt > NOW() - INTERVAL '30 days'
GROUP BY u.name
ORDER BY event_count DESC;
Plus the picker / packer streams — if task_events for the SKU clusters around one or two users, you have a candidate. This is not yet evidence of wrongdoing. It's just the candidate set for further investigation.
Step 5 — Localize by time. Pull a histogram of variance events by hour-of-day or day-of-week. Theft tends to cluster (end-of-shift, weekends, before holidays). Process errors don't.
SELECT
EXTRACT(HOUR FROM e.createdAt) AS hour,
COUNT(*) AS event_count
FROM inventory_events e
JOIN inventory_units iu ON e.inventoryUnitId = iu.id
JOIN product_variants v ON iu.productVariantId = v.id
WHERE v.sku = '{sku}'
AND e.type IN ('inventory:unit_damaged', 'inventory:unit_adjusted')
AND e.createdAt > NOW() - INTERVAL '90 days'
GROUP BY hour
ORDER BY event_count DESC;
4.3 Categorize — the four root-cause buckets
Every variance traces to one of four buckets. Before deciding action, place the case in a bucket:
| Bucket | Signal | Examples |
|---|---|---|
| Process error | The right thing was tried but the wrong outcome happened — usually due to procedural ambiguity | Counter recounted a bin where there was a hidden second pallet of the same SKU; picker pulled from the wrong rack of identical-looking products; receiver entered wrong qty on a non-blind count |
| System error | The WMS itself produced an inconsistent state | Multi-bin pack overwriting quantityPicked (per WMS-PACK-001 §4.6); allocation race producing two Allocation rows; stale cache showing wrong qty |
| Vendor error | The variance traces back to receiving — the WMS recorded what was claimed, but vendor under-shipped or sent wrong product | Receiving variance not caught at intake; PO discrepancy found weeks later; vendor packing-slip mismatch |
| Human factor | A person, with intent or not, removed product or falsified records | Theft (with or without scanning); pencil-whipped count; collusion with vendor; sabotage |
The audit trail mostly distinguishes the first three from the fourth. The first three have inventory_events explaining what happened. The fourth often doesn't — units disappear with no corresponding write.
⚠ Don't rush to the human-factor bucket. Most variance is process or system. Theft is the rarest cause but the most expensive when it's real. Exhaust the other three buckets first; if the audit trail can't explain the variance with a process / system / vendor cause, then treat it as human-factor and follow §4.6 escalation. Calling theft on what's actually a system bug poisons trust and makes the real fix harder.
4.4 Resolve — process or system
Most cases land here. Once you've identified the cause:
Process fixes (most common):
- Update the relevant SOP. If pickers are confusing two SKUs that look similar, document the visual distinction in the SKU's variant notes and brief the team.
- Move the affected SKU's pick face for visual clarity (per WMS-INV-004 §4.2).
- Increase cycle-count frequency for the affected SKU or location until the pattern resolves (per WMS-INV-002 §4.x).
- Add a barcode-verification step (when WMS-PICK-001 §8 wires server-side scan validation, this becomes default).
System fixes:
- File an engineering ticket against the relevant SOP's §8 (escalation). Cross-reference the case file ID.
- If the bug is reproducible, attach a minimal reproduction. The audit trail rows + a SQL query that exposes the inconsistency are the gold standard.
- Until the fix lands, document the workaround and circulate to managers.
Vendor fixes:
- The buyer (not the warehouse) drives the vendor conversation. Provide the audit evidence (receiving session approval audit + subsequent variance) to the buyer.
- Adjust receiving SOPs if a particular vendor needs tighter intake (e.g., 100% blind count vs. spot-check).
- Track via WMS-AUD-003 (when written) — receiving-side variance is its own SOP family.
Document the resolution in the case file:
Case: SHRK-2026-04-29-03
Status: RESOLVED
Bucket: process
Root cause: Pickers were pulling from RACK A-01 instead of RACK A-02 because the labels were nearly identical.
Resolution: Relabeled both racks with high-contrast color-coded labels. Updated WMS-INV-004 §X to note label color convention. Briefed pick team 2026-04-29.
Cycle count of affected location 2026-04-30 to verify: passed.
Signed: {manager}, {date}
4.5 Resolve — financial write-off
Some variance will never be explainable. After a reasonable investigation, the loss has to be booked.
Steps:
- Confirm with the buyer + finance that the SKU's value justifies a write-off vs. continuing investigation.
- Run a final cycle count of the affected location(s) per WMS-INV-002 §4.x.
- Approve per WMS-INV-002 §4.8 — the resulting
InventoryAdjustmentis the formal write-off record. - Note the case file ID in the cycle-count
reviewNotesso the financial write-off and the investigation are linked. - Mark the case
RESOLVED — write-offwith the dollar amount and approval signature.
The system has no separate "write-off" workflow — it's just a cycle count with a documented case file behind it. Per WMS-INV-007 §4.1 Path A, the resulting InventoryAdjustment row carries reason: 'CYCLE_COUNT' and sourceId pointing at the session. Linking that back to the case file is operational, not enforced.
4.6 Escalate — human factor
The hardest path, used last.
Trigger conditions (any of):
- Audit trail shows variance with no system explanation across multiple events
- Variance localizes to a small candidate set of users with no other explanation
- Anonymous tip + supporting audit-trail evidence
- Pattern matches known theft signatures (clustering at end-of-shift, weekend, near specific SKUs)
Steps:
- Stop. Do not act unilaterally. The two-person rule applies — warehouse operations manager + one other manager review the evidence together.
- Compile the case file: queries, raw row dumps, time-correlation, candidate-user analysis. The case file must contain the evidence; nothing should be reconstructed from memory later.
- Escalate to HR per your organization's procedure. The warehouse manager hands the case file to HR and steps back from the process — direct confrontation by the warehouse manager is a labor-relations risk.
- Do not confront the suspected user yourself, share suspicion with peers, or change the user's WMS access pre-emptively. HR coordinates. If WMS access needs to change (e.g., to preserve evidence), that's an admin's decision, not the warehouse manager's, and it happens through formal channels.
- Document the escalation in the case file. Status:
ESCALATED. The case stays open until HR closes it.
What HR does (paraphrased — this is not your concern as the warehouse manager):
- Interviews per their procedure
- Reviews evidence with legal counsel if applicable
- Decides outcome (coaching, performance plan, suspension, termination, criminal referral)
- Returns case status to warehouse operations manager
After resolution, file an after-action review:
- What signal was the earliest indicator? Could we have caught it sooner?
- What process change closes the gap that allowed the loss?
- What audit-trail enhancement would have made detection automatic?
These after-action items become engineering tickets in §8 of relevant SOPs.
5. Reference
5.1 The full audit-trail shopping list
For an investigation, you may need to query any of these tables. Each has its own structure; cross-correlation by createdAt is the common technique.
| Table | What it captures | SOP reference |
|---|---|---|
inventory_events | All inventory unit lifecycle events (received, moved, damaged, adjusted, split) | This SOP §4.2 |
cycle_count_audits | Per-session count actions (start, count, submit, approve, reject, reopen) | WMS-AUD-001 §5.1 |
cycle_count_sessions | The session-level state with reviewer + timestamps | WMS-INV-002 |
task_events (type=PICKING) | Pick events: ITEM_COMPLETED, ITEM_SHORT | WMS-PICK-001 §5.3 |
task_events (type=PACKING) | Pack events: TASK_COMPLETED with weight/dimensions | WMS-PACK-001 §5.3 |
audit_logs | Receiving session audit (RECEIVING_SESSION, RECEIVING_LINE) | WMS-REC-003 §6 |
inventory_adjustments | Formal adjustments — the only table that records dollar-impact variance | WMS-INV-007 §5 |
fulfillment_events | Order-level events (allocated, backordered, picked, packed, shipped) | WMS-INV-006 §5.2 |
5.2 Net shrinkage by SKU (the executive summary)
For a quarterly board report:
SELECT
v.sku,
v.name,
-- Received in period
COALESCE(SUM(CASE WHEN e.type = 'inventory:received' THEN (e.payload->>'quantity')::int ELSE 0 END), 0) AS received,
-- Sold in period (via shipped order items)
COALESCE((
SELECT SUM(oi.quantityShipped)
FROM order_items oi
WHERE oi.productVariantId = v.id
AND oi.updatedAt > NOW() - INTERVAL '90 days'
), 0) AS shipped,
-- Adjustments in period (negative = shrinkage)
COALESCE((
SELECT SUM(a.changeQty)
FROM inventory_adjustments a
WHERE a.productVariantId = v.id
AND a.approvedAt > NOW() - INTERVAL '90 days'
), 0) AS net_adjustment,
-- Net change should be: received - shipped + adjustments
-- Persistent gap = unexplained shrinkage
COALESCE(SUM(CASE WHEN e.type = 'inventory:received' THEN (e.payload->>'quantity')::int ELSE 0 END), 0)
- COALESCE((SELECT SUM(oi.quantityShipped) FROM order_items oi WHERE oi.productVariantId = v.id AND oi.updatedAt > NOW() - INTERVAL '90 days'), 0)
+ COALESCE((SELECT SUM(a.changeQty) FROM inventory_adjustments a WHERE a.productVariantId = v.id AND a.approvedAt > NOW() - INTERVAL '90 days'), 0)
AS net_change_calc
FROM product_variants v
LEFT JOIN inventory_units iu ON iu.productVariantId = v.id
LEFT JOIN inventory_events e ON e.inventoryUnitId = iu.id AND e.createdAt > NOW() - INTERVAL '90 days'
WHERE v.active = true
GROUP BY v.id, v.sku, v.name
HAVING COALESCE(SUM(CASE WHEN e.type = 'inventory:received' THEN (e.payload->>'quantity')::int ELSE 0 END), 0) > 0
ORDER BY net_change_calc ASC
LIMIT 50;
The SKUs at the top (most negative net_change_calc) are the shrinkage candidates. Cross-reference each against the §4.2 audit walk before opening cases.
5.3 Chronic-variance counter detection (long form)
WMS-AUD-001 §5.4 already has the by-counter query for cycle-count sessions. For a fuller picture across all touchpoints:
WITH counter_activity AS (
SELECT
u.id, u.name,
COUNT(*) FILTER (WHERE e.type = 'inventory:unit_damaged') AS damages_marked,
COUNT(*) FILTER (WHERE e.type = 'inventory:unit_adjusted') AS adjustments_made,
COUNT(*) FILTER (WHERE e.type = 'inventory:unit_moved') AS moves_made
FROM users u
LEFT JOIN inventory_events e ON e.userId = u.id
AND e.createdAt > NOW() - INTERVAL '90 days'
GROUP BY u.id, u.name
),
counter_picks AS (
SELECT
u.id, u.name,
COUNT(*) FILTER (WHERE te.eventType = 'ITEM_COMPLETED') AS picks_complete,
COUNT(*) FILTER (WHERE te.eventType = 'ITEM_SHORT') AS picks_short,
ROUND(100.0 * COUNT(*) FILTER (WHERE te.eventType = 'ITEM_SHORT')
/ NULLIF(COUNT(*), 0), 1) AS short_rate_pct
FROM users u
LEFT JOIN task_events te ON te.userId = u.id
AND te.createdAt > NOW() - INTERVAL '90 days'
AND te.eventType IN ('ITEM_COMPLETED', 'ITEM_SHORT')
GROUP BY u.id, u.name
)
SELECT
ca.name,
ca.damages_marked,
ca.adjustments_made,
ca.moves_made,
cp.picks_complete,
cp.picks_short,
cp.short_rate_pct
FROM counter_activity ca
LEFT JOIN counter_picks cp ON ca.id = cp.id
WHERE ca.damages_marked + ca.adjustments_made > 5
OR cp.short_rate_pct > 5
ORDER BY (ca.damages_marked + ca.adjustments_made + COALESCE(cp.picks_short, 0)) DESC;
A counter at the top with significantly more damages, adjustments, AND short picks than peers is the candidate. One high metric is noise; three is signal.
5.4 Time-of-day clustering for theft signature
SELECT
EXTRACT(HOUR FROM e.createdAt) AS hour,
COUNT(*) FILTER (WHERE e.type IN ('inventory:unit_damaged', 'inventory:unit_adjusted')) AS damage_or_adjust,
COUNT(*) FILTER (WHERE e.type = 'inventory:unit_moved') AS moves
FROM inventory_events e
WHERE e.createdAt > NOW() - INTERVAL '90 days'
GROUP BY EXTRACT(HOUR FROM e.createdAt)
ORDER BY hour;
A spike at end-of-shift hours (e.g., 16:30–17:00 if shift ends at 17:00), or in the last hour before a weekend, is a signature worth investigating. Compare against your normal pick / pack distribution — if pick volume is uniform but damage / adjust spikes at 16:45, that's atypical.
5.5 Related SOPs
- WMS-INV-001 — Per-unit operations (move, adjust, damage)
- WMS-INV-002 — Cycle counts (the formal reconciliation point)
- WMS-INV-007 — Formal adjustments (the dollar-impact ledger)
- WMS-PICK-003 — Short-pick recovery (where shorts get reconciled — failure to reconcile feeds shrinkage)
- WMS-AUD-001 — Cycle-count approval review (the upstream that surfaces the patterns this SOP investigates)
- WMS-AUD-003 — Receiving variance (when written) — vendor-side root cause investigation
- WMS-AUD-004 — Customer dispute resolution (when written) — customer-side variance signals
6. Audit & compliance
This SOP is the audit. Its outputs — case files, queries run, decisions made — feed regulatory audits (cannabis / vape inventory reporting), insurance claims, and HR investigations.
Case file standard: every investigation, regardless of outcome, gets a case file. The case file lives in your operation's secure document store (encrypted, access-controlled, retention-policied).
Required case-file contents:
- Case ID (
SHRK-{YYYY-MM-DD}-{nn}) - Open date + opening manager
- Signal that triggered (query results attached)
- Scope (SKUs / locations / users / time window)
- Audit-trail evidence (queries run with results)
- Triage findings (process / system / vendor / human-factor candidates considered)
- Resolution decision + rationale
- Resolution date + closing manager signature
- For escalations: HR ticket reference, outcome (when known), after-action review
Retention: at minimum 7 years (most US tax / insurance requirements), longer for cannabis / vape per state regulation.
Quarterly governance (Warehouse Operations Manager + Finance):
- Total shrinkage value (sum of negative
inventory_adjustmentsfromsourceType: 'CYCLE_COUNT') per quarter - Investigations opened vs. resolved vs. escalated this quarter
- Top 10 SKUs by net negative variance — buyer review
- Top 5 locations by net negative variance — physical review
- Trends over rolling 4 quarters
Annual governance (CEO / COO + Warehouse Operations Manager):
- Total shrinkage as % of inventory value — industry benchmark comparison (typically
1-2%for ecommerce,>3%is concerning) - HR / loss-prevention escalations and outcomes
- After-action recommendations from the year's cases — implementation status
7. Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
Suspected variance but inventory_events shows nothing for the SKU | Either the variance is older than the query window, or the variance is genuine shrinkage with no system trace (theft, count error never reconciled) | Widen the window. If nothing surfaces over 6 months, treat as untraceable shrinkage — write off via §4.5 with case-file documentation. |
| Variance localizes to a small candidate set, but you can't tell process from human-factor | Audit trail is ambiguous — the events recorded are consistent with both | Apply the rule: process if the events make procedural sense (e.g., damages happened during pack which is normal); human-factor if the events are anomalous (e.g., adjustments happened with no associated pick/pack/count). |
| Re-running the trigger query a week later shows no pattern | Pattern resolved through normal flow — receiving math caught up, cycle counts cleaned up the variance | Close case as RESOLVED — pattern cleared, no further action. Note the close in the case file. Don't archive prematurely; some patterns oscillate. |
| Counter who was investigated is now requesting their case file | HR-related concern | Per your organization's HR procedure. The warehouse manager's role is to provide the evidence to HR; access to case files for the involved party is HR's call, not the manager's. |
| Cycle count shows positive variance in the same SKU and location after an investigation | Stock that was lost was found — system was wrong, not physical reality | Update the case file. Run an offset cycle count. Note the case as RESOLVED — count error, not loss. |
| Investigation is dragging past 5 days with no decision | Either the data is genuinely ambiguous, or you're chasing diminishing returns | Write up findings and escalate to senior leadership for direction. Fixed-time-box discipline matters. |
| The same SKU appears in three concurrent investigations | Fragmented case management | Consolidate into one parent case; attach the children. Single SKU, single investigation. |
| Audit trail row was deleted (impossible per schema, but suspected) | DB tampering — cycle_count_audits and inventory_events are insert-only at the application layer but the schema doesn't enforce this | IT escalation. Pull pg_audit / pgaudit logs (if enabled) for direct DB writes. If not enabled, enable it now — see §8. |
| Investigation surfaces a pattern that's actually a known system bug | Cross-check engineering tickets first | Note the bug reference in the case file. Resolution: case stays open until the bug is fixed and a follow-up cycle count clears the variance. |
8. Escalation
- Enable database-level audit logging (
pgauditextension) oninventory_events,cycle_count_audits,audit_logs,inventory_adjustments. The application layer enforces insert-only via service-layer discipline; the database doesn't. A directDELETEfrom a privileged user would erase evidence. Audit logging at the DB level is the only defense. - Build a shrinkage dashboard. The reports page is a placeholder (per
apps/web/src/pages/reports/index.tsx). A dedicated/reports/shrinkageview that runs the §5.2 query daily and surfaces the top variance SKUs would close the detection-discovery gap. Cross-reference: this SOP currently relies on managers running ad-hoc SQL. - Auto-flag suspicious patterns. A weekly cron that runs §5.3 and §5.4 and emails the warehouse operations manager is cheap. The manager doesn't need to remember to look — the system reminds them.
- Tighten the receiving variance audit — per WMS-REC-003 §6, receiving has its own audit trail in
audit_logs. WMS-AUD-003 (when written) is the parallel investigation SOP for vendor-side variance. Until then, follow this SOP's §4.2 walk-the-trail discipline pointed at receiving instead of inventory operations. - Two-person counts for high-shrinkage SKUs. If a SKU has shown chronic variance, the next 3 cycle counts of its locations should require two counters and two reviewers (cross-reference WMS-INV-002 §8). The pattern reduction of two-person counts is well-documented; the WMS doesn't enforce it; operational discipline does.
- Suspected systemic theft (more than one SKU, more than one user, distributed pattern): immediate escalation to senior leadership + outside loss-prevention consulting. Patterns at this scale are not for in-house warehouse management to resolve alone.
- Suspected vendor fraud (variance traces consistently to one vendor): buyer + finance + legal. The vendor relationship is the buyer's; the audit evidence is the warehouse's contribution.
9. Revision history
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | [DATE] | [NAME] | Initial release. Documents the procedural workflow for shrinkage investigation — detection → triage → categorize (process / system / vendor / human factor) → resolve (process fix, system ticket, vendor follow-up, financial write-off) or escalate (HR / loss prevention). Provides production-ready SQL queries for net shrinkage by SKU (§5.2), chronic-variance counter detection across all touchpoints (§5.3), and time-of-day theft signature analysis (§5.4). Catalogs the seven inventory event types (per packages/pubsub/src/index.ts: inventory:received, inventory:unit_moved, inventory:unit_split_source, inventory:unit_split_dest, inventory:unit_adjusted, inventory:unit_damaged, inventory:unit_damaged_source/_dest). Documents the four root-cause buckets and the audit-trail signature each leaves. Documents the two-person rule for human-factor escalations and the case-file standard required for retention compliance (7+ years US, longer for regulated products). Cross-references WMS-INV-001/002/007 (inventory operations producing the audit trail), WMS-PICK-003 (short-pick recovery as the upstream reciprocal procedure), WMS-AUD-001 (the cycle-count approval review that surfaces patterns this SOP acts on), WMS-AUD-003 / WMS-AUD-004 (when written — vendor-side and customer-side investigations). Code references: packages/db/prisma/schema/inventory.prisma:126-142 (inventory_events model), packages/pubsub/src/index.ts (event-type constants), packages/domain/src/services/inventory.service.ts:211-295 (event emit pattern). Notes that the reports page is a placeholder (apps/web/src/pages/reports/index.tsx) — a true shrinkage dashboard is engineering work flagged in §8. |