Skip to main content

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 SUBMITTED from /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

RoleView submitted sessionApproveRejectReopen rejected
READONLY
STAFF✓ (counter only — the user who submitted)
MANAGER
ADMIN
SUPER_ADMIN

API enforcement (verified against the codebase):

  • POST /receiving/:sessionId/approve and POST /receiving/:sessionId/reject both check user.role inline and return HTTP 403 with "Only Admin or Manager can approve" / "Only Admin or Manager can reject" if the caller is not ADMIN, MANAGER, or SUPER_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/reopen checks 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_REOPENED action with userId), not blocked at the API.

Operational expectation: do not approve your own count. The counter at submit time becomes submittedBy. The approver becomes approvedBy. 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, or SUPER_ADMIN.
  • The session status is SUBMITTED. (If IN_PROGRESS, the counter hasn't submitted yet. If APPROVED or REJECTED, the decision has already been made.)

Steps:

  1. Open the session via the bell-notification link, the Pending Approval queue (GET /receiving/pending), or directly at /receiving/approve/:sessionId.
  2. The page header shows: PO reference, vendor, the counter's name, when it was submitted, the receiving location, and overall progress as a percentage.
  3. Read the three top-of-page summary cards:
    • Counted — total units physically counted
    • Expected — total units the PO expected
    • VarianceCounted − Expected. Color-coded: green if exactly equal, yellow if positive (overage), red if negative (shortage).
  4. Look for the two alert banners:
    • Variances Detected (yellow): lists {n} items short and/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 the goodQuantity = quantityCounted − quantityDamaged portion is.
  5. 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).
  6. 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 InventoryUnit with quantity: 95, not 100. The 5 damaged units do not get a DAMAGED-status inventory row at approval time — the receiving session simply records that they arrived damaged via quantityDamaged and the linked ReceivingException (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:

  1. From the approval page, tap the green Approve button.
  2. 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.
  3. On success, the page refreshes and the session shows status APPROVED with the timestamp and your user as approvedBy. A toast confirms the put-away task number (PUTAWAY-{poRef}-{shortId}).

What approval actually does (one transaction):

  • For each receiving line where quantityCounted > 0 and productVariantId is matched:
    • Calculates goodQuantity = quantityCounted − quantityDamaged.
    • Skips the line if goodQuantity <= 0 (everything was damaged).
    • Looks for an existing InventoryUnit at the receiving location with the same productVariantId and lotNumber:
      • Match found → increments that unit's quantity by goodQuantity, sets status to AVAILABLE.
      • No match → creates a new InventoryUnit with quantity: goodQuantity, status: AVAILABLE, lotNumber, expiryDate, and receivedFrom: "PO:{poReference}".
    • Writes a fulfillment_events row of type inventory:received with the variant, location, quantity, PO reference, and your user ID.
  • Creates a WorkTask of type PUTAWAY, status PENDING, priority 50, with one TaskItem per 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_logs row with action: SESSION_APPROVED, including counts of items received, units received, and the putaway task number.
  • After the transaction commits, the route handler enqueues one enqueueCheckBackorders job 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, or STORAGE as 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-ops for 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:

HTTPAPI messageWhat 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 InventoryAdjustment rows 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:

  1. From the approval page, tap the red Reject button. The Reject Session modal opens.
  2. Provide a reason. The modal text reads: "Please provide a reason for rejection. The counter will be notified and can reopen the session."
  3. 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
  4. Edit or extend the reason as needed. The textarea is required — the API rejects with HTTP 400 "Rejection reason is required" if blank.
  5. 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 Notification for the counter (session.submittedBy if set, otherwise session.countedBy) with title Recount Required: {poReference}, message set to the reason, and link to the session.
  • Writes an audit_logs row with action: SESSION_REJECTED and the reason.
  • Emits a real-time receiving:rejected SSE event targeted at the counter — they get a live in-app notification within seconds.
  • Best-effort enqueues a notifyCounter worker 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:

HTTPAPI messageWhat 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:

  1. Open the rejected session via the bell notification link, or directly via /receiving/session/:sessionId.
  2. 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.)
  3. Tap the button. The button shows Reopening… while the request runs.
  4. The session resets to IN_PROGRESS with the lock acquired by you. You're now back in the counting flow from WMS-REC-001 §4.2.
  5. 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_logs row with action: SESSION_REOPENED and your user ID.
  • Counts and exception records are preserved. Reopening doesn't reset counts to zero — you adjust from where you left off. The quantityCounted on 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:

HTTPAPI messageWhat 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_logs rows for SESSION_SUBMITTED, SESSION_REJECTED, and SESSION_REOPENED are all permanent. Don't rely on the cleared rejectionReason field 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.5
  • SUBMITTED → APPROVED: §4.2
  • SUBMITTED → REJECTED: §4.3
  • REJECTED → IN_PROGRESS: §4.4
  • APPROVED is 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)

ObjectWhereWhenNotes
InventoryUnit row(s)inventory_unitsOne per (variant + lot) per receiving line, or increment of existing matchStatus AVAILABLE, receivedFrom: "PO:{poReference}"
fulfillment_events rowfulfillment_eventsPer inventory linetype inventory:received
WorkTask (putaway)work_tasksOne per sessiontype: PUTAWAY, status: PENDING, priority: 50, taskNumber: PUTAWAY-{poRef}-{base36}
TaskItem rowstask_itemsOne per inventory lineSequenced 1..n in order processed
Session updatereceiving_sessionsOne rowstatus: APPROVED, approvedBy, approvedAt, putawayTaskId
audit_logs rowaudit_logsOne rowaction: SESSION_APPROVED with item/unit counts and putaway task
Backorder check jobsBullMQ (queue)One per unique received variantBest-effort, post-transaction; failures logged not raised

5.3 What rejection creates

ObjectWhereWhenNotes
Session updatereceiving_sessionsOne rowstatus: REJECTED, rejectedBy, rejectedAt, rejectionReason
Notification rownotificationsOne rowTargets submittedBy ?? countedBy, type receiving:recount_required
audit_logs rowaudit_logsOne rowaction: SESSION_REJECTED with reason
SSE event(real-time)One eventreceiving:rejected, targeted at the counter
Worker jobBullMQOne jobenqueueNotifyCounter; 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 needed
  • Wrong quantities
  • Items not received
  • Data entry error
  • Damaged not reported

For anything that doesn't fit one of these, write a custom reason. Be specific (see §4.3 callout).

  • 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 WorkTask this 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_REJECTED rows 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 submittedAt to approvedAt|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

SymptomCauseResolution
"Only Admin or Manager can approve" (HTTP 403)Your role is below MANAGERHand 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 reopeningCounter 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 ApproveReceiving location was deleted or deactivated since session startDatabase 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 queuePossible: page filter excludes this PUTAWAY task; or the task is at a priority that's sorted off-screenSearch 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 locationCounter recorded quantityDamaged == quantityCounted for every lineApproval 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 approveQueue 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 notificationBell notification is in notifications table; SSE was lost; worker enqueue may have failedConfirm 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 showsThe session's rejectionReason is cleared on reopen; this is a UI cacheRefresh. 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-ops post 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. |