Skip to main content

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 (#5 queries) — that's how investigations work in practice. Database access for the warehouse operations manager is the assumed baseline.

RolePull audit queriesLocalize patternsDecide resolutionEscalate 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:

SignalSourceThreshold
SKU with 3+ cycle-count variance sessions in 90 days, net negativeWMS-AUD-001 §5.4 by-SKU queryNet loss >10% of received quantity for the period
Location with chronic varianceWMS-AUD-001 §5.4 by-location querytotal_units_off > 50 over 90 days
Counter with consistently high variance vs. peersWMS-AUD-001 §5.4 by-counter queryavg_variance_per_session >2x peer median
Recurring shorts on a SKUWMS-PICK-003 §5.3 weekly shorts query>5 shorts on same SKU in 30 days
Customer "wrong item / missing item" returns clusteringWMS-RET-002 (when written)>3 returns on same SKU per month coded as wrong-pack
Sudden drop in available inventory not explained by salesmanual observationunexpected gap with no inventory_events correlation
Anonymous tip / ethics reportoutside the WMSalways 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 typeMeans
inventory:receivedStock landed via receiving
inventory:unit_movedStock moved between locations (per WMS-INV-001 §4.1)
inventory:unit_split_sourceSource of a split — partial qty moved out
inventory:unit_split_destDestination of a split — new unit at new location
inventory:unit_adjustedLightweight adjust per WMS-INV-001 §4.2
inventory:unit_damagedFull unit marked damaged per WMS-INV-001 §4.3
inventory:unit_damaged_source / inventory:unit_damaged_destPartial 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 timetask_events WHERE eventType = 'ITEM_SHORT' AND ... for the same time window
A receiving session in the variance time windowaudit_logs WHERE entityType = 'RECEIVING_SESSION' for that session — was it approved correctly?
A cycle count adjustmentcycle_count_audits for that session — does the count make sense?
Nothing in inventory_events for the periodThe 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:

BucketSignalExamples
Process errorThe right thing was tried but the wrong outcome happened — usually due to procedural ambiguityCounter 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 errorThe WMS itself produced an inconsistent stateMulti-bin pack overwriting quantityPicked (per WMS-PACK-001 §4.6); allocation race producing two Allocation rows; stale cache showing wrong qty
Vendor errorThe variance traces back to receiving — the WMS recorded what was claimed, but vendor under-shipped or sent wrong productReceiving variance not caught at intake; PO discrepancy found weeks later; vendor packing-slip mismatch
Human factorA person, with intent or not, removed product or falsified recordsTheft (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:

  1. Confirm with the buyer + finance that the SKU's value justifies a write-off vs. continuing investigation.
  2. Run a final cycle count of the affected location(s) per WMS-INV-002 §4.x.
  3. Approve per WMS-INV-002 §4.8 — the resulting InventoryAdjustment is the formal write-off record.
  4. Note the case file ID in the cycle-count reviewNotes so the financial write-off and the investigation are linked.
  5. Mark the case RESOLVED — write-off with 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:

  1. Stop. Do not act unilaterally. The two-person rule applies — warehouse operations manager + one other manager review the evidence together.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

TableWhat it capturesSOP reference
inventory_eventsAll inventory unit lifecycle events (received, moved, damaged, adjusted, split)This SOP §4.2
cycle_count_auditsPer-session count actions (start, count, submit, approve, reject, reopen)WMS-AUD-001 §5.1
cycle_count_sessionsThe session-level state with reviewer + timestampsWMS-INV-002
task_events (type=PICKING)Pick events: ITEM_COMPLETED, ITEM_SHORTWMS-PICK-001 §5.3
task_events (type=PACKING)Pack events: TASK_COMPLETED with weight/dimensionsWMS-PACK-001 §5.3
audit_logsReceiving session audit (RECEIVING_SESSION, RECEIVING_LINE)WMS-REC-003 §6
inventory_adjustmentsFormal adjustments — the only table that records dollar-impact varianceWMS-INV-007 §5
fulfillment_eventsOrder-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.

  • 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_adjustments from sourceType: '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

SymptomCauseResolution
Suspected variance but inventory_events shows nothing for the SKUEither 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-factorAudit trail is ambiguous — the events recorded are consistent with bothApply 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 patternPattern resolved through normal flow — receiving math caught up, cycle counts cleaned up the varianceClose 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 fileHR-related concernPer 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 investigationStock that was lost was found — system was wrong, not physical realityUpdate 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 decisionEither the data is genuinely ambiguous, or you're chasing diminishing returnsWrite up findings and escalate to senior leadership for direction. Fixed-time-box discipline matters.
The same SKU appears in three concurrent investigationsFragmented case managementConsolidate 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 thisIT 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 bugCross-check engineering tickets firstNote 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 (pgaudit extension) on inventory_events, cycle_count_audits, audit_logs, inventory_adjustments. The application layer enforces insert-only via service-layer discipline; the database doesn't. A direct DELETE from 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/shrinkage view 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

VersionDateAuthorChanges
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.