SOP: Approving, Rejecting, & Reopening a Receiving Session
Document ID: WMS-REC-003
Version: 1.0
Effective date: 04/30/2026
Owner: Warehouse Operations Manager
Next review: [six months from effective date]
Applies to: Users with role MANAGER, ADMIN, or SUPER_ADMIN reviewing submitted receiving sessions; counters reopening rejected sessions
1. Purpose
This procedure governs the manager-side review of receiving sessions that counters have submitted under WMS-REC-001, and the counter-side reopen flow for sessions that get rejected. Approval is the action that converts a counted session into real inventory — it creates InventoryUnit rows, generates a putaway WorkTask, triggers backorder checks for the received SKUs, and writes the immutable SESSION_APPROVED audit row. Rejection sends the session back to the counter for re-counting and notifies them via bell + the worker queue.
If the manager doesn't approve, no inventory is created. If the manager approves wrong counts, the variance lives forever in the system. Both branches are high-stakes and irreversible without further work.
2. Scope
In scope:
- Reviewing a session in status
SUBMITTEDfrom/receiving/approve/:sessionId - The Approve action (creates inventory + putaway task)
- The Reject action (returns session to counter with a reason)
- The counter-side Reopen for Re-counting action on rejected sessions
- The notification cascade (bell notification + queued worker job) that follows rejection
Out of scope:
- Starting and counting a session — see WMS-REC-001
- Filing exceptions during count — see WMS-REC-002
- Putaway from an approved session (the work task that approval creates) — see WMS-REC-004
- Investigating receiving variance trends across many sessions — see WMS-AUD-003
- Cancelling a session outright (no UI path; database-only operation) — escalate per §8
3. Roles & permissions
| Role | View submitted session | Approve | Reject | Reopen rejected |
|---|---|---|---|---|
| READONLY | ✓ | — | — | — |
| STAFF | ✓ | — | — | ✓ (counter only — the user who submitted) |
| MANAGER | ✓ | ✓ | ✓ | ✓ |
| ADMIN | ✓ | ✓ | ✓ | ✓ |
| SUPER_ADMIN | ✓ | ✓ | ✓ | ✓ |
API enforcement (verified against the codebase):
POST /receiving/:sessionId/approveandPOST /receiving/:sessionId/rejectboth checkuser.roleinline and return HTTP 403 with"Only Admin or Manager can approve"/"Only Admin or Manager can reject"if the caller is notADMIN,MANAGER, orSUPER_ADMIN. This is one of the few places in the WMS API where role gating is technically enforced — see WMS-INV-001 §3 for context on why most other operations are not.POST /receiving/:sessionId/reopenchecks authentication only. Any authenticated user can reopen a rejected session. Operationally, only the original counter should reopen — the counter receives the bell notification and is the user who must re-count. Discipline is enforced through the audit trail (SESSION_REOPENEDaction withuserId), not blocked at the API.
Operational expectation: do not approve your own count. The counter at submit time becomes
submittedBy. The approver becomesapprovedBy. These should be different users for any session that resulted in inventory adjustments to a customer-affecting SKU. The system does not enforce this — manager discipline is the control.
4. Procedures
4.1 Reviewing a submitted session
Use when: A counter has submitted a session and you've been notified (bell notification, or you opened the Pending Approval queue).
Prerequisites:
- Your role is
MANAGER,ADMIN, orSUPER_ADMIN. - The session status is
SUBMITTED. (IfIN_PROGRESS, the counter hasn't submitted yet. IfAPPROVEDorREJECTED, the decision has already been made.)
Steps:
- Open the session via the bell-notification link, the Pending Approval queue (
GET /receiving/pending), or directly at/receiving/approve/:sessionId. - The page header shows: PO reference, vendor, the counter's name, when it was submitted, the receiving location, and overall progress as a percentage.
- Read the three top-of-page summary cards:
- Counted — total units physically counted
- Expected — total units the PO expected
- Variance —
Counted − Expected. Color-coded: green if exactly equal, yellow if positive (overage), red if negative (shortage).
- Look for the two alert banners:
- Variances Detected (yellow): lists
{n} items shortand/or{n} items over. Read both. - Damaged Items Reported (red): shows
{n} items with damage ({totalDamaged} units). These are units the counter marked damaged (quantityDamaged) — they will not be added to inventory at approval time, only thegoodQuantity = quantityCounted − quantityDamagedportion is.
- Variances Detected (yellow): lists
- Expand the Items with Variance section if it appears. Each row shows expected vs. counted vs. damaged. Decide for each whether the variance is:
- Acceptable — vendor occasionally over-ships by 1–2; an exception was filed (see WMS-REC-002) explaining the shortage; the variance is small and within tolerance for this vendor.
- Not acceptable — large unexplained shortage, no exception filed, item with damage but no exception attached. Reject (§4.3).
- If you have any doubt, walk to the receiving location and physically verify before approving. The receiving location is shown in the session header.
⚠ Damage reduces what gets stocked. A line counted as 100 with 5 marked damaged creates an
InventoryUnitwithquantity: 95, not 100. The 5 damaged units do not get aDAMAGED-status inventory row at approval time — the receiving session simply records that they arrived damaged viaquantityDamagedand the linkedReceivingException(per WMS-REC-002). Damaged-on-arrival product does not enter inventory under this flow. If you need the damaged units in inventory for insurance / vendor-credit accounting, mark them via WMS-INV-001 §4.3 after approval — not via this SOP.
4.2 Approving the session
Use when: You've reviewed §4.1 and the count is acceptable.
Steps:
- From the approval page, tap the green Approve button.
- Wait for the spinner. The button shows Approving… while the transaction runs. Do not refresh or navigate away during this — the approve transaction creates inventory units, the putaway task, and the audit row atomically.
- On success, the page refreshes and the session shows status
APPROVEDwith the timestamp and your user asapprovedBy. A toast confirms the put-away task number (PUTAWAY-{poRef}-{shortId}).
What approval actually does (one transaction):
- For each receiving line where
quantityCounted > 0andproductVariantIdis matched:- Calculates
goodQuantity = quantityCounted − quantityDamaged. - Skips the line if
goodQuantity <= 0(everything was damaged). - Looks for an existing
InventoryUnitat the receiving location with the sameproductVariantIdandlotNumber:- Match found → increments that unit's
quantitybygoodQuantity, sets status toAVAILABLE. - No match → creates a new
InventoryUnitwithquantity: goodQuantity,status: AVAILABLE,lotNumber,expiryDate, andreceivedFrom: "PO:{poReference}".
- Match found → increments that unit's
- Writes a
fulfillment_eventsrow of typeinventory:receivedwith the variant, location, quantity, PO reference, and your user ID.
- Calculates
- Creates a
WorkTaskof typePUTAWAY, statusPENDING, priority50, with oneTaskItemper inventory line (sequence 1, 2, 3…). Task number format:PUTAWAY-{poReference}-{base36-timestamp}. - Updates the session:
status: APPROVED,approvedBy: <you>,approvedAt: <now>,putawayTaskId: <new task ID>. - Writes an
audit_logsrow withaction: SESSION_APPROVED, including counts of items received, units received, and the putaway task number. - After the transaction commits, the route handler enqueues one
enqueueCheckBackordersjob per unique received variant (triggerSource: "receiving:{sessionId}"). This is best-effort — the approval succeeds even if the queue enqueue fails (errors are logged, not raised).
Result:
- Inventory is now available at the receiving location (a location of type
RECEIVING, orSTORAGEas fallback). It is not at its final pickable bin yet — that's WMS-REC-004 (putaway). - Backorders for any received SKU are checked asynchronously; staff should watch
#warehouse-opsfor backorder allocation alerts in the next minute or two. See WMS-INV-006. - The putaway task appears in the work task queue for whoever is on putaway duty.
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 403 | "Only Admin or Manager can approve" | Your role is STAFF, READONLY, or SALES_REP. Contact a manager. |
| 400 | "Cannot approve: session is {status}" | Session is IN_PROGRESS (counter hasn't submitted), APPROVED (already done — refresh), REJECTED (counter must reopen first), or CANCELLED. |
| 400 | "No receiving location configured" | Session was created against a location that no longer exists or was deactivated. Escalate per §8 — the session needs a database fix before approval can proceed. |
| 404 | "Session not found" | Session ID is wrong or session was deleted. Escalate. |
⚠ Approval is irreversible from the UI. Once you tap Approve, there is no Unapprove button. If you approve a session you shouldn't have, the only recovery paths are: (a) manual
InventoryAdjustmentrows to subtract the wrongly-added quantities (WMS-INV-007), or (b) database intervention by an admin. Treat Approve like a signature.
4.3 Rejecting the session
Use when: You've reviewed §4.1 and the count is not acceptable. Common reasons: large unexplained variance, missing exceptions for damage you can see, wrong receiving location, the counter mis-counted on a high-value SKU.
Steps:
- From the approval page, tap the red Reject button. The Reject Session modal opens.
- Provide a reason. The modal text reads: "Please provide a reason for rejection. The counter will be notified and can reopen the session."
- The modal offers five quick-pick reasons; tap one to populate the textarea:
- Recount needed
- Wrong quantities
- Items not received
- Data entry error
- Damaged not reported
- Edit or extend the reason as needed. The textarea is required — the API rejects with HTTP 400
"Rejection reason is required"if blank. - Tap Reject. The button shows Rejecting… while the request runs.
What rejection actually does:
- Updates the session:
status: REJECTED,rejectedBy: <you>,rejectedAt: <now>,rejectionReason: <the reason text>. - Creates a bell
Notificationfor the counter (session.submittedByif set, otherwisesession.countedBy) with titleRecount Required: {poReference}, message set to the reason, and link to the session. - Writes an
audit_logsrow withaction: SESSION_REJECTEDand the reason. - Emits a real-time
receiving:rejectedSSE event targeted at the counter — they get a live in-app notification within seconds. - Best-effort enqueues a
notifyCounterworker job (enqueueNotifyCounter) for additional notification work (e.g., email, Slack — depends on your worker configuration). Failures are logged but do not affect the rejection itself.
Result:
- Session is read-only with the rejection reason banner displayed.
- The counter is notified through three channels (bell, SSE, queued job).
- No inventory is created. No putaway task is created. Nothing else happens until the counter reopens (§4.4).
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 403 | "Only Admin or Manager can reject" | Same as approve — role insufficient. |
| 400 | "Rejection reason is required" | Textarea was empty or whitespace only. The route enforces this before checking role. |
| 400 | "Cannot reject: session is {status}" | Session is not in SUBMITTED. Refresh — someone may have already approved or rejected. |
⚠ The reason field is permanent and visible to the counter. Be specific and constructive: "Line for SKU LIT-PAX-3000 shows 144 counted but PO expected 120 with no overage exception filed — please recount and file an OVERAGE exception (WMS-REC-002 §4.x) if confirmed." Not: "wrong".
4.4 Reopening a rejected session (counter-side)
Use when: You're the counter who submitted a session and got a Recount Required notification with the reason explaining what to fix.
Prerequisites:
- Session status is
REJECTED. - You can read the rejection reason in the page banner. If you don't understand it, ask the manager who rejected (their name is in the audit log; their reason is in the banner).
Steps:
- Open the rejected session via the bell notification link, or directly via
/receiving/session/:sessionId. - The session page renders read-only with a red banner showing the rejection reason and a Reopen & Recount button. (On the manager-side approve page, the button label is Reopen for Re-counting — same endpoint, same effect.)
- Tap the button. The button shows Reopening… while the request runs.
- The session resets to
IN_PROGRESSwith the lock acquired by you. You're now back in the counting flow from WMS-REC-001 §4.2. - Address the rejection reason. Recount what was flagged. File any missing exceptions per WMS-REC-002. Submit again.
What reopen actually does:
- Updates the session:
status: IN_PROGRESS,lockedBy: <you>,lockedAt: <now>, and clears:submittedAt,submittedBy,approvedBy,approvedAt,rejectedBy,rejectedAt,rejectionReason. - Writes an
audit_logsrow withaction: SESSION_REOPENEDand your user ID. - Counts and exception records are preserved. Reopening doesn't reset counts to zero — you adjust from where you left off. The
quantityCountedon each line stays as it was at submit time. Use +/− or Set Exact per WMS-REC-001 §4.3 to fix specific lines.
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 400 | "Cannot reopen: session is {status}" | Session is not REJECTED. If APPROVED, the inventory is already created — you need a formal adjustment (WMS-INV-007), not a reopen. |
| 404 | "Session not found" | Wrong ID or deleted. Escalate. |
⚠ Reopening clears the rejection trail from the session row but not from the audit log. The
audit_logsrows forSESSION_SUBMITTED,SESSION_REJECTED, andSESSION_REOPENEDare all permanent. Don't rely on the clearedrejectionReasonfield as a "fresh start" — the trail of every submit/reject cycle for this session is reconstructable from the audit table forever.
5. Reference
5.1 Session lifecycle (this SOP's slice)
IN_PROGRESS → SUBMITTED → APPROVED (terminal — inventory created)
│
└─→ REJECTED → IN_PROGRESS (cycle repeats)
IN_PROGRESS → SUBMITTED: counter action, see WMS-REC-001 §4.5SUBMITTED → APPROVED: §4.2SUBMITTED → REJECTED: §4.3REJECTED → IN_PROGRESS: §4.4APPROVEDis terminal — there is no transition out of it via this SOP. Corrections to approved sessions go through WMS-INV-007.
5.2 What approval creates (cheat sheet)
| Object | Where | When | Notes |
|---|---|---|---|
InventoryUnit row(s) | inventory_units | One per (variant + lot) per receiving line, or increment of existing match | Status AVAILABLE, receivedFrom: "PO:{poReference}" |
fulfillment_events row | fulfillment_events | Per inventory line | type inventory:received |
WorkTask (putaway) | work_tasks | One per session | type: PUTAWAY, status: PENDING, priority: 50, taskNumber: PUTAWAY-{poRef}-{base36} |
TaskItem rows | task_items | One per inventory line | Sequenced 1..n in order processed |
| Session update | receiving_sessions | One row | status: APPROVED, approvedBy, approvedAt, putawayTaskId |
audit_logs row | audit_logs | One row | action: SESSION_APPROVED with item/unit counts and putaway task |
| Backorder check jobs | BullMQ (queue) | One per unique received variant | Best-effort, post-transaction; failures logged not raised |
5.3 What rejection creates
| Object | Where | When | Notes |
|---|---|---|---|
| Session update | receiving_sessions | One row | status: REJECTED, rejectedBy, rejectedAt, rejectionReason |
Notification row | notifications | One row | Targets submittedBy ?? countedBy, type receiving:recount_required |
audit_logs row | audit_logs | One row | action: SESSION_REJECTED with reason |
| SSE event | (real-time) | One event | receiving:rejected, targeted at the counter |
| Worker job | BullMQ | One job | enqueueNotifyCounter; best-effort, failures logged not raised |
5.4 Quick-reject reasons (modal presets)
The Reject modal offers these five quick-pick reasons. Use them when they fit; they appear verbatim in the audit log and on the counter's notification:
Recount neededWrong quantitiesItems not receivedData entry errorDamaged not reported
For anything that doesn't fit one of these, write a custom reason. Be specific (see §4.3 callout).
5.5 Related SOPs
- WMS-REC-001 — Starting & counting a receiving session (everything before this SOP)
- WMS-REC-002 — Receiving exceptions (the procedure that handles damaged / wrong / missing / overage / quality issues during count)
- WMS-REC-004 — Putaway from an approved session (uses the
WorkTaskthis SOP creates) - WMS-INV-006 — Backorder resolution (where backorder-check enqueues land)
- WMS-INV-007 — Formal inventory adjustments (the only way to correct an over-approved session)
- WMS-AUD-003 — Receiving variance investigation (when reject patterns reveal systemic issues)
6. Audit & compliance
Every approve, reject, and reopen writes one row to audit_logs with entityType: "ReceivingSession". Approvals additionally write fulfillment_events rows of type inventory:received — one per received variant. The combination is the system of record for "what physically arrived, when, with whose signature."
Manager weekly review (recommended):
- Pull all
SESSION_REJECTEDrows from the past 7 days. For each, confirm the reason is specific enough that a counter could act on it. Vague rejection reasons ("recount","wrong") leave the counter guessing and the audit trail thin. - Pull all sessions where
(approvedAt - submittedAt) > 4 hours. Long approval delays usually mean managers aren't seeing the bell notification. Check who's on the alert list. - Pull all sessions with
rejectedAt IS NOT NULL AND status = 'APPROVED'— sessions that were rejected at least once before being approved. These are interesting for vendor-quality and counter-training discussions, not necessarily problems.
Quarterly governance (Warehouse Operations Manager):
- Reject rate per counter (rejections / submits)
- Reject rate per vendor (rejections / total sessions for that vendor)
- Mean time from
submittedAttoapprovedAt|rejectedAt - Approvers concentration (one manager handling 90% of approvals = single point of failure)
Persistent high reject rate for a single counter → coaching, not punishment, per WMS-000 §7. Persistent high reject rate for a single vendor → buyer conversation.
7. Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
"Only Admin or Manager can approve" (HTTP 403) | Your role is below MANAGER | Hand off to a manager. Don't request a role change just to clear this — the role gate exists for a reason. |
"Cannot approve: session is APPROVED" (HTTP 400) | Already approved (page is stale) | Refresh. If the duplicate-action concerns you, check the audit log to see who actually approved it. |
"Cannot approve: session is REJECTED" (HTTP 400) | Counter rejected before reopening | Counter must reopen first (§4.4). Don't attempt to approve a rejected session — there are no counts to validate. |
"No receiving location configured" (HTTP 400) on Approve | Receiving location was deleted or deactivated since session start | Database fix required. Escalate per §8 — do not work around by changing the session's receivingLocationId directly without auditing. |
| Approve succeeded but no putaway task appears in the queue | Possible: page filter excludes this PUTAWAY task; or the task is at a priority that's sorted off-screen | Search work tasks for taskNumber LIKE 'PUTAWAY-{poRef}-%'. If absent from work_tasks, the approve transaction had a partial commit — escalate immediately. |
| Approve succeeded but inventory doesn't appear at the receiving location | Counter recorded quantityDamaged == quantityCounted for every line | Approval correctly skipped lines with goodQuantity <= 0. The damaged units are accounted for in quantityDamaged and via WMS-REC-002 exceptions, not via inventory rows. |
| Backorder check didn't run after approve | Queue was down or enqueueCheckBackorders failed (best-effort, not blocking) | Approval still succeeded — inventory is correct. Run the backorder check manually per WMS-INV-006 §4. Check #wms-support for queue health. |
| Counter says they didn't get a rejection notification | Bell notification is in notifications table; SSE was lost; worker enqueue may have failed | Confirm the row exists in notifications with type: receiving:recount_required and the correct userId. If yes, the counter's UI didn't render it — refresh their page. If no, the rejection transaction itself failed — escalate. |
Reopen failed with "Cannot reopen: session is APPROVED" | Session was already approved (not the rejected one you thought) | You're on the wrong session. Approved sessions are not reversible via reopen — corrections require WMS-INV-007. |
| Counter reopened, recounted, resubmitted, but the rejection reason still shows | The session's rejectionReason is cleared on reopen; this is a UI cache | Refresh. The cleared field is in the database — only the page is stale. |
8. Escalation
- Approval transaction partially committed (inventory created but no putaway task, or vice versa): IT on-call via
#wms-support. Include the session ID and the time. Database may need a manual rollback or a hand-stitched putaway task; do not attempt approve again. - Suspected wrongful approval (you approved counts you shouldn't have): Stop the putaway immediately if the task hasn't started. If putaway is in progress, let it complete; reverse via WMS-INV-007. Notify the warehouse manager and document.
- Receiving location was deleted/deactivated mid-flow: Warehouse manager + IT. The session row references a location ID that no longer satisfies the approve-time check.
- Counter is unavailable to reopen a rejected session and the goods are blocking the dock: Manager may reopen on the counter's behalf (the API does not block this), but document in a
#warehouse-opspost why. The audit log will show your user as the reopener — that's a reportable departure from the operational expectation. - Bell notification not delivered to counter repeatedly: IT — there's a notification table failure or the worker isn't running. Block further submits until the notification path is healthy.
9. Revision history
| Version | Date | Author | Changes |
| ------- | ------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.0 | [DATE] | [NAME] | Initial release. Covers the manager-side approve and reject flow, the counter-side reopen flow, and the irreversibility of approval. Documents that approval is the action that creates InventoryUnit rows, generates the putaway WorkTask, and triggers enqueueCheckBackorders per received variant. Documents that rejection cascades through three notification channels (bell Notification row, real-time SSE event, queued notifyCounter worker job). Documents damageQuantity-handling: damaged units do not enter inventory at approval time. Documents the role gate on approve/reject (Only Admin or Manager can approve | reject) and the absence of one on reopen. Cross-references WMS-REC-001 (everything before submit), WMS-REC-002 (exceptions), WMS-REC-004 (putaway), WMS-INV-006 (backorders), WMS-INV-007 (post-approval correction). Error strings, audit action codes, and quick-reject reason presets pulled verbatim from apps/api/src/routes/receiving.routes.ts, packages/domain/src/services/receiving.service.ts, and apps/web/src/pages/receiving/approve.tsx. |