Skip to main content

SOP: Cycle-Count Approval Review

Document ID: WMS-AUD-001 Version: 1.0 Effective date: 04/30/2026 Owner: Warehouse Operations Manager Next review: [six months from effective date] Applies to: Managers and admins reviewing submitted cycle-count sessions; auditors investigating inventory variance patterns


1. Purpose

This procedure governs the manager-side review discipline for cycle-count sessions submitted by counters. The mechanical steps of opening a submitted session, viewing line items, and tapping Approve / Reject / Reopen are documented in WMS-INV-002 §4.8. This SOP is the layer above that: what the manager actually looks at, what patterns to watch, what to escalate, and how to use the audit trail to detect counter performance issues or fraud.

Cycle-count approval is the only point in the WMS where inventory variance is formally reconciled into InventoryAdjustment rows (per WMS-INV-007 §4.1 Path A). Every approved session that had variance produces a permanent inventory truth statement. Sloppy approvals propagate forward — a rubber-stamped session with bad counts doesn't just produce wrong adjustments, it gives downstream allocators wrong stock numbers to plan against.

2. Scope

In scope:

  • The review queue at /cycle-count (Pending tab) and the review UI at /cycle-count/review/:sessionId
  • The three terminal decisions: approve, reject, reopen
  • The cycle_count_audits table — what's written, when, and how to query it
  • Manager-side patterns: self-approval detection, high-variance escalation thresholds, recurring-variance patterns by SKU / location / counter
  • The history tab and weekly / monthly review queries
  • The post-approval cascade (checkBackorders, Shopify sync)

Out of scope:

  • The counter's mechanical workflow (start, count, submit) — see WMS-INV-002 §4.1–§4.7
  • The mechanics of approve / reject / reopen (what fields update, what events fire) — see WMS-INV-002 §4.8
  • Floor count — see WMS-INV-003 (similar audit shape, different SOP)
  • Receiving variance approval — see WMS-AUD-003 (when written) — different table, different review mechanics
  • Investigation of recurring patterns beyond the queries here — see WMS-AUD-002 (when written) for shrinkage investigation

3. Roles & permissions

API enforcement: the cycle-count approve / reject / reopen endpoints (POST /cycle-count/sessions/:id/approve, /reject, /reopen) are auth-only. There is no inline role gate, and no self-approval check. Any authenticated user can approve any SUBMITTED session, including their own. This is enforced operationally, not technically.

RoleView pending sessionsOpen review UIApproveRejectReopenApprove own count
READONLY✓ (read-only)
STAFF✓ technically; — operationally✓ technically; — operationally✓ technically; — operationally✓ technically; NO operationally
MANAGER✓ technically; NO operationally
ADMIN✓ technically; — operationally with documented justification
SUPER_ADMIN✓ technically; — operationally with documented justification

Operational expectations (the review discipline this SOP enforces by procedure, not code):

  • Approval is a manager / admin decision. STAFF should not approve cycle counts in routine ops, even though the API allows it. If STAFF approves, the audit trail flags it (per §6) and the manager spot-checks weekly.
  • Self-approval is forbidden except in narrow documented cases. A counter who submitted the session should not approve it themselves. If countedById === reviewedById, that's a flag the audit query catches (per §5.3). The exception: small variance (<3%) for a routine recurring location, with the manager's standing approval. Document the exception in reviewNotes.
  • High-variance sessions require senior review. A session with variance > 10% of totalExpected should not be approved by a routine reviewer — escalate to the warehouse operations manager. The system doesn't enforce this; the discipline does.

4. Procedures

4.1 The review queue

Use when: Daily review pass — first thing in the morning, end of shift, or whenever the SSE notification fires that a new session was submitted.

Steps:

  1. Open /cycle-count. Tap the Pending tab.
  2. The page calls GET /cycle-count/sessions/pending — returns sessions with status: SUBMITTED.
  3. Each row shows: location, counter name, total expected, total counted, variance count, time since submitted.
  4. Tap a session to open the review UI at /cycle-count/review/:sessionId.

What the queue order tells you:

  • Sessions are returned by submission time (oldest first by default in listSessions). A session sitting >24 hours unreviewed is a discipline gap — counters expect feedback within a shift.
  • High-variance sessions don't bubble up by default. The manager sorts mentally by varianceCount and triages — large variances first.

⚠ The queue does not flag high-variance or self-submitted sessions. Both need to be detected by visual review. A future UI improvement (per §8) would add a HIGH VARIANCE badge for sessions where abs(totalCounted - totalExpected) > totalExpected * 0.10, and a SELF-COUNT badge if countedById === reviewedById (which can't apply pre-review, but the queue could highlight sessions submitted by a user with admin rights so the reviewer is alert).

4.2 The per-session review

Use when: A submitted session is open in /cycle-count/review/:sessionId and you're deciding approve / reject / reopen.

The review UI shows three groups of data:

Header — context:

  • Location name, task number (if part of a campaign), counter's name, started/submitted timestamps
  • blindCount: true|false — whether the counter saw expected quantities. Blind counts are higher-confidence — the counter can't pencil-whip.
  • Status badge (SUBMITTED)

Summary — what the counter found:

  • totalItems, totalExpected, totalCounted, varianceCount
  • Net variance: totalCounted - totalExpected. Negative = shrinkage (less found than expected); positive = unexpected stock (more found than expected).

Line items — the variance detail:

  • Per CycleCountLine: SKU, product name, system qty, counted qty, variance (countedQty - systemQty), isUnexpected flag (true if not in the expected list)
  • Variant image (helps visual-confirm the SKU)

The reviewer's three questions (in order):

  1. Are the line items consistent with what would physically be at this location? Quick gut check — does the SKU mix match what the location should hold? If a STORAGE-zone bin shows packing supplies, something's wrong with the count or the location data.
  2. Are the variances explainable? Variance of 1-2 units on a high-velocity SKU is normal drift. Variance of 50 units on a slow-mover is suspicious — short pick that wasn't reconciled, theft, or count error.
  3. Is anything isUnexpected: true? A SKU that wasn't supposed to be at this location showed up. Possible causes: putaway was wrong, allocation released stock to wrong location, vendor shipped wrong SKU. Investigate before approving.

Decision branches:

DecisionWhen
ApproveCounts look correct, variances are explainable or within normal drift, no surprises that need investigation.
RejectCounts are visibly wrong (impossible variance, SKUs that don't make sense, mismatched units). The counter needs to re-count. The session moves to REJECTED. Counter can reopen and recount.
Reopen (after reject)Counter starts again on the same session. Useful when the rejection was over a single line and the rest was fine.

⚠ Approve is a one-way door for the inventory. Once approved, InventoryAdjustment rows are created (per WMS-INV-007 §4.1 Path A) and InventoryUnit.quantity updates. Reversing requires a counter-adjustment per WMS-INV-007 §8 — fix-via-new-adjustment, not undo. Reject anything you'd want to take back.

4.3 Approve — what the cascade does

Per cycle-count.service.ts:838-940 and the route at cycle-count.routes.ts:357-440:

In transaction:

  • For each CycleCountLine with variance != 0 && variance != null:
    • One InventoryAdjustment row with reason: 'CYCLE_COUNT', sourceType: 'CYCLE_COUNT', sourceId: sessionId, full previousQty / adjustedQty / changeQty, status: 'APPROVED', createdById = approvedById = userId
    • The InventoryUnit.quantity is updated to countedQty (or a new unit is created for isUnexpected: true && countedQty > 0)
  • The CycleCountSession itself: status: APPROVED, reviewedById: userId, reviewedAt: now, reviewNotes: <notes>
  • A cycle_count_audits row of type APPROVED with the user and any session-level notes
  • If the session belonged to a CycleCountTask and all sessions are now complete, the task is also marked COMPLETED

After transaction (best-effort, per route handler 372-440):

  • For every variant where variance > 0 (more found than expected), enqueueCheckBackorders is called per variant with triggerSource: cycle-count:{sessionId}. This kicks off the auto-retry pipeline per WMS-INV-006 §4.2 — orders waiting on stock that just appeared in the count get retried.
  • A Shopify inventory sync is enqueued for each affected variant — keeps Shopify's available stock in sync with the WMS truth.

This means a cycle-count approval can directly cause:

  • Backordered orders to fulfill within minutes (positive variance only)
  • Shopify storefront available stock to update within minutes
  • Allocator decisions to immediately reflect the new truth

If your approval was wrong, all of these cascades fire on the wrong data. The discipline gate matters.

4.4 Reject — when and how

Use when: The submitted counts are visibly wrong, can't be explained, or need re-counting before any inventory changes are made.

Steps:

  1. Tap Reject. The page prompts for a reason — required.
  2. The page calls POST /cycle-count/sessions/:id/reject with { reason }.
  3. Service updates session: status: REJECTED, reviewedById, reviewedAt, reviewNotes: <reason>.
  4. A cycle_count_audits row of type REJECTED is written with the user and reason.
  5. No InventoryAdjustment rows created. No inventory updates. No backorder retry. No Shopify sync. The counter needs to redo the count.

Communicate the rejection:

  • The counter's UI doesn't auto-notify (no SSE event for rejection per the listed event types). Tell the counter directly via Slack / radio so they don't lose time.
  • The counter can reopen the session (per §4.5) to recount on the same session ID.

Common rejection reasons (templated):

  • Variance > 30% with no explanation — recount required
  • Unexpected SKU {sku} at this location — investigate before counting
  • System qty appears stale — refresh and recount
  • Count submitted in <5 minutes for {n} items — please recount with care

4.5 Reopen — recovery from rejection

Use when: A session was rejected and the counter needs to recount.

Steps:

  1. From the rejected session (visible in the History tab — per §4.6), the counter (or a manager on their behalf) calls POST /cycle-count/sessions/:id/reopen.
  2. Service flips status: REJECTED → IN_PROGRESS. The counts are preserved on the lines but the session is editable again.
  3. A cycle_count_audits row of type REOPENED is written.
  4. Counter recounts (modifying line items as needed) and re-submits per WMS-INV-002 §4.7.
  5. The session goes back to SUBMITTED and re-enters the approval queue.

A session can be rejected → reopened → resubmitted multiple times. Each cycle is captured in cycle_count_audits. If a session is rejected 3+ times, that's a flag — either the counter doesn't know the procedure, the location data is fundamentally wrong, or there's something else going on. Investigate.

4.6 The history tab

Use when: Looking back at last week's approvals, finding a specific past session, or exporting for a quarterly audit.

Steps:

  1. From /cycle-count, tap History tab.
  2. The page calls GET /cycle-count/sessions?status=APPROVED,REJECTED&limit=20&offset=N.
  3. Sessions are returned newest-first with full per-session data: location, counter, totals, variance count, status.

Pagination: the history tab paginates 20 at a time. For larger pulls (monthly review, quarterly audit), use the SQL queries in §5 directly.

5. Reference

5.1 The cycle_count_audits table

Per cycle-count.prisma:101-116:

FieldPurpose
idPrimary key
sessionIdThe session this row belongs to
actionOne of: SESSION_STARTED, ITEM_COUNTED, UNEXPECTED_ITEM_ADDED, SUBMITTED, APPROVED, REJECTED, REOPENED
userIdWho performed the action
lineIdOptional — for line-level actions like ITEM_COUNTED
dataOptional JSON payload — varies by action (notes, item details)
createdAtTimestamp

This is the authoritative audit log for every cycle-count action. Cascade-deletes with the session (which themselves don't auto-delete — sessions are insert-only after approval).

5.2 Detecting self-approval

Self-approval = CycleCountSession.countedById === CycleCountSession.reviewedById. Per §3 operational expectations, this should rarely happen.

SELECT
s.id,
s.status,
s.totalExpected,
s.totalCounted,
s.varianceCount,
s.startedAt,
s.submittedAt,
s.reviewedAt,
u.name AS counter_and_reviewer,
l.name AS location,
s.reviewNotes
FROM cycle_count_sessions s
JOIN users u ON s.countedById = u.id
JOIN locations l ON s.locationId = l.id
WHERE s.countedById = s.reviewedById
AND s.status = 'APPROVED'
AND s.reviewedAt > NOW() - INTERVAL '30 days'
ORDER BY s.reviewedAt DESC;

A non-zero result here is the manager's investigation queue. Each row is either a documented exception (note in reviewNotes) or a discipline gap.

5.3 High-variance approvals

SELECT
s.id,
s.varianceCount,
s.totalExpected,
s.totalCounted,
s.totalCounted - s.totalExpected AS net_variance,
ROUND(
100.0 * ABS(s.totalCounted - s.totalExpected) / NULLIF(s.totalExpected, 0),
1
) AS variance_pct,
l.name AS location,
uc.name AS counter,
ur.name AS reviewer,
s.reviewedAt
FROM cycle_count_sessions s
JOIN locations l ON s.locationId = l.id
JOIN users uc ON s.countedById = uc.id
LEFT JOIN users ur ON s.reviewedById = ur.id
WHERE s.status = 'APPROVED'
AND s.reviewedAt > NOW() - INTERVAL '30 days'
AND s.totalExpected > 0
AND ABS(s.totalCounted - s.totalExpected) > s.totalExpected * 0.10
ORDER BY variance_pct DESC;

Anything in this result with variance_pct > 25 should have been escalated for senior review. If those rows show a routine reviewer (not the warehouse operations manager) approving, that's a discipline gap.

5.4 Recurring-variance patterns

By SKU — find SKUs that show variance across many sessions:

SELECT
l.sku,
COUNT(DISTINCT l.sessionId) AS sessions_with_variance,
SUM(l.variance) AS net_variance_units,
COUNT(*) FILTER (WHERE l.variance < 0) AS shrinkage_count,
COUNT(*) FILTER (WHERE l.variance > 0) AS unexpected_count
FROM cycle_count_lines l
JOIN cycle_count_sessions s ON l.sessionId = s.id
WHERE s.status = 'APPROVED'
AND s.reviewedAt > NOW() - INTERVAL '90 days'
AND l.variance IS NOT NULL
AND l.variance != 0
GROUP BY l.sku
HAVING COUNT(DISTINCT l.sessionId) >= 3
ORDER BY ABS(SUM(l.variance)) DESC;

A SKU with 3+ variance sessions in 90 days is the buyer's reorder priority list — and possibly WMS-AUD-002's investigation queue. Recurring shrinkage is theft, picking error, or persistent miscount; recurring positive is bad receiving math.

By location — find locations with chronic variance:

SELECT
loc.name AS location,
COUNT(DISTINCT s.id) AS approved_sessions,
AVG(s.varianceCount) AS avg_variance_per_session,
SUM(ABS(s.totalCounted - s.totalExpected)) AS total_units_off
FROM cycle_count_sessions s
JOIN locations loc ON s.locationId = loc.id
WHERE s.status = 'APPROVED'
AND s.reviewedAt > NOW() - INTERVAL '90 days'
GROUP BY loc.name
HAVING COUNT(DISTINCT s.id) >= 3
ORDER BY total_units_off DESC;

Locations with chronic variance suggest physical issues — bad pick face, unclear labeling, mixed SKUs, or theft hotspot.

By counter — find counters with high variance rate:

SELECT
u.name AS counter,
COUNT(*) AS sessions_counted,
AVG(s.varianceCount) AS avg_variance_per_session,
SUM(s.varianceCount) AS total_variance_count,
AVG(EXTRACT(EPOCH FROM (s.submittedAt - s.startedAt)) / 60) AS avg_minutes_per_session
FROM cycle_count_sessions s
JOIN users u ON s.countedById = u.id
WHERE s.status = 'APPROVED'
AND s.reviewedAt > NOW() - INTERVAL '90 days'
GROUP BY u.name
HAVING COUNT(*) >= 5
ORDER BY avg_variance_per_session DESC;

A counter with consistently high variance compared to peers is a coaching opportunity — either training gap, scanner issue, or care issue. Speed correlation matters too: very-fast sessions with high variance suggest pencil-whipping.

5.5 Cascade outcomes from approvals

For accountability tracing — "approval X led to backorder retries Y and Shopify sync Z":

SELECT
s.id AS session_id,
s.reviewedAt,
ur.name AS approver,
COUNT(DISTINCT a.id) AS adjustments_created,
SUM(CASE WHEN l.variance > 0 THEN 1 ELSE 0 END) AS positive_variance_lines,
SUM(CASE WHEN l.variance < 0 THEN 1 ELSE 0 END) AS negative_variance_lines
FROM cycle_count_sessions s
LEFT JOIN cycle_count_lines l ON l.sessionId = s.id
LEFT JOIN inventory_adjustments a ON a.sourceId = s.id AND a.sourceType = 'CYCLE_COUNT'
LEFT JOIN users ur ON s.reviewedById = ur.id
WHERE s.status = 'APPROVED'
AND s.reviewedAt > NOW() - INTERVAL '7 days'
GROUP BY s.id, s.reviewedAt, ur.name
ORDER BY s.reviewedAt DESC;

Each row's positive_variance_lines count corresponds 1:1 to enqueueCheckBackorders calls per §4.3 (each variant only enqueues once per approval). High positive-variance counts on a single session = high downstream activity that hour.

  • WMS-INV-002 §4.8 — The mechanics of approve / reject (what the page does, what fields update)
  • WMS-INV-003 §4.4 — Floor count (similar audit shape, different table)
  • WMS-INV-006 §4.2 — How the post-approval enqueueCheckBackorders fits the auto-retry pipeline
  • WMS-INV-007 §4.1 Path A — The InventoryAdjustment rows that approval creates
  • WMS-PICK-003 §4.5 — Cycle count after short pick (the reciprocal direction — picking creates work for this SOP)
  • WMS-AUD-002 — Shrinkage investigation (when written) — where chronic variance patterns get acted on
  • WMS-AUD-003 — Receiving variance investigation (when written) — different table, similar review shape

6. Audit & compliance

The cycle-count audit story is the strongest in the WMS — full provenance from session start through approval, captured in cycle_count_audits plus the resulting inventory_adjustments rows. This is by design: cycle counts are the formal reconciliation point, so the audit story has to support regulatory review.

Per session, the audit trail captures:

  • Who started the session (SESSION_STARTED) with timestamp
  • Each line counted (ITEM_COUNTED) with line ID and counted qty in data
  • Each unexpected item added (UNEXPECTED_ITEM_ADDED) with the new SKU
  • The submission (SUBMITTED) with the counter's notes
  • The terminal decision (APPROVED / REJECTED) with the reviewer's notes
  • Any REOPENED events between rejection and resubmission

Plus:

  • CycleCountSession itself: countedById, reviewedById, reviewedAt, reviewNotes
  • InventoryAdjustment rows from approval: createdById, approvedById (both set to the approver's userId)
  • The post-approval cascade is logged in fulfillment_events (backorder retry events, Shopify sync events)

What's missing:

  • No formal escalation log. If a high-variance session was reviewed by a senior manager rather than the routine reviewer, the audit trail just shows their userId — not the fact that this was an escalation. Adding a reviewLevel: 'ROUTINE' | 'SENIOR' | 'COMPLIANCE' field on the session would close this. See §8.
  • No threshold rules in code. The 10% / 25% high-variance thresholds in this SOP are operational discipline, not enforced by the API. A future enhancement would auto-flag sessions above thresholds and require senior approval.
  • No counter-attribution beyond the session level. If a count was performed by two people sharing a tablet, the system records one countedById. Audit reconstruction relies on operational discipline (don't share devices) rather than enforcement.

Manager weekly review checklist

Run these queries every Monday morning. They take about 15 minutes.

QueryWhat you're looking forAction
§5.2 — self-approvals (last 7 days)Any rowsEach row is either a documented exception or a discipline gap. Flag and discuss.
§5.3 — high-variance approvals (last 7 days)variance_pct > 25 rows reviewed by anyone other than warehouse ops managerEscalate for senior review.
§5.4 by SKU (last 7 days, filtered to recent)SKUs with 2+ variance sessions in 1 weekInvestigate per WMS-AUD-002 (when written).
§5.4 by counter (last 7 days)A counter with avg_variance_per_session 2× their peersCoaching conversation.
§5.4 by location (last 7 days)Locations with chronic variancePhysical investigation — sign, lighting, label condition, mixed SKUs.
§5.5 — cascade outcomesSessions with positive_variance_lines > 5High-impact approvals — verify they were correct.

Quarterly governance (Warehouse Operations Manager)

  • Total inventory_adjustments from sourceType: 'CYCLE_COUNT' per quarter — net write-off / write-on dollar value.
  • % of approved sessions that were ever rejected (rework rate). Trending up = training or process issue.
  • Median time submittedAt → reviewedAt. Should be <24h. Trending up = approval bottleneck, possibly add a second approver.
  • Audit-trail completeness check — every APPROVED session should have at minimum one SESSION_STARTED, one SUBMITTED, and one APPROVED audit row. Gaps indicate either a service-level bug or DB tampering.

7. Troubleshooting

SymptomCauseResolution
Session is not submitted (HTTP 400) on approveSession is in another status (IN_PROGRESS, APPROVED, REJECTED)Check status. Only SUBMITTED is approvable. If IN_PROGRESS, the counter hasn't submitted yet. If APPROVED or REJECTED, action already complete.
Approve returned 200 but variance didn't materialize as adjustmentsService-level error in the per-line transaction loopCheck inventory_adjustments WHERE sourceId = '{sessionId}'. If empty, check API logs around the approval timestamp. Service may have committed the session update without the per-line writes (transaction boundary issue). Escalate to IT.
Self-approved a session by accidentReviewer was the same user as counterDocument in reviewNotes after the fact: Self-approval — {reason}, see ticket #{n}. Manager weekly review will pick it up; explain when it does. Don't try to undo (one-way door).
Approved a session that should have been rejectedVariance was wrong, counts didn't match physicalCannot un-approve. Recovery: open a new cycle count for the same location, count correctly, approve. The new adjustment will offset the wrong one. Note: the inventory_events audit will show two adjustments — that's the correct paper trail.
Counter rejected the rejection (insists their count was right)Disagreement on the truthRe-count together, with both people present, with photos / video if needed. Whichever count both agree to becomes the authoritative submission. If still disagree, escalate to warehouse operations manager.
cycle_count_audits is missing rows for a session that was clearly worked onService-level audit-write failure (rare; the audits are inside the same transaction as the action)Escalate to IT. The session itself may also be inconsistent.
A SKU that was just adjusted upward via approval still shows zero stock to the allocatorThe Shopify sync or the post-approval cascade may not have completedCheck #wms-support queue health. The enqueueCheckBackorders job may be backed up. Run a manual fulfill from the backorders dashboard per WMS-INV-006 §4.4.
Session shows status: APPROVED but no reviewedByIdDatabase row was modified outside the applicationInvestigate. The service always sets reviewedById in the same transaction. Manual SQL update is the only way this state could exist. Escalate.
Two simultaneous reviewers tried to approve the same sessionRace — service doesn't lock for reviewWhichever request committed second will fail because the session's status will already be APPROVED. The first reviewer's userId wins. The second reviewer should refresh the queue and move on.

8. Escalation

  • Add a role gate to the approve endpoint. Today auth-only. A real MANAGER+ check at the route level would prevent accidental STAFF approvals. Engineering ticket — five-line addition matching the receiving approval pattern (receiving.routes.ts already has Only Admin or Manager can approve|reject per WMS-REC-003 §3).
  • Add a self-approval block. The service already knows countedById and the approving userId. A if (session.countedById === userId) throw new Error('Cannot approve a session you submitted') would close the gap. Allow override via a forceSelfApprove: true body field that requires MANAGER+ and records the override in reviewNotes automatically.
  • Add a high-variance flag to the queue UI. The Pending tab should sort or filter by variance percentage so reviewers see big variances first. Cross-reference §4.1 callout.
  • Add reviewLevel field to the session. Routine vs. senior vs. compliance review — captured at approval time, queryable for audit.
  • Auto-flag sessions where variance > 25% for senior approval. Configurable threshold per warehouse. The session would go to SUBMITTED but be visually flagged in the queue and require senior reviewer to approve.
  • Suspected fraud or repeated discipline gaps. Cross-functional escalation — warehouse ops manager + HR. The audit trail in cycle_count_audits is sufficient evidence for performance reviews. Pull the §5.2 and §5.4-by-counter results for the affected user over the last 60 days.
  • Audit trail tampering suspected. IT / DBA — cycle_count_audits rows should never be deleted or modified post-write (the schema doesn't enforce this; PostgreSQL doesn't natively, either). Pull pg_audit logs (if enabled) for any direct DB writes to the table.

9. Revision history

VersionDateAuthorChanges
1.0[DATE][NAME]Initial release. Documents the manager-side review discipline for cycle-count approval — companion to WMS-INV-002 §4.8 which covers the mechanics. Documents the cycle_count_audits table as the authoritative audit log per cycle-count.prisma:101-116 with seven action types (SESSION_STARTED, ITEM_COUNTED, UNEXPECTED_ITEM_ADDED, SUBMITTED, APPROVED, REJECTED, REOPENED). Confirms the API role-gate and self-approval gaps per cycle-count.routes.ts:357-440 and cycle-count.service.ts:838-940 — both auth-only, no role check, no self-approval check. Documents the post-approval cascade (enqueueCheckBackorders per variant with positive variance, Shopify inventory sync) per route handler 372-440. Provides production-ready SQL queries for self-approval detection (§5.2), high-variance approvals (§5.3), recurring-variance patterns by SKU / location / counter (§5.4), and cascade outcomes (§5.5) for weekly and quarterly review. Cross-references WMS-INV-002 (mechanics), WMS-INV-006 (backorder retry pipeline), WMS-INV-007 (resulting adjustments), WMS-PICK-003 (the reciprocal direction — picking creates work), WMS-AUD-002 / WMS-AUD-003 (downstream investigation SOPs when written). Code references: packages/db/prisma/schema/cycle-count.prisma:36-116, packages/domain/src/services/cycle-count.service.ts:838-940, apps/api/src/routes/cycle-count.routes.ts:208-498, apps/web/src/pages/cycle-count/{index,review,session,start}.tsx.