Appearance
G3N/Inventory — Mobile App Developer Guide
This document is the single reference for building the G3N/Inventory Android mobile app. It covers authentication, session lifecycle, org/facility context selection, role-based capability gating, every backend endpoint the app calls, barcode scanning patterns, error handling, and idempotency.
App profile: Android-only (phones, tablets, Zebra TC-series), online-only (no offline/background processing), camera-based barcode scanning, first-party, user session auth. No push notifications — user refreshes manually.
Stack base URL: https://api.g3nretailstack.com
Business Requirements
Functional Requirements
FR-1: Authentication & Context
| ID | Requirement | Priority |
|---|---|---|
| FR-1.1 | User logs in with email and passcode. No SSO, no biometric — email+passcode only. | Must |
| FR-1.2 | After login, user sees a list of organizations they belong to (as owner or member). User selects one. | Must |
| FR-1.3 | After selecting an org, user sees a list of logical facilities they have access to within that org. User selects one. | Must |
| FR-1.4 | After selecting a facility, the app determines which functions the user can access based on their facility-scoped roles. Functions the user cannot access are hidden (not grayed out — hidden). | Must |
| FR-1.5 | If the user belongs to only one org, skip the org picker and auto-select. Same for single-facility. | Should |
| FR-1.6 | User can switch org or facility without logging out (return to picker). | Must |
| FR-1.7 | Session remains active for the duration of use without re-login as long as the user is active (1-hour sliding window, auto-extends). | Must |
| FR-1.8 | On session expiry or revocation, the app navigates to the login screen with a clear message. No silent failures. | Must |
FR-2: Barcode Scanning
| ID | Requirement | Priority |
|---|---|---|
| FR-2.1 | The app provides a barcode scanner (camera-based) as the primary input method for product identification. | Must |
| FR-2.2 | Supported barcode formats: EAN-13, EAN-8, UPC-A, UPC-E, Code 128, Code 39, QR Code, DataMatrix, GS1-128. | Must |
| FR-2.3 | On scan, the app resolves the barcode to a product variant within 500ms (including network round-trip). If resolution fails, allow manual entry of product code or identifier. | Must |
| FR-2.4 | After barcode resolution, the app displays: product name/description, variant attributes (color, size, etc.), current stock level at the selected facility, and a thumbnail image (if available). | Must |
| FR-2.5 | The app caches recent barcode→variant mappings locally (LRU, 500 entries, 5-minute TTL) to avoid repeated lookups for the same item during a work session. | Should |
| FR-2.6 | Continuous scan mode: user can scan items in rapid succession without dismissing the scanner between scans (e.g., during receiving or counting). | Should |
| FR-2.7 | Camera-based scanning must work in low-light warehouse conditions. Use torch/flashlight toggle in the scanner UI. | Must |
| FR-2.8 | On Zebra TC-series devices, accept scan input from the built-in imager via DataWedge intent broadcast as a secondary input path alongside camera. No other hardware scanner integrations. | Should |
FR-3: Purchase Order Receiving
| ID | Requirement | Priority |
|---|---|---|
| FR-3.1 | User can view a list of open/issued purchase orders for the selected facility, filtered by status. PO list shows human-readable vendor name (or vendor code) and issued date for quick identification. | Must |
| FR-3.2 | User can select a PO and see its line items with ordered quantities, previously received quantities, and remaining quantities. | Must |
| FR-3.3 | User can record a partial receipt against a PO — receiving some lines or partial quantities on a line. The system tracks cumulative received vs. ordered. | Must |
| FR-3.4 | For each line being received, user can specify: quantity received, quantity rejected, and rejection reason from the RECEIVING.* taxonomy (8 codes: DAMAGED, DEFECTIVE, WRONG_ITEM, SHORT_SHIPMENT, QUALITY_ISSUE, EXPIRED, PACKAGING, OTHER). PCM validates the reason code server-side against the shared allowlist. | Must |
| FR-3.5 | After recording a PCM receipt, the app also records the corresponding ICS inbound receipt (stock entry into the receiving zone/bin). These are two API calls — PCM receipt then ICS receive. | Must |
| FR-3.6 | User can mark a received shipment for QC inspection or complete QC directly (accepted/rejected/partial). | Should |
| FR-3.7 | User can initiate putaway after receiving: scan destination bin barcode, confirm variant and quantity, record the putaway movement from receiving zone to storage bin. | Must |
| FR-3.8 | Each receiving action generates an idempotency key so that network retries do not create duplicate receipts. | Must |
| FR-3.9 | The app detects over-receipt (received total exceeds ordered quantity) and displays a warning. Over-receipt is allowed (not blocked) but flagged prominently so the user can review with the vendor. | Must |
| FR-3.10 | After recording a receipt, the app shows a collapsible receipt history for the PO — each receipt entry shows timestamp, line count, total received, total rejected, and receipt ID. Expanding a receipt shows per-line detail (variant, quantity received, quantity rejected, rejection reason). | Must |
FR-4: Return to Vendor (RTV)
| ID | Requirement | Priority |
|---|---|---|
| FR-4.1 | User can create a new RTV when rejecting received goods. The RTV references the original PO receipt and rejected line items. | Must |
| FR-4.2 | User can view open/pending RTV requests and fulfill them (mark items as shipped back to vendor). | Must |
| FR-4.3 | Each RTV line specifies: variant, quantity, reason code from RECEIVING.* taxonomy (DAMAGED, DEFECTIVE, WRONG_ITEM, QUALITY_ISSUE, EXPIRED, PACKAGING, SHORT_SHIPMENT, OTHER), and free-text notes. RTV reuses the same taxonomy as receiving rejection. | Must |
| FR-4.4 | RTV creation links back to the source receipt via source_refs for full traceability. | Must |
| FR-4.5 | The app shows a confirmation preview before RTV submission: "Return {qty}× {variant_code} → {vendor_name} ({reason_label})". | Must |
| FR-4.6 | Vendor selection uses a dropdown populated from PVM /vendor/list (cached per session). Falls back to manual text input if vendor list unavailable. Shows vendor name + code in dropdown options. | Must |
FR-5: Transfer Management
| ID | Requirement | Priority |
|---|---|---|
| FR-5.1 | User can view inbound transfers (shipments destined for the current facility) with status and expected items. | Must |
| FR-5.2 | User can receive an inbound transfer: scan items, confirm quantities received, record discrepancies (short shipment, damaged items, wrong items). | Must |
| FR-5.3 | For discrepancies, user records exceptions with quantities and reasons. Exceptions generate comments for follow-up and may trigger adjustments. | Must |
| FR-5.4 | User can create outbound transfer requests (request stock to be sent from another facility, or fulfill a request to send stock to another facility). | Must |
| FR-5.5 | User can record transfer shipment: mark items as picked, packed, and shipped for an approved transfer. | Must |
| FR-5.6 | User with ics_transfer_approve role can approve pending transfer requests from within the app. | Should |
| FR-5.7 | Transfers display source and destination facility names (resolved from facility context cache, not raw GUIDs). | Must |
| FR-5.8 | Transfer list shows human-readable facility names, dates, line counts, and status badges with color coding (submitted=yellow, approved=blue, shipped=purple, received=green). | Must |
| FR-5.9 | Transfer detail view shows status timeline (requested → approved → shipped → received with timestamps), per-line breakdown, and collapsible shipment history. | Must |
| FR-5.10 | Transfer create form uses a facility dropdown (populated from context cache) instead of raw GUID input. Shows confirmation preview: "{source} → {dest} · {qty}× {variant}". | Must |
FR-6: Stock Lookup & Stock Cards
| ID | Requirement | Priority |
|---|---|---|
| FR-6.1 | User can look up current stock levels for any variant at the selected facility. Search by barcode scan, product code, or text search. | Must |
| FR-6.2 | Stock lookup displays: on-hand, available, reserved, committed, allocated, in-transit, quarantine, damaged, and consignment quantities, plus zone/bin breakdown (if available). | Must |
| FR-6.3 | User can view the stock card (ledger history) for a variant — chronological list of all movements (receives, adjustments, transfers, picks, etc.) with timestamps, actors, and quantities. | Must |
| FR-6.4 | Stock card entries are paginated. User can scroll to load more history. | Must |
| FR-6.5 | Cost data (unit cost, total value) is displayed only if the user has cost visibility roles (cost_view, ics_cost_admin, or finance_audit). Otherwise cost columns are completely hidden. | Must |
| FR-6.6 | Stock card entries show human-readable action labels (e.g., "Received", "Adjusted", "Putaway") instead of raw type codes. Reason codes display as colored taxonomy badges. | Must |
| FR-6.7 | Stock card shows a ledger summary above the table: total in, total out, net change, and entry count for the visible window. | Must |
FR-7: Quantity Adjustments
| ID | Requirement | Priority |
|---|---|---|
| FR-7.1 | User can create inventory adjustments: increase or decrease stock for a variant at the current facility. | Must |
| FR-7.2 | Each adjustment requires: variant (via barcode scan or lookup), quantity delta (positive or negative), reason code (from canonical list: INVENTORY.SHRINK, INVENTORY.DAMAGE, INVENTORY.ADJUST, INVENTORY.RECALL, INVENTORY.ADJUST_FIFO, INVENTORY.ADJUST_FEFO, INVENTORY.PUTAWAY), and free-text reason. | Must |
| FR-7.3 | User can transition stock between statuses (e.g., available → damaged, damaged → available) with quantity and reason. | Must |
| FR-7.4 | Adjustments are audited: the system records who made the adjustment, when, and why (via actor, reason, source_refs in the API). | Must |
| FR-7.5 | Activity history shows actor display names (resolved from OFM, cached per session) instead of raw GUIDs. Ledger entries display human-readable action labels, reason badges, and zone/bin paths. | Must |
| FR-7.5 | After scanning a product, the app loads and displays recent activity history (stock card entries) for the variant below the form. Entries show action type, quantity delta, reason code badge, and timestamp. | Must |
| FR-7.6 | Large-delta warning: if | delta |
| FR-7.7 | After recording an adjustment, the app refreshes both stock levels and activity history to show the updated state. Inline confirmation: "Adjusted {variant_code}: +/-{delta} units ({reason_label})". | Must |
FR-8: Flag for Review (Comments)
| ID | Requirement | Priority |
|---|---|---|
| FR-8.1 | User can flag any product's inventory for review by adding a comment. The comment is attached to the stock entity (variant at facility). | Must |
| FR-8.2 | Comments include: free-text body (up to 2,000 characters), reason tag (discrepancy, damage, shelf-life, location-error, quality, recount, other), and the variant + facility context. | Must |
| FR-8.3 | Existing comments are displayed FIRST (history-first pattern) before the compose form. Shows comment count with open/unresolved count badge. Each comment shows: body, relative timestamp, author display name (resolved from OFM, cached per session), hashtag badges, and status if not current. | Must |
| FR-8.4 | User can revise their own comments (edit body text). | Should |
| FR-8.5 | Comments are visible to all users with read access at that facility — they serve as a communication channel between shifts/teams. | Must |
FR-9: Product Information
| ID | Requirement | Priority |
|---|---|---|
| FR-9.1 | User can look up product details by scanning a barcode, searching by code, or browsing. | Must |
| FR-9.2 | Product detail screen shows: style name, variant attributes (color, size, material), variant code, all registered barcodes, identifiers (UPC, GTIN, vendor SKU), status (with color-coded badge), and product images. Primary barcode is highlighted. | Must |
| FR-9.3 | Identifier resolution supports multiple schemes: scan a vendor label, warehouse label, or standard barcode and the app resolves to the same variant. | Should |
| FR-9.4 | Product lookup provides three tabs: Details (variant info + barcodes), Stock (current stock levels at this facility via ICS), and Activity (recent stock card ledger). | Must |
Non-Functional Requirements
NFR-1: Performance
| ID | Requirement | Target |
|---|---|---|
| NFR-1.1 | Barcode scan → product display (including stock level) | < 500ms end-to-end (scan to pixels on screen) |
| NFR-1.2 | Stock lookup response time | < 300ms (API round-trip) |
| NFR-1.3 | Mutation operations (receive, adjust, transfer) | < 1s (API round-trip) |
| NFR-1.4 | App cold start to login screen | < 2s |
| NFR-1.5 | App warm resume (session still valid) to last screen | < 500ms |
| NFR-1.6 | Scrolling and list rendering | 60fps, no jank. Stock card and PO line lists must support 500+ items without lag. |
| NFR-1.7 | Concurrent users per facility | System supports 50+ concurrent app users per facility without degradation. |
NFR-2: Reliability
| ID | Requirement | Target |
|---|---|---|
| NFR-2.1 | All mutation operations use idempotency keys. Network retry on timeout must not create duplicates. | Zero duplicate transactions from retries |
| NFR-2.2 | Transient network errors (timeout, 429, 5xx) are retried automatically with exponential backoff (max 3 attempts). | Transparent retry for transient failures |
| NFR-2.3 | Non-transient errors (400, 403, 404, 409) are surfaced to the user immediately with actionable messages. No silent swallowing. | Every error visible to user |
| NFR-2.4 | If the backend is in maintenance mode (503), display a clear maintenance message. Do not retry. | Graceful maintenance handling |
NFR-3: Security
| ID | Requirement | Target |
|---|---|---|
| NFR-3.1 | Session token stored in Android Keystore (hardware-backed where available). Never in SharedPreferences or plain storage. | Hardware-backed credential storage |
| NFR-3.2 | No credentials (passcode, session_guid, API keys) written to application logs, crash reports, or analytics. | Zero credential leakage |
| NFR-3.3 | All network communication over HTTPS (TLS 1.2+). Certificate pinning for api.g3nretailstack.com recommended. | Encrypted transport |
| NFR-3.4 | Role-based UI gating is defense-in-depth only. The server enforces all access control (403 on violation). Never rely solely on client-side hiding. | Server-authoritative access control |
| NFR-3.5 | Cost data (unit cost, margin, total value) is never displayed, cached, or logged unless the user has explicit cost visibility roles. | Cost data isolation |
| NFR-3.6 | On session expiry or 401, immediately clear all cached data and stored session. Navigate to login. | Clean session teardown |
NFR-4: Usability
| ID | Requirement | Target |
|---|---|---|
| NFR-4.1 | The app is designed for one-handed operation on standard Android phones (5.5"–6.7" screens). Primary actions reachable by thumb. | One-handed warehouse use |
| NFR-4.2 | Large touch targets for all interactive elements (minimum 48dp). Warehouse workers wear gloves. | Glove-friendly interaction |
| NFR-4.3 | High-contrast UI. Readable in bright warehouse lighting and dim stockroom conditions. Support system dark/light mode. | All-environment readability |
| NFR-4.4 | Barcode scanner activates with a single tap or hardware button. No multi-step navigation to reach the scanner from any screen. | Instant scanner access |
| NFR-4.5 | Confirmation dialogs for destructive or high-impact actions (adjustments, RTV creation, transfer approval). Quick actions (scan, lookup) require no confirmation. | Appropriate confirmation friction |
| NFR-4.6 | Clear visual feedback for all operations: success (green check/toast), error (red banner with message), loading (spinner with operation name). | Unambiguous status feedback |
| NFR-4.7 | All quantities displayed with UOM (unit of measure). Never display a bare number. | Unambiguous quantity display |
NFR-5: Platform & Compatibility
| ID | Requirement | Target |
|---|---|---|
| NFR-5.1 | Android minimum SDK: API 26 (Android 8.0 Oreo). Target SDK: latest stable. | Broad device support |
| NFR-5.2 | Supported device types: low-end to high-end Android handhelds (phones) and Android tablets. Also Zebra TC-series rugged devices (TC21, TC26, TC52, TC57, TC72, TC77 and successors). No Honeywell, no Datalogic. | Device range: budget phone → enterprise rugged |
| NFR-5.3 | Camera is the primary barcode scanner. The app uses the device camera (via ML Kit, ZXing, or similar) for all barcode scanning. No dependency on integrated hardware scanners. On Zebra TC-series devices, the app should also accept scan input from the built-in imager via Zebra DataWedge intent broadcast as a secondary input path for faster scanning in high-volume workflows. | Camera-first scanning, Zebra DataWedge optional |
| NFR-5.4 | The app must function well on small screens (5" budget phone) and large screens (10" tablet). Layouts adapt to screen size. Tablet layout may show master-detail (e.g., PO list + line items side by side). | Responsive layout: phone + tablet |
| NFR-5.5 | Language: English (en_US) only for v1. All server error messages arrive in en_US. | English only |
| NFR-5.6 | Network: requires active network connection (Wi-Fi or cellular). No offline mode. Clear error when network unavailable. | Online-only with clear offline indicator |
NFR-6: Observability & Support
| ID | Requirement | Target |
|---|---|---|
| NFR-6.1 | Every API response's stats.request_id and stats.build.build_id is logged locally (ring buffer, last 20 requests). | Request traceability |
| NFR-6.2 | The X-API-Version response header is logged per-session. If it changes mid-session, log a warning (backend was redeployed). | Version drift detection |
| NFR-6.3 | A "Support" screen shows: current session info (user, org, facility, session age), last 10 request IDs, API version, app version, device model. User can copy this to clipboard for support tickets. | Self-service diagnostics |
| NFR-6.4 | Crash reporting (e.g., Firebase Crashlytics) enabled with session metadata (org, facility, app version) but NOT session_guid or passcode. | Safe crash reporting |
NFR-7: Data Handling
| ID | Requirement | Target |
|---|---|---|
| NFR-7.1 | No local persistent storage of business data (stock levels, POs, products). All data is fetched from the API on demand. | No stale local data |
| NFR-7.2 | Barcode→variant cache is in-memory only (cleared on app kill or session change). | Ephemeral cache only |
| NFR-7.3 | Pagination cursors (next_token) are opaque and treated as ephemeral. Never persisted across sessions. | Cursor hygiene |
| NFR-7.4 | The app does not poll or auto-refresh. User pulls to refresh or taps a refresh button. | Manual refresh only |
1. Architecture Overview
The app talks directly to the g3nretailstack API Gateway — no integration plane, no BFF. Every service is reachable at https://api.g3nretailstack.com/{service}/....
┌─────────────────────┐
│ G3N/Inventory App │
│ (Android / Kotlin) │
└──────────┬──────────┘
│ HTTPS (JSON)
▼
┌──────────────────────────────────────────┐
│ api.g3nretailstack.com │
│ ┌─────┬─────┬─────┬─────┬─────┬──────┐ │
│ │ USM │ UAS │ OFM │ ICS │ PCM │ PVM │ │
│ └─────┴─────┴─────┴─────┴─────┴──────┘ │
└──────────────────────────────────────────┘Services the app uses:
| Service | Purpose | Base path |
|---|---|---|
| USM | Session creation and management | /usm |
| UAS | User snapshot (org memberships) | /uas |
| OFM | Org/facility/role resolution | /ofm |
| ICS | Inventory operations (receive, adjust, transfer, stock) | /ics |
| PCM | Procurement (PO receive, RTV) | /pcm |
| PVM | Product/barcode lookup | /pvm |
2. Authentication & Session Lifecycle
2.1 Login
POST /usm/session/create
Content-Type: application/json
{
"email": "user@merchant.com",
"passcode": "their-passcode",
"ttl_seconds": 3600,
"caption": "G3N/Inventory Android"
}Response (200):
json
{
"success": true,
"data": {
"session_guid": "sess-abc123...",
"user_id": "u-def456...",
"status": "active",
"expires_at_utc": "2026-03-04T14:30:00Z",
"ttl_seconds": 3600
}
}Store session_guid securely (Android Keystore). This is the bearer token for all subsequent calls.
Session properties:
- TTL: 1 hour, sliding — auto-extends on every API call. An active warehouse worker never gets logged out mid-shift.
- Expiry: If idle for 1 hour, session expires. App must detect 401 and prompt re-login.
- Revocation: Sessions can be revoked server-side (logout-everywhere, user suspended). Detect via 401.
2.2 Request Headers (every call after login)
| Header | Required | Value |
|---|---|---|
x-session-guid | Yes | The session_guid from login |
x-orgcode | Yes (after org selected) | Uppercase org code, e.g. ACMECORP |
x-logical-guid | Yes (after facility selected) | Facility GUID for scoped operations |
x-cccode | Optional | Cost centre code (format: XXXX-XXXX-XXXX) |
Content-Type | Yes (POST) | application/json |
2.3 Response Envelope (every response)
Every response follows this shape:
json
{
"success": true,
"data": { ... },
"revision": "rev-abc123",
"stats": {
"service": "ics",
"call": "ics_stock_get",
"request_id": "req-xyz",
"timestamp_utc": "2026-03-04T13:30:00Z",
"build": { "build_id": "MONDAY-1770260725" }
}
}Always log stats.request_id and stats.build.build_id — these are required for support tickets.
Response headers: Every response includes X-API-Version: YYYY-MM-DD (current: 2026-03-04). Log this for version tracking.
2.4 Explicit Logout
POST /usm/session/close
Content-Type: application/json
{ "session_guid": "sess-abc123..." }Response: { data: { session_guid, status: "doomed", doom_reason: "closed" } }. After closing, clear all local state.
2.5 Session Expiry Detection
On any 401 response with tag unauthorized or invalid-session:
- Clear stored session
- Navigate to login screen
- Do not retry the failed request automatically
3. Org & Facility Selection Flow
After login, the app must determine which org and facility the user is acting on.
3.1 Get User Snapshot
POST /uas/stat
Content-Type: application/json
{
"email": "user@merchant.com",
"passcode": "their-passcode"
}Returns the user's profile (emails, passcode state). Note: /uas/stat uses email+passcode auth (public endpoint), not session headers.
Org discovery: The user snapshot does not include org memberships directly. The app should call /ofm/member/resolve for each known orgcode to verify membership, or maintain a list of orgcodes from a prior session. For first-time setup, the user provides their orgcode(s) and the app validates via /ofm/member/resolve.
3.2 Resolve Membership & Roles for Selected Org
POST /ofm/member/resolve
x-session-guid: sess-abc123...
Content-Type: application/json
{
"orgcode": "ACMECORP"
}Returns:
json
{
"success": true,
"data": {
"org_guid": "org-12345",
"orgcode": "ACMECORP",
"org_status": "verified",
"is_owner": false,
"member_state": "active",
"roles": ["ics_operator", "ics_view", "pcm_buyer", "pvm_view"],
"logical_access": false
}
}If org_status is not verified, the org is read-only (writes blocked with 403 org-write-blocked).
3.3 List Facilities
POST /ofm/facility/logical/list
x-session-guid: sess-abc123...
x-orgcode: ACMECORP
Content-Type: application/json
{
"org_guid": "org-12345",
"status": "active"
}Note: This endpoint requires org_guid (from the /ofm/member/resolve response), not orgcode. It also requires owner-level access. For non-owner members, the app should use /ofm/member/resolve with each known logical_guid to check facility access, or the backend should provide a member-scoped facility list.
Returns { facilities: [...] } where each facility includes logical_guid, code, caption, physical_guid, legal_guid, status. Build a facility picker from this.
3.4 Resolve Facility-Scoped Roles
After user picks a facility, resolve roles again with logical_guid:
POST /ofm/member/resolve
x-session-guid: sess-abc123...
Content-Type: application/json
{
"orgcode": "ACMECORP",
"logical_guid": "lq-facility-001"
}Returns facility-scoped roles in logical_roles. If logical_access is false, the user has no assignment to this facility — show an error.
Key fields to check:
logical_access: true— user has facility assignmentlogical_roles— facility-scoped roles (may differ from org-levelroles)is_owner: true— owner bypasses all role checks
4. Role-Based Capability Mapping
After facility selection, derive the app's feature flags from the user's roles. The server enforces roles on every call (403 on violation), but the app should hide UI for inaccessible features.
4.1 Role → Feature Map
kotlin
data class Capabilities(
val receiving: Boolean, // PO receiving + ICS receive
val putaway: Boolean, // Move stock to storage
val pickPackShip: Boolean, // Fulfillment operations
val adjustments: Boolean, // Qty adjustments, stock transitions
val counts: Boolean, // Inventory cycle counts
val transferCreate: Boolean, // Create transfer requests
val transferApprove: Boolean, // Approve transfer requests
val transferReceive: Boolean, // Receive inbound transfers
val rtvCreate: Boolean, // Return to vendor
val stockLookup: Boolean, // View stock levels + cards
val productLookup: Boolean, // Product info + barcode scan
val comments: Boolean, // Flag for review with comments
val poLookup: Boolean, // View purchase orders
val costView: Boolean // See cost data
)
fun deriveCapabilities(roles: List<String>, isOwner: Boolean): Capabilities {
if (isOwner) return Capabilities(/* all true */)
return Capabilities(
receiving = "ics_operator" in roles || "pcm_buyer" in roles,
putaway = "ics_operator" in roles,
pickPackShip = "ics_operator" in roles,
adjustments = "ics_adjust" in roles || "ics_operator" in roles,
counts = "ics_count" in roles,
transferCreate = "ics_operator" in roles,
transferSubmit = "ics_transfer_approve" in roles,
transferApprove = "ics_transfer_approve" in roles,
transferReceive = "ics_operator" in roles,
rtvCreate = "pcm_buyer" in roles,
stockLookup = roles.any { it.startsWith("ics_") },
productLookup = "pvm_view" in roles || roles.any { it.startsWith("ics_") },
comments = "ics_operator" in roles || "ics_planner" in roles || "ics_adjust" in roles,
poLookup = "pcm_view" in roles || "pcm_buyer" in roles || "pcm_po_approve" in roles || "finance_audit" in roles,
costView = "cost_view" in roles || "ics_cost_admin" in roles || "finance_audit" in roles
)
}4.2 Standard Role Profiles
OFM assigns role profiles to members. These expand server-side:
| Profile | Roles Granted |
|---|---|
warehouse_staff | ics_view, ics_operator, ics_count |
warehouse_manager | ics_view, ics_operator, ics_planner, ics_adjust, ics_count, ics_transfer_approve, pcm_view, pcm_buyer |
store_manager | crm_view, crm_manage, ics_view, ics_operator, scm_view, scm_order, scm_fulfillment, scm_returns, ppm_view, pcm_view |
buyer | pvm_view, pcm_view, pcm_buyer, ppm_view, ics_view |
A warehouse_staff user sees: stock lookup, receiving, putaway, counts. No adjustments, no transfers approve, no RTV. A warehouse_manager sees everything.
5. Barcode Scanning
Barcode scanning is the primary input method. The scan→resolve→action chain must be fast.
5.1 Resolve Barcode
GET /pvm/barcode/resolve?orgcode=ACMECORP&value=5901234123457
x-session-guid: sess-abc123...
x-orgcode: ACMECORPResponse (200):
json
{
"success": true,
"data": {
"barcode": {
"barcode_id": "bc-001",
"variant_id": "v-red-shirt-s",
"value": "5901234123457",
"scheme": "EAN13",
"packaging_level": "unit",
"is_primary": true,
"status": "active"
},
"owner": {
"variant_id": "v-red-shirt-s",
"variant_code": "SHIRT-RED-S"
}
}
}Latency target: ~100ms (Tier B SLO). Fire this immediately on scan.
GTIN-only: /pvm/barcode/resolve only accepts GTIN-standard barcodes (8, 12, 13, or 14 digit codes with valid check digit — EAN-8, UPC-A, EAN-13, GTIN-14). For non-GTIN codes (Code 128, Code 39, QR, DataMatrix, vendor SKUs), use /pvm/identifier/resolve instead.
5.2 Resolve Identifier (UPC, vendor SKU, etc.)
GET /pvm/identifier/resolve?orgcode=ACMECORP&type=sku&value=VENDOR-SKU-123
x-session-guid: sess-abc123...
x-orgcode: ACMECORPUse when the scanned code isn't a standard barcode (e.g., vendor-specific label).
5.3 Get Product Details
After resolving variant_id:
GET /pvm/variant/get?orgcode=ACMECORP&variant_id=v-red-shirt-s&style_id=s-red-shirt
x-session-guid: sess-abc123...
x-orgcode: ACMECORPReturns full variant details: name, color, size, images, status, cost (if cost_view role), etc.
5.4 Configurable Kits
Variants can have a kit_type field: standard (fixed BOM) or configurable (customer picks components from slots). The app should detect kit_type on resolved variants and adjust workflows accordingly.
Key endpoints (all require pvm_view or equivalent):
| Method | Path | Purpose |
|---|---|---|
| POST | /pvm/kit/slot/add | Add a slot to a configurable kit variant |
| GET | /pvm/kit/slot/list | List slots for a kit variant |
| POST | /pvm/kit/slot/choice/add | Add a variant choice to a slot |
| GET | /pvm/kit/slot/choice/list | List available choices for a slot |
| POST | /pvm/kit/configure/validate | Validate a customer's kit configuration against rules |
| POST | /pvm/kit/rule/add | Add a constraint rule (inclusion/exclusion) |
| GET | /pvm/kit/rule/list | List rules for a kit variant |
Impact on inventory workflows:
- Receiving: Kit variants with
kit_type=standardare received as a single unit; SCM explodes them into component stock positions automatically. - Transfers: Same explosion behavior applies — transfer the kit SKU, and ICS tracks component-level stock.
- Configurable kits: Not typically stocked as assembled units. The app receives/transfers individual component variants. Validation (
/pvm/kit/configure/validate) is used at order time, not at receiving time.
5.5 Performance Pattern
For scan-and-display:
- Scan → call
/pvm/barcode/resolve(~100ms) - Parallel → call
/ics/stock/getwithvariant_id+logical_guid(~250ms) - Display: product name + current stock level within ~350ms total
Cache recommendation: LRU cache for barcode→variant mappings (5-min TTL, 500 entries). Product info changes rarely; stock levels should not be cached.
6. Inventory Operations (ICS)
All ICS mutation endpoints require x-logical-guid header. All support optional idempotency_key for safe retry.
request_context on mutations. Most ICS mutations derive request_context automatically from the x-session-guid and x-orgcode headers — you do not need to include it in the request body. The exceptions are endpoints that validate a nested request_context in the body:
POST /ics/transfer/request/create— requirestransfer.request_contextPOST /ics/reservation/create— requiresreservation.request_contextPOST /ics/allocation/create— requiresallocation.request_contextPOST /ics/commit/create— requirescommit.request_context
For those endpoints, include:
json
{
"transfer": {
"request_context": {
"session_guid": "<from login>",
"orgcode": "ACMECORP",
"actor": "<user_guid>",
"context_source": "session",
"session_fingerprint": "<SHA-256 hex of 'session_guid:<value>'>"
},
...other transfer fields...
}
}The session_fingerprint is SHA256("session_guid:" + sessionGuid) as a lowercase hex string. Compute it once when the session is created.
policy_versions required on mutations. Adjustment, transition, receiving, and transfer endpoints require a policy_versions object (e.g., { "adjustment_policy_version": "default" }). Use "default" until custom policies are configured.
Reason codes. All reason_code fields must use the canonical allowlist format: INVENTORY.SHRINK, INVENTORY.DAMAGE, INVENTORY.ADJUST, INVENTORY.RECALL, INVENTORY.PUTAWAY, INVENTORY.ADJUST_FIFO, INVENTORY.ADJUST_FEFO. Lowercase or unscoped codes (e.g., "shrinkage") are rejected.
6.1 Stock Lookup
POST /ics/stock/get
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{ "variant_id": "v-red-shirt-s" }Roles required: ics_view, ics_operator, ics_planner, ics_adjust, ics_count, ics_transfer_approve, ics_cost_admin, finance_audit
POST /ics/stock/list
{ "limit": 20, "next_token": null }6.2 Stock Card (Ledger History)
POST /ics/stock/card/list
{ "variant_id": "v-red-shirt-s", "limit": 20 }Returns the chronological ledger of all stock movements for a variant at the current facility.
6.3 Receiving (ICS side)
POST /ics/receive/record
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"receipt": {
"lines": [
{
"variant_id": "v-red-shirt-s",
"qty": { "qty": 50, "uom": "EACH" }
}
],
"receiving_zone_code": "RECEIVING",
"receiving_bin_code": "RECEIVING-01",
"qc_required": false,
"policy_versions": { "receiving_policy_version": "default" }
},
"reason": "PO-2026-001 partial receipt",
"source_refs": [{ "kind": "po", "id": "po-guid-001" }],
"idempotency_key": "recv-po001-line3-attempt1"
}Notes: Multi-line receiving is supported (up to 20 lines per call). qc_required: true puts received items into QC hold status.
Roles required: ics_operator
6.4 QC Complete
POST /ics/qc/complete
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"qc": {
"receipt_id": "rcv-guid-001",
"lines": [
{
"line_id": "line-001",
"passed_qty": { "qty": 47, "uom": "EACH" },
"failed_qty": { "qty": 3, "uom": "EACH" }
}
]
},
"reason": "Visual inspection — 3 units with torn packaging",
"source_refs": [{ "kind": "receive", "id": "rcv-guid-001" }]
}Notes: QC is per-line with separate passed_qty/failed_qty (not a simple pass/fail disposition). Each line_id must match a line from the original receipt.
Roles required: ics_operator
6.5 Putaway
POST /ics/putaway/record
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"putaway": {
"variant_id": "v-red-shirt-s",
"qty": { "qty": 50, "uom": "EACH" },
"to_zone_code": "STOCKROOM",
"to_bin_code": "STOCKROOM-02"
},
"reason": "Standard putaway from receiving",
"source_refs": [{ "kind": "receive", "id": "rcv-guid-001" }],
"idempotency_key": "putaway-rcv001-attempt1"
}Notes: from_zone_code/from_bin_code are not required for putaway (the system knows where the stock currently is). Only to_zone_code/to_bin_code are required.
Roles required: ics_operator
6.6 Qty Adjustment
POST /ics/adjustment/create
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"adjustment": {
"variant_id": "v-red-shirt-s",
"delta_qty": { "qty": -3, "uom": "EACH" },
"reason_code": "INVENTORY.DAMAGE",
"policy_versions": { "adjustment_policy_version": "default" }
},
"reason": "Found 3 units water-damaged in bin A-03-02",
"source_refs": [{ "kind": "count", "id": "count-guid-001" }],
"idempotency_key": "adj-damaged-20260304-1"
}Valid reason codes: INVENTORY.ADJUST, INVENTORY.SHRINK, INVENTORY.DAMAGE, INVENTORY.RECALL, INVENTORY.PUTAWAY, INVENTORY.ADJUST_FIFO, INVENTORY.ADJUST_FEFO.
Roles required: ics_adjust or ics_operator
6.7 Stock Transition (bucket change)
POST /ics/stock/transition
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"transition": {
"variant_id": "v-red-shirt-s",
"qty": { "qty": 3, "uom": "EACH" },
"from_bucket": "available",
"to_bucket": "damaged",
"reason_code": "INVENTORY.DAMAGE",
"policy_versions": { "transition_policy_version": "default" }
},
"reason": "Water damage identified during count",
"source_refs": [{ "kind": "adjustment", "id": "adj-guid-001" }]
}Valid buckets: available, quarantine, damaged. from_bucket and to_bucket must differ.
Roles required: ics_adjust or ics_operator
6.8 Transfers
Transfer lifecycle: created → submitted → approved → shipped → received.
Create transfer request (between facilities):
POST /ics/transfer/request/create
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-warehouse-001
{
"transfer": {
"request_context": {
"session_guid": "...", "orgcode": "ACMECORP", "actor": "...",
"context_source": "session", "session_fingerprint": "...",
"logical_guid": "lq-warehouse-001"
},
"source_logical_guid": "lq-warehouse-001",
"dest_logical_guid": "lq-store-002",
"lines": [
{ "variant_id": "v-red-shirt-s", "qty": { "qty": 20, "uom": "EACH" } }
],
"policy_refs": { "transfer_policy_version": "default" }
},
"reason": "Store replenishment",
"source_refs": [{ "kind": "webapp", "id": "dev-seed" }]
}Roles: ics_operator. This is one of the few endpoints that requires request_context nested inside the body (at transfer.request_context). Response: { data: { transfer: { transfer_id, status: "requested", ... } } }.
Submit for approval:
POST /ics/transfer/request/submit
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-warehouse-001
{ "transfer_id": "tr-guid-001", "expected_revision": 1, "reason": "Ready for review", "source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }] }Roles: ics_transfer_approve. Moves from requested → submitted.
Approve transfer:
POST /ics/transfer/request/approve
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-warehouse-001
{ "transfer_id": "tr-guid-001", "expected_revision": 2, "reason": "Approved", "source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }] }Roles: ics_transfer_approve. Moves from submitted → approved. Note: expected_revision is required (numeric, starts at 1, increments on each state change).
Record shipment (send from source facility):
POST /ics/transfer/shipment/record
x-logical-guid: lq-warehouse-001
{
"shipment": {
"transfer_id": "tr-guid-001",
"lines": [
{ "variant_id": "v-red-shirt-s", "qty": { "qty": 20, "uom": "EACH" } }
]
},
"reason": "Shipped via internal courier",
"source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }]
}Roles: ics_operator. Response: { data: { shipment: { shipment_id, ... } } }.
Receive shipment (at destination facility):
POST /ics/transfer/shipment/receive
x-logical-guid: lq-store-002
{
"shipment_id": "ship-guid-001",
"expected_revision": 1,
"reason": "Transfer received, all units good",
"source_refs": [{ "kind": "webapp", "id": "g3n-inventory" }]
}Roles: ics_operator. If received_lines is omitted, all shipped lines are received in full. For partial receipt or discrepancies, include received_lines with per-line quantities. Short shipments generate FULFILLMENT.SHORT exceptions automatically.
List transfer requests:
POST /ics/transfer/request/list
{ "status": "submitted", "limit": 20 }List shipments:
POST /ics/transfer/shipment/list
{ "transfer_id": "tr-guid-001", "limit": 20 }6.9 Comments (Flag for Review)
POST /ics/comment
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"target_type": "stock",
"target_id": "v-red-shirt-s",
"body": "Stock level doesn't match shelf count. Flagging for full recount.",
"hashtags": ["discrepancy"]
}Response: { data: { comment: { comment_id, target_type, target_id, body, hashtags, status, created_at, ... } } }
Roles required: ics_operator, ics_planner, or ics_adjust
Additional comment operations:
| Endpoint | Request Fields | Notes |
|---|---|---|
POST /ics/comment/get | comment_id, target_type, target_id | |
POST /ics/comment/list | target_type, target_id, optional status, hashtag, limit, next_token | Paginated |
POST /ics/comment/revise | comment_id, target_type, target_id, body, optional expected_revision | Author-only |
POST /ics/comment/status | comment_id, target_type, target_id, status (current/archived/doomed) | Author-only |
Notes: Comments also support caption, attachments (MRS references), parent_comment_id (threaded replies), mentions_users, mentions_teams. At least one of caption, body, or attachments is required.
7. Procurement Operations (PCM)
7.1 PO Lookup
POST /pcm/po/get
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{ "po_id": "po-guid-001" }Roles required: pcm_view, pcm_buyer, pcm_po_approve, finance_audit
POST /pcm/po/list
{ "status": "issued", "limit": 20 }7.2 PO Receipt (partial or full)
POST /pcm/receipt/record
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"receipt": {
"po_id": "po-guid-001",
"request_context": {
"session_guid": "...", "orgcode": "ACMECORP", "actor": "...",
"context_source": "session", "session_fingerprint": "..."
},
"lines": [
{
"po_line_id": "pol-001",
"variant_id": "v-red-shirt-s",
"qty": { "qty": 50, "uom": "EACH" },
"rejected_qty": 3,
"rejection_reason": "RECEIVING.DAMAGED"
}
]
},
"reason": "Partial receipt - 1 of 3 pallets",
"source_refs": [{ "kind": "po", "id": "po-guid-001" }],
"idempotency_key": "receipt-po001-pallet1"
}Rejection reason codes (RECEIVING.* taxonomy — server-validated by PCM):
| Code | Use when |
|---|---|
RECEIVING.DAMAGED | Physical damage during shipping |
RECEIVING.DEFECTIVE | Manufacturing defect |
RECEIVING.WRONG_ITEM | Item doesn't match PO line |
RECEIVING.SHORT_SHIPMENT | Fewer units than BOL claims |
RECEIVING.QUALITY_ISSUE | Fails quality standards |
RECEIVING.EXPIRED | Past expiry / insufficient shelf life |
RECEIVING.PACKAGING | Packaging compromised |
RECEIVING.OTHER | Catch-all (add free-text in reason field) |
Notes: The receipt body must be wrapped in a receipt object. request_context goes inside receipt, not at the top level. Response: { data: { receipt: { receipt_id, status, ... }, po_status: "received" | "partially_received" } }.
Roles required: pcm_buyer
Partial receiving is native — include the lines and quantities received in this shipment. Repeat for subsequent pallets/shipments.
PO status auto-update: When po_id is provided, the server automatically transitions the PO status based on cumulative received quantities across all receipts:
- All PO lines fulfilled → PO status becomes
received - Some lines still pending → PO status becomes
partially_received - The response includes
po_statusso the app can update the UI immediately without re-fetching the PO. - Receipt default status is
received(notrecorded).
Note on cumulative tracking: To show received vs. ordered progress, the app can fetch receipts for the PO (via /pcm/receipt/list { po_id }) and compute the cumulative totals client-side, or simply check the PO's own status field which is auto-updated on each receipt.
7.3 Return to Vendor (RTV)
POST /pcm/rtv/create
x-session-guid: ... | x-orgcode: ACMECORP | x-logical-guid: lq-001
{
"rtv": {
"vendor_ref": { "vendor_code": "VENDCORP" },
"request_context": {
"session_guid": "...", "orgcode": "ACMECORP", "actor": "...",
"context_source": "session", "session_fingerprint": "..."
},
"lines": [
{
"variant_id": "v-red-shirt-s",
"qty": 5,
"reason_code": "damaged",
"reason": "Water-damaged units from PO-2026-001"
}
]
},
"reason": "Rejecting damaged goods from PO receipt",
"source_refs": [{ "kind": "receipt", "id": "rcpt-guid-001" }],
"idempotency_key": "rtv-po001-damaged-batch1"
}Notes: The body must be wrapped in an rtv object. vendor_ref is an object (not a bare vendor_code string). request_context goes inside rtv. reason at the top level is required. RTV-level reason codes are not validated against the canonical allowlist (stored as-is), but we recommend using the CATEGORY.CODE format for consistency.
Roles required: pcm_buyer
8. Error Handling
8.1 HTTP Status Codes
| Status | Tag | Meaning | App Action |
|---|---|---|---|
| 200 | — | Success | Process response |
| 400 | validation-error | Invalid input | Show field-level errors |
| 401 | unauthorized, invalid-session | Session expired/revoked | → Login screen |
| 403 | forbidden-role | User lacks required role | Show "access denied" |
| 403 | org-write-blocked | Org not verified | Show "org not active" |
| 403 | (no JSON body) | WAF blocked request | See section 8.4 |
| 404 | not-found | Record missing or anti-enumeration | Show "not found" |
| 409 | conflict | Revision mismatch | Re-fetch and retry |
| 428 | expected-revision-required | Missing expected_revision | Re-fetch to get current revision |
| 429 | throttled | Rate limited | Backoff and retry after 1-5s |
| 500 | internal-error | Server error | Show generic error, log request_id |
| 502 | dependency-unavailable | Upstream service down | Retry with backoff |
| 503 | maintenance-active | Scheduled maintenance | Show maintenance message |
8.2 Error Response Shape
json
{
"success": false,
"error": {
"error_code": "ics.validation_failed",
"http_status": 400,
"retryable": false,
"major": { "tag": "validation-error", "message": { "en_US": "Invalid input" } },
"details": { "field": "qty", "reason": "must be positive" }
},
"stats": { "request_id": "req-xyz", ... }
}Use error.retryable to decide whether to retry. Use error.major.tag for programmatic handling. Use error.major.message.en_US for user-facing display.
8.3 Retry Strategy
- Retryable errors (429, 500, 502, 504): exponential backoff with jitter (100ms, 200ms, 400ms, max 3 attempts).
- Non-retryable errors (400, 401, 403, 404, 409, 428): do not retry. Handle in the UI.
- Network errors (timeout, connection refused): retry with backoff, max 3 attempts.
- Mutations: always include
idempotency_keyso retries are safe.
8.4 WAF (Web Application Firewall)
All API endpoints and CloudFront distributions are protected by AWS WAF. WAF inspects every request at the network layer before it reaches the application.
How to distinguish WAF 403 from application 403:
- Application 403: Returns a JSON body with
error.major.tag(e.g.,forbidden-role,org-write-blocked). - WAF 403: Returns a short HTML body (
<html>... 403 Forbidden ...</html>) with no JSON. Noerrorobject, norequest_id.
When WAF blocks a request:
- The request body contained a pattern matching a known attack signature (SQL injection, XSS, JNDI injection, path traversal).
- The caller's IP address is on a known-bad-actor list.
- The caller exceeded 2,000 requests per 5-minute window from the same IP.
App handling:
- If you receive a 403 with no JSON body, do not retry. Check the request payload for unusual characters or patterns.
- If the block is unexpected, it may be a false positive from product descriptions or free-text fields containing HTML-like content. Contact support with the request timestamp and source IP.
WAF rules active:
- AWS Managed Rules: Common Rule Set (OWASP top 10), Known Bad Inputs (Log4j, etc.), Amazon IP Reputation List.
- Rate limit: 2,000 requests per 5-minute window per source IP.
9. Idempotency
All mutation endpoints accept an optional idempotency_key field. This prevents duplicate operations when the app retries after a network timeout.
9.1 How It Works
- App generates a unique key per user action (e.g.,
"recv-po001-line3-{uuid}"). - Sends it in the request body as
idempotency_key. - If the server already processed that key (within 24h), it returns the cached response.
- If the same key is sent with a different payload, server returns
409 idempotency-conflict.
9.2 Key Generation Pattern
kotlin
fun idempotencyKey(action: String, contextId: String): String {
return "$action-$contextId-${UUID.randomUUID()}"
}
// Usage:
val key = idempotencyKey("receive", "po-001-line-3")Generate the key once per user action (not per retry). Store it locally until the request succeeds or the user abandons the action.
10. App-Relevant Endpoint Reference
ICS — Inventory Control
| Category | Method | Path | Roles |
|---|---|---|---|
| Stock | POST | /ics/stock/get | READ |
| POST | /ics/stock/list | READ | |
| POST | /ics/stock/card/list | READ | |
| POST | /ics/stock/policy/get | READ | |
| POST | /ics/stock/transition | ADJUST | |
| Receiving | POST | /ics/receive/record | OPERATOR |
| POST | /ics/receive/get | READ | |
| POST | /ics/receive/list | READ | |
| POST | /ics/qc/complete | OPERATOR | |
| Putaway | POST | /ics/putaway/record | OPERATOR |
| Movement | POST | /ics/pick/record | OPERATOR |
| POST | /ics/pack/record | OPERATOR | |
| POST | /ics/ship/record | OPERATOR | |
| Adjustment | POST | /ics/adjustment/create | ADJUST |
| Transfers | POST | /ics/transfer/request/create | OPERATOR |
| POST | /ics/transfer/request/submit | TRANSFER_APPROVE | |
| POST | /ics/transfer/request/approve | TRANSFER_APPROVE | |
| POST | /ics/transfer/request/get | READ | |
| POST | /ics/transfer/request/list | READ | |
| POST | /ics/transfer/shipment/record | OPERATOR | |
| POST | /ics/transfer/shipment/receive | OPERATOR | |
| POST | /ics/transfer/shipment/get | READ | |
| POST | /ics/transfer/shipment/list | READ | |
| POST | /ics/transfer/suggest | PLANNER | |
| POST | /ics/transfer/report | READ | |
| Bins/Zones | POST | /ics/bin/get | READ |
| POST | /ics/bin/list | READ | |
| POST | /ics/zone/get | READ | |
| POST | /ics/zone/list | READ | |
| Comments | POST | /ics/comment | COMMENT_WRITE |
| POST | /ics/comment/get | READ | |
| POST | /ics/comment/list | READ | |
| POST | /ics/comment/revise | COMMENT_WRITE | |
| POST | /ics/comment/status | COMMENT_WRITE |
Role groups: READ = ics_view, ics_operator, ics_planner, ics_adjust, ics_count, ics_transfer_approve, ics_cost_admin, finance_audit. OPERATOR = ics_operator. ADJUST = ics_adjust, ics_operator. PLANNER = ics_planner, ics_operator. TRANSFER_APPROVE = ics_transfer_approve. COMMENT_WRITE = ics_operator, ics_planner, ics_adjust.
PCM — Procurement
| Category | Method | Path | Roles |
|---|---|---|---|
| PO | POST | /pcm/po/get | READ |
| POST | /pcm/po/list | READ | |
| Receipt | POST | /pcm/receipt/record | BUYER |
| POST | /pcm/receipt/get | READ | |
| POST | /pcm/receipt/list | READ | |
| RTV | POST | /pcm/rtv/create | BUYER |
| POST | /pcm/rtv/get | READ | |
| POST | /pcm/rtv/list | READ | |
| Credit | POST | /pcm/credit/apply | PO_APPROVE |
Role groups: READ = pcm_view, pcm_buyer, pcm_po_approve, finance_audit. BUYER = pcm_buyer. PO_APPROVE = pcm_po_approve.
PVM — Product Lookup
| Category | Method | Path | Roles |
|---|---|---|---|
| Barcode | GET | /pvm/barcode/resolve | READ |
| GET | /pvm/barcode/list | READ | |
| Identifier | GET | /pvm/identifier/resolve | READ |
| GET | /pvm/identifier/list | READ | |
| Product | GET | /pvm/variant/get | READ |
| GET | /pvm/variant/list | READ | |
| GET | /pvm/style | READ | |
| GET | /pvm/style/get | READ | |
| GET | /pvm/resolve/code | READ |
Role groups: READ = pvm_view (mapped as pvv internally). Owner access bypasses role checks.
11. Workflow Examples
11.1 Receive a PO Shipment
1. Browse open POs
→ POST /pcm/po/list { status: "issued" }
→ PO list shows vendor name + issued date for quick identification
→ User selects the matching PO
2. Review PO lines + receipt history
→ App shows line items with ordered/received/remaining quantities
→ Collapsible receipt history: each prior receipt shows timestamp,
line count, totals, and per-line detail (expand to see)
→ Over-receipt warning: if received_total > ordered_qty on any line,
the app flags it (warn, don't block)
3. Scan item barcode
→ GET /pvm/barcode/resolve → variant_id
→ Match against PO line
4. Record PCM receipt (with rejections)
→ POST /pcm/receipt/record {
receipt: { po_id, request_context: {...},
lines: [{variant_id, qty: {qty, uom:"EA"},
rejected_qty: N, rejection_reason: "RECEIVING.DAMAGED"}]
}, reason: "...", source_refs: [...]
}
→ rejection_reason must be a RECEIVING.* taxonomy code (server-validated)
→ Partial receiving: repeat for each pallet
5. ICS receiving (stock inbound)
→ POST /ics/receive/record { receipt: { lines: [{variant_id, qty: {qty, uom:"EA"}}], receiving_zone_code: "RECEIVING", receiving_bin_code: "RECEIVING-01", policy_versions: {...} }, reason: "...", source_refs: [...] }
6. QC inspection (if required)
→ POST /ics/qc/complete { qc: { receipt_id, lines: [{line_id, passed_qty: {qty, uom}, failed_qty: {qty, uom}}] }, reason: "...", source_refs: [...] }
7. Putaway to storage location
→ Scan bin barcode → resolve to bin_code
→ POST /ics/putaway/record { putaway: { variant_id, qty: {qty, uom:"EA"}, to_zone_code: "STOCKROOM", to_bin_code: "STOCKROOM-02" }, reason: "...", source_refs: [...] }11.2 Process an Inbound Transfer
1. View pending inbound transfers
→ POST /ics/transfer/shipment/list { status: "shipped", limit: 20 }
2. Select shipment, scan each item
→ GET /pvm/barcode/resolve → variant_id
→ Match against shipment manifest
3. Record receipt (defaults to full receipt if received_lines omitted)
→ POST /ics/transfer/shipment/receive {
shipment_id, expected_revision: 1, reason: "...", source_refs: [...]
}
→ For partial receipt: include received_lines: [{ line_id, variant_id, qty: {qty, uom} }]
4. Flag exceptions with comments
→ POST /ics/comment { target_type: "transfer_shipment", target_id: shipment_id,
body: "3 units arrived damaged, photos attached via MRS" }11.3 Adjust Stock & Flag for Review
1. Scan product
→ GET /pvm/barcode/resolve → variant_id
2. Check current stock
→ POST /ics/stock/get { variant_id }
3. Create adjustment
→ POST /ics/adjustment/create {
adjustment: { variant_id, delta_qty: { qty: -3, uom: "EA" }, reason_code: "INVENTORY.SHRINK",
policy_versions: { adjustment_policy_version: "default" } },
reason: "Shelf count 47, system 50", source_refs: [{ kind: "recount", id: "manual" }]
}
4. Add review comment
→ POST /ics/comment { target_type: "stock", target_id: "v-...",
body: "Shelf count 47, system shows 50. Adjusted -3. Needs investigation." }11.4 Informative UI Pattern (design standard)
All screens in G3N/Inventory follow the informative UI pattern established by the Receiving page. This means:
- Human-readable identifiers — show vendor names, facility names, product codes; never raw GUIDs
- History-first — show existing state/history before the action form (e.g., comments shows existing comments above the compose form)
- Collapsible detail — list views are expandable; collapsed shows summary (counts, badges), expanded shows per-line detail
- Status color coding — consistent across all screens: submitted=yellow, approved=blue, shipped=purple, received/completed=green, rejected/cancelled=red
- Taxonomy reason codes — all reason inputs use the shared
RECEIVING.*/INVENTORY.*taxonomy fromreasonCode.ts; displayed as colored badges - Over-limit warnings — flag unusual conditions (over-receipt, large adjustments) prominently but don't block
- Confirmation previews — before mutations, show a human-readable preview of what will happen
- Post-action refresh — after recording, refresh data to show updated state; show inline confirmation message
Shared infrastructure (in webapp/src/inventory/lib/):
| Module | Purpose |
|---|---|
format.ts | fmtDate(), fmtTime(), fmtRelative() — consistent date formatting |
reasonCodes.ts | reasonInfo(code) → {label, color, bgColor}, reasonCodesFor(category), actionLabel(type) |
nameResolver.ts | loadMemberNames(org_guid) → bulk-load all org member display names via POST /ofm/member/names, resolveActorNames(guids[], org_guid?) → batch display names (triggers loadMemberNames on first call), resolveActorName(guid) → single name from cache, loadVendors() → vendor dropdown via PVM, resolveVendorName(ref), cacheFacilities(facilities[]) → populate facility cache from context, resolveFacilityName(guid) → caption, resolveFacilityCode(guid) → code, displayEntityId(entity, type), clearNameCaches() → call on logout. All caches are session-lifetime. |
Shared components (in webapp/src/inventory/components/):
| Component | Purpose |
|---|---|
HistoryList | Generic collapsible history list with expandable per-entry detail |
StatusBadge | Color-coded status badge (used on transfers, product status, comments) |
11.5 Screen-by-Screen Guide (complete functional + API reference)
This section describes every screen in G3N/Inventory from the merchant's perspective — what it does, who uses it, why it matters, and the exact API calls in order.
Base URL: https://api.g3nretailstack.comAll requests carry headers: x-session-guid, x-orgcode, x-logical-guid
Screen 1: Scan — "What is this item?"
What it is: The "what is this?" button. Staff picks up an item, scans it, instantly sees what it is and how much stock they have.
Who uses it: Everyone — cashiers checking if something's in stock for a customer, warehouse staff identifying unmarked items, managers doing spot checks.
What it does:
- Camera-based barcode scan or manual entry (type a UPC/SKU)
- Shows: product name, variant (color, size), image, barcode value
- Shows: current stock levels at this facility
- If the user has cost visibility permission, also shows cost data
Merchant value: "A customer asks if you have this shirt in Medium. Staff scans the Large on the shelf, sees the product, checks stock — Medium shows 3 available. No walking to a computer, no calling the back room."
Real-world use: 50+ times per day per store. The most-used screen by far. Takes <2 seconds from scan to answer.
API flow:
Step 1: Resolve barcode → product identity
GET /pvm/barcode/resolve?orgcode={orgcode}&value={barcode}
→ { data: { barcode: { variant_id, variant_code }, owner: { variant_code } } }
If not a GTIN barcode (manual SKU entry):
GET /pvm/identifier/resolve?orgcode={orgcode}&type=sku&value={input}
→ { data: { identifier: { variant_id } } }
Step 2: Load product details
GET /pvm/variant/get?orgcode={orgcode}&variant_id={variant_id}
→ { data: { variant_code, style_id, name, color, size, images } }
Step 3: Load stock at this facility
POST /ics/stock/get
{ variant_id: "abc-123" }
→ { data: { on_hand: 47, available: 42, reserved: 3, committed: 2, in_transit: 5 } }Screen 2: Stock Lookup — "Full stock history"
What it is: The deeper version of Scan. Not just "what is this" but "tell me everything about its stock history."
Who uses it: Warehouse managers investigating discrepancies, finance/audit staff reviewing cost movements, planners checking reorder points.
What it does:
- Stock Levels tab: Full 9-bucket breakdown — on-hand, reserved, allocated, committed, available, in-transit, quarantine, damaged, consignment
- Stock Card tab: Complete chronological ledger of every stock movement — receives, adjustments, transfers, picks, putaways, returns. Each entry shows: date, movement type (human-readable label), quantity change (+/-), reason code (colored badge), and running totals
- Paginated — "Load more" for long histories
Merchant value: "Your physical count says 47 but the system says 52. Stock Lookup shows the full ledger — you see 5 units were adjusted out yesterday with reason 'Damage/Shrinkage' by Bob at 3pm. Mystery solved."
Real-world use: During cycle counts, discrepancy investigations, and when something doesn't add up. Maybe 5-10 times per day.
API flow:
Steps 1-3: Same as Scan (barcode resolve + variant get + stock get)
Step 4: Load stock card ledger (paginated)
POST /ics/stock/card/list
{ variant_id: "abc-123", limit: 20 }
→ { data: { ledger: [
{ timestamp, movement_type: "receive", qty: +50, reason_code: "RECEIVING.OTHER", actor_ref: "user-guid" },
{ timestamp, movement_type: "adjustment", qty_delta: -3, reason_code: "INVENTORY.SHRINK" },
...
], next_token: "..." } }
Step 5: Load more (pagination)
POST /ics/stock/card/list
{ variant_id: "abc-123", limit: 20, next_token: "eyJQSyI6..." }
→ next page of ledger entriesScreen 3: Receiving — "Goods arrived from vendor"
What it is: The "goods in" workflow. When a truck arrives with a purchase order, this is how stock enters the system.
Who uses it: Receiving clerks, warehouse managers, anyone processing inbound shipments.
What it does:
- Shows all issued purchase orders waiting to be received (list with vendor name, date, PO code)
- Tap a PO → see every line: product, ordered qty, already received, remaining
- Enter receive qty and reject qty with reason code (Damaged, Defective, Wrong Item, Quality Issue, Expired, Packaging Issue, Short Shipment, Other)
- Receipt history — collapsible timeline of previous receipts against this PO
- Over-receipt warning — highlights in red but doesn't block
- Submit → records PCM receipt + ICS inbound stock atomically
Merchant value: "A truck arrives with 200 boxes from Nike. The clerk opens Receiving, taps the Nike PO, counts boxes, enters quantities. 3 boxes are crushed — reject 3 with 'Damaged'. Submit. Inventory is instantly updated, vendor performance auto-calculates, and the buyer sees which items still haven't arrived."
Real-world use: Every delivery. 2-3 times per day (small store) to 20+ times per day (distribution center). This is where inventory accuracy starts.
API flow:
Step 1: Load purchase orders (page load — issued + partially received)
POST /pcm/po/list
{ status: "issued", limit: 50 }
→ { data: { pos: [{ po_id, po_code: "PO-2026-0042", vendor_ref: { vendor_code: "NIKE" }, issued_at }] } }
Also load partially received POs (can still receive remaining lines):
POST /pcm/po/list
{ status: "partially_received", limit: 50 }
Vendor names pre-loaded for display:
GET /pvm/vendor
{ status: "active", limit: 256 }
→ { data: { vendors: [{ vendor_code: "NIKE", caption: "Nike Inc." }] } }
Step 2: User taps a PO → load detail + receipt history (parallel)
POST /pcm/po/get POST /pcm/receipt/list
{ po_id: "po-guid" } { po_id: "po-guid", limit: 50 }
→ PO lines with ordered qtys → Prior receipts with per-line breakdown
Step 3: User enters quantities and submits (two sequential calls)
POST /pcm/receipt/record
{ receipt: {
po_id, request_context: { session_guid, orgcode, actor, context_source, session_fingerprint, logical_guid },
lines: [
{ po_line_id, variant_id, qty: { qty: 95, uom: "EACH" }, rejected_qty: 3, rejection_reason: "RECEIVING.DAMAGED" },
{ po_line_id, variant_id, qty: { qty: 50, uom: "EACH" } }
],
policy_refs: { receipt_policy_version: "default" }
},
reason: "PO receipt via G3N/Inventory",
source_refs: [{ kind: "po", id: "po-guid" }],
idempotency_key: "pcm-receipt-po-guid-{uuid}"
}
→ Response includes { data: { receipt: { receipt_id, status: "received", ... }, po_status: "received" | "partially_received" } }
→ The PO status is auto-updated server-side. Use po_status from the response to update the UI.
Then immediately:
POST /ics/receive/record
{ receipt: {
request_context: { ... },
lines: [{ variant_id, qty: { qty: 95, uom: "EACH" } }, ...],
policy_versions: { receiving_policy_version: "default" }
},
reason: "PO receipt via G3N/Inventory",
source_refs: [{ kind: "po", id: "po-guid" }],
idempotency_key: "ics-receive-po-guid-{uuid}"
}Screen 4: Putaway — "Shelve received goods"
What it is: After goods are received, they need to go to a storage location. This records where each item was put.
Who uses it: Warehouse staff pushing carts of received goods to shelves, bins, and racks.
What it does:
- Scan the item → shows product + current stock
- Select zone (dropdown, e.g., "Main Floor", "Back Room") and bin (e.g., "Aisle 3 Shelf B")
- Falls back to text input if zone/bin lists unavailable
- Confirmation preview: "Putting away 5× SHIRT-BLU-M → MAIN FLOOR / AISLE-3-SHELF-B"
- Submit → records putaway, refreshes stock
Merchant value: "When the picker needs to find that Medium Blue Shirt, they look up its bin location instead of searching the whole stockroom. Putaway is how items get their 'address' in the warehouse."
Real-world use: After every receiving session. 50-200 putaway records per receiving event.
API flow:
Step 1: Load zones (page mount)
POST /ics/zone/list
{ limit: 50 }
→ { data: { zones: [
{ zone_code: "MAIN", caption: "Main Floor", bins: [{ bin_code: "A3-B", caption: "Aisle 3 Shelf B" }] },
{ zone_code: "BACK", caption: "Back Room", bins: [...] }
] } }
Step 2: Scan product (same barcode resolve + stock get as Scan)
Step 3: User selects zone/bin, enters qty, submits
POST /ics/putaway/record
{ putaway: { variant_id, qty: { qty: 5, uom: "EACH" }, to_zone_code: "MAIN", to_bin_code: "A3-B" },
reason: "Putaway via G3N/Inventory",
source_refs: [{ kind: "webapp", id: "g3n-inventory" }],
idempotency_key: "putaway-{variant_id}-{uuid}"
}
Step 4: Post-putaway stock refresh
POST /ics/stock/get { variant_id }
→ updated stock position (confirms putaway recorded)Screen 5: Transfers — "Move stock between stores"
What it is: Moving stock between stores/warehouses. Store A has too many, Store B needs more.
Who uses it: District managers, warehouse managers, inventory planners.
What it does:
- Inbound tab: Transfers coming TO your facility. Approve or receive shipments.
- Outbound tab: Transfers you sent. Track status.
- + New tab: Create a new transfer request (destination, product, qty).
- Status timeline: requested → approved → shipped → received
- Facility names shown as human-readable captions (not GUIDs)
Merchant value: "Your Downtown store is sold out of the hot new hoodie but your Mall store has 30. Create a transfer, approve, ship, receive. Stock balances update at both locations automatically."
Real-world use: Multi-store merchants use this daily. Critical for seasonal rebalancing.
API flow:
Step 1: Load transfers (page load / tab switch)
POST /ics/transfer/request/list
{ status: "approved", limit: 50 } ← inbound tab
POST /ics/transfer/request/list
{ limit: 50 } ← outbound tab
Step 2: User taps a transfer → load detail + shipments (parallel)
POST /ics/transfer/request/get POST /ics/transfer/shipment/list
{ transfer_id } { transfer_id, limit: 50 }
Step 3a: Approve (outbound, status=submitted)
POST /ics/transfer/request/approve
{ transfer_id, expected_revision, reason: "Approved via G3N/Inventory", source_refs: [...] }
Step 3b: Receive shipment (inbound, status=shipped)
POST /ics/transfer/shipment/receive
{ shipment_id, expected_revision, reason: "Received via G3N/Inventory",
source_refs: [...], idempotency_key: "transfer-receive-{shipment_id}-{uuid}" }
Step 3c: Create new transfer
POST /ics/transfer/request/create
{ transfer: {
request_context: { ... },
source_logical_guid: "your-facility",
dest_logical_guid: "other-facility",
lines: [{ variant_id, qty: 10 }],
policy_refs: { transfer_policy_version: "default" }
},
reason: "Transfer request via G3N/Inventory", source_refs: [...]
}Screen 6: Adjustments — "Fix discrepancies"
What it is: Fixing inventory when the system doesn't match reality. Two modes: change quantity or change condition.
Who uses it: Warehouse managers, inventory controllers, loss prevention staff.
What it does:
- Qty Adjust mode: Add or subtract stock with reason code (Cycle Count, Damage, Shrinkage, Found Stock, etc.)
- Transition mode: Move stock between buckets (Available → Quarantine → Damaged) without changing total
- Large-delta warning at ±100 units
- Activity history shows recent ledger entries with actor display names, reason badges, zone/bin paths
Merchant value: "Physical count says 47 shirts, system says 52. Adjust -5 with reason 'Cycle Count'. Or: a customer returns a shirt with a stain. Transition 1 unit from Available to Damaged."
Real-world use: During cycle counts, after discovering theft/shrinkage, when items are damaged. 10-30 adjustments per day per store.
API flow:
Step 1: Scan product (barcode resolve + stock get + ledger load)
POST /ics/stock/get { variant_id }
POST /ics/stock/card/list { variant_id, limit: 15 }
Actor names batch-resolved via: POST /ofm/member/names { org_guid } → returns all member display_names in one call (cached per session)
Step 2a: Qty Adjust mode
POST /ics/adjustment/create
{ adjustment: {
variant_id, delta_qty: { qty: -5, uom: "EACH" },
reason_code: "INVENTORY.SHRINK",
policy_versions: { adjustment_policy_version: "default" }
},
reason: "Cycle count variance",
source_refs: [{ kind: "webapp", id: "g3n-inventory" }],
idempotency_key: "adjustment-{variant_id}-{uuid}"
}
Step 2b: Transition mode
POST /ics/stock/transition
{ transition: {
variant_id, qty: { qty: 2, uom: "EACH" },
from_bucket: "available", to_bucket: "damaged",
reason_code: "INVENTORY.DAMAGE",
policy_versions: { transition_policy_version: "default" }
},
reason: "Stained items found", source_refs: [...],
idempotency_key: "transition-{variant_id}-{uuid}"
}
Step 3: Post-action refresh (stock get + stock card list)Screen 7: RTV (Return to Vendor) — "Send defective goods back"
What it is: Sending defective merchandise back to the vendor for credit or replacement.
Who uses it: Receiving clerks, buyers, warehouse managers.
What it does:
- Scan defective product
- Select vendor from dropdown (populated from PVM vendor list, falls back to text input)
- Enter qty + reason (Damaged, Defective, Wrong Item, Quality Issue, Expired, etc.)
- Confirmation preview in red: "Return 3× SHIRT-BLU-M → Nike Inc. (Damaged)"
- Submit → creates PCM RTV record
Merchant value: "You received 100 jackets from North Face but 12 have broken zippers. Create an RTV for 12 units, reason 'Defective'. This feeds into the vendor performance scorecard — North Face's quality score drops."
Real-world use: During or after receiving. 1-5 RTVs per week per store.
API flow:
Step 1: Load vendors (page mount)
GET /pvm/vendor
{ status: "active", limit: 256 }
→ { data: { vendors: [{ vendor_code: "NIKE", caption: "Nike Inc." }, ...] } }
→ Populates vendor dropdown. Falls back to text input if unavailable.
Step 2: Scan product (barcode resolve)
Step 3: User selects vendor, enters qty/reason, submits
POST /pcm/rtv/create
{ rtv: {
vendor_ref: { vendor_code: "NIKE" },
request_context: { session_guid, orgcode, actor, context_source, session_fingerprint, logical_guid },
lines: [{ variant_id, qty: 3, reason_code: "RECEIVING.DAMAGED", reason: "Broken zippers" }]
},
reason: "Broken zippers on 3 units",
source_refs: [{ kind: "webapp", id: "g3n-inventory" }],
idempotency_key: "rtv-{variant_id}-{uuid}"
}Screen 8: Comments — "Flag for review"
What it is: Sticky notes on inventory items. Staff flags issues and communicates across shifts.
Who uses it: Everyone — cashiers noticing something wrong, warehouse staff flagging discrepancies, managers leaving instructions.
What it does:
- Scan product → see existing comments first (history-first pattern)
- Open count badge — how many unresolved flags (e.g., "3 open")
- Each comment shows: text, author display name (resolved from OFM), relative time ("2h ago"), tag badges, status
- 7 tags: Discrepancy, Damage, Shelf Life, Location Error, Quality Issue, Recount Needed, Other
- Post new comment with optional tag
Merchant value: "Morning shift notices the system says 20 widgets but the shelf has only 15. They flag it: 'Discrepancy — shelf count 15, system 20. Please recount.' Afternoon shift sees the flag, does a recount, adjusts stock, resolves it."
Real-world use: The informal communication channel for inventory issues. 5-15 comments per day per store. Critical for multi-shift operations.
API flow:
Step 1: Scan product (barcode resolve)
Step 2: Load existing comments
POST /ics/comment/list
{ target_type: "stock", target_id: "{variant_id}", limit: 30 }
→ { data: { comments: [
{ comment_id, body, author_guid, created_at, hashtags: ["discrepancy"], status: "current" },
...
] } }
Step 3: Resolve author names (single bulk call, cached per session)
POST /ofm/member/names { org_guid: "{org_guid}" }
→ { data: { names: [{ user_guid: "...", display_name: "Jane Smith" }, ...] } }
→ One call returns all org member display names. Cached in nameResolver.
Step 4: Post new comment
POST /ics/comment
{ target_type: "stock", target_id: "{variant_id}",
body: "Found 5 units behind the shelf. Adjusting stock.",
hashtags: ["recount"], caption: "Found 5 units behind the shelf..." }
Step 5: Reload comments (same as Step 2 — shows new comment in list)Screen 9: Products — "Product encyclopedia"
What it is: Everything the system knows about a product, in one place.
Who uses it: Managers investigating products, buyers checking vendor/brand data, staff verifying barcodes.
What it does:
- Details tab: Variant code, style code, color, size, status, kit type. All registered barcodes with scheme and primary badge.
- Stock tab: Same 9-bucket stock display as Stock Lookup.
- Activity tab: Same ledger as Stock Lookup (lazy-loaded on tab click).
Merchant value: "A new barcode label won't scan. The manager opens Product Lookup, searches by the old barcode, checks the Barcodes section — the new barcode isn't registered. Now they know to contact the buyer to add it."
Real-world use: Reference/investigation tool. 5-10 lookups per day.
API flow:
Step 1: Scan product (barcode resolve)
Step 2: Load variant + barcodes + stock (parallel)
GET /pvm/variant/get?orgcode={orgcode}&variant_id={variant_id}
→ { data: { variant_code, style_code, color, size, status, kit_type } }
GET /pvm/barcode/list?orgcode={orgcode}&variant_id={variant_id}
→ { data: { barcodes: [
{ value: "0123456789012", scheme: "ean-13", is_primary: true },
{ value: "7890123456789", scheme: "upc-a", is_primary: false }
] } }
POST /ics/stock/get { variant_id }
→ stock position
Step 3: User clicks Activity tab → lazy-load ledger
POST /ics/stock/card/list { variant_id, limit: 20 }
→ same ledger format as Stock LookupWho Uses What — Role Matrix
| Role | Primary Screens | Frequency |
|---|---|---|
| Cashier / Sales staff | Scan, Comments | 50+ scans/day |
| Receiving clerk | Receiving, RTV, Putaway | Every delivery |
| Warehouse staff | Putaway, Scan, Adjustments | All day |
| Warehouse manager | All screens | All day |
| Inventory controller | Stock Lookup, Adjustments, Comments | During counts |
| Buyer / Procurement | Receiving (monitor), RTV, Products | Weekly |
| Store manager | Transfers, Adjustments, Stock Lookup | As needed |
| District manager | Transfers (multi-store rebalancing) | Weekly |
The Flow of Goods
Vendor ships → [Receiving] → [Putaway] → shelf
↓ (defective)
[RTV] → back to vendor
shelf → customer buys (POS) → stock decremented
shelf → damaged found → [Adjustments] → quarantine/damaged
shelf → wrong count → [Adjustments] → qty correction
shelf → needs to move → [Transfers] → another store
Any time → [Scan] to check what something is
Any time → [Stock Lookup] to see full history
Any time → [Comments] to flag an issue
Any time → [Products] to see product master dataAPI Summary per Screen
| Screen | Read calls | Write calls | Services hit |
|---|---|---|---|
| Scan | 3 | 0 | PVM, ICS |
| Stock Lookup | 4+ | 0 | PVM, ICS |
| Receiving | 4 | 2 | PCM, ICS, PVM |
| Putaway | 4 | 1 | PVM, ICS |
| Transfers | 3-4 | 1 | ICS |
| Adjustments | 4 | 1 | PVM, ICS, OFM |
| RTV | 3 | 1 | PVM, PCM |
| Comments | 3+ | 1 | PVM, ICS, OFM |
| Products | 4+ | 0 | PVM, ICS |
Every screen uses the shared scanner hook (PVM barcode/resolve → variant/get) as its entry point. Every mutation includes idempotency_key, reason, and source_refs for auditability. Every request carries session headers (x-session-guid, x-orgcode, x-logical-guid).
12. SDK Recommendations
Build a thin Kotlin SDK (g3n-inventory-sdk) that wraps these patterns:
12.1 Core Responsibilities
- Header injection: Automatically add
x-session-guid,x-orgcode,x-logical-guidto every request from stored context. - Idempotency key management: Generate and attach keys for mutations; store pending keys until confirmed.
- Retry with backoff: On retryable errors (429, 5xx, network), retry up to 3 times with exponential backoff + jitter.
- Session expiry handling: On 401, clear context and emit a "session expired" event for the UI to handle.
- Barcode cache: LRU cache (500 entries, 5-min TTL) for barcode→variant mappings.
- Typed models: Data classes for each request/response per endpoint.
12.2 Context Object
kotlin
data class G3nContext(
val sessionGuid: String,
val orgcode: String,
val logicalGuid: String,
val cccode: String? = null,
val capabilities: Capabilities
)Set once after login + org/facility selection. Pass to every SDK call.
12.3 Request Timeouts
| Operation | Timeout |
|---|---|
| Barcode resolve (PVM) | 3s |
| Stock lookup (ICS) | 5s |
| Receive/putaway/adjustment (ICS) | 10s |
| PO receipt (PCM) | 10s |
| Transfer operations (ICS) | 10s |
| Session create (USM) | 10s |
13. Security Considerations
- Never store passcode on device. Store only
session_guidin Android Keystore. - Never log
session_guid,x-api-key, or passcode values. - Always use HTTPS. The API Gateway enforces TLS 1.2+.
- Anti-enumeration: Some endpoints return 404 even when the record exists (if the caller isn't associated with the org). Do not assume 404 means "does not exist."
- Cost data: Only display cost fields if
cost_view,ics_cost_admin, orfinance_auditis in the user's roles. The server strips cost fields from responses for unauthorized users, but the app should also hide cost UI elements.
14. Development Environment
The dev-seed scripts create three pre-loaded orgs for testing against the live api.g3nretailstack.com stack.
| Org | Orgcode | Vertical | Styles | Variants | Facilities |
|---|---|---|---|---|---|
| Aurora Fashion | AURORA3 | Fashion/apparel | 75 | 798 | 5 (1 DC + 4 stores) |
| NexGen Sporting | NEXGEN3 | Sporting goods | 59 | 496 | 4 (1 DC + 3 stores) |
| Shemew Pet Supply | SHEMEW3 | Pet supply | 37 | 195 | 3 (1 DC + 2 stores) |
Test users: Three pre-created users with different role profiles. Credentials are provisioned by the dev-seed script (scripts/dev-seed/run.ts) — see the script source for current values.
| Role | Purpose | |
|---|---|---|
owner@g3ntest.dev | Owner (all permissions, sees costs) | Full access testing, cost visibility |
warehouse@g3ntest.dev | Warehouse operator | Primary app persona: receiving, adjustments, transfers |
viewer@g3ntest.dev | Read-only (ics_view, pvm_view, pcm_view, ofm_view) | Test role gating: no mutations, no cost visibility |
Pre-seeded enrichment data (per org):
- 5 stock comments with various hashtags
- 5 adjustments (shrink, damage, adjust, FIFO, recall)
- 2 stock transitions (available→damaged, available→quarantine)
- 4 transfers in different states: pending, approved, in_transit, completed
- 4-5 open POs (status: issued) for receiving practice
- Zones and bins at all facilities (DC: 6 zones, stores: 4 zones, 3 bins each)
Sample barcodes for scanning tests: 3000000000014 (AURORA3), 3000000007990 (NEXGEN3).
Running the dev-seed:
bash
npx tsx scripts/dev-seed/run.ts # Creates orgs, facilities, products, stock, zones, bins, transfers
npx tsx scripts/dev-seed/enrich.ts # Adds comments, adjustments, transitions, multi-state transfers
npx tsx scripts/dev-seed/simulate.ts # Runs full workflow simulation (PO → receive → putaway → transfer)15. OpenAPI Specs
Full machine-readable contracts for code generation. Each service provides a downloadable YAML spec and an interactive API explorer:
| Service | OpenAPI YAML | Interactive Explorer |
|---|---|---|
| ICS | openapi.yaml | Explorer |
| PCM | openapi.yaml | Explorer |
| PVM | openapi.yaml | Explorer |
| OFM | openapi.yaml | Explorer |
| USM | openapi.yaml | Explorer |
| UAS | openapi.yaml | Explorer |
| SCM | openapi.yaml | Explorer |
Use the YAML specs with OpenAPI code generators (e.g., openapi-generator for Kotlin) to produce typed request/response models. The interactive explorers (powered by Scalar) let you browse endpoints, schemas, and try requests directly.
16. Common References
| Topic | URL |
|---|---|
| Headers & identity | https://doc.g3nretailstack.com/common/headers-identity.html |
| Error tags | https://doc.g3nretailstack.com/common/error-tags.html |
| Idempotency & retries | https://doc.g3nretailstack.com/common/idempotency-retry.html |
| Rate limits & timeouts | https://doc.g3nretailstack.com/common/rate-limit-timeouts.html |
| Performance SLOs | https://doc.g3nretailstack.com/common/performance-slos.html |
| Versioning & compatibility | https://doc.g3nretailstack.com/common/versioning-compat.html |
| Role matrix | https://doc.g3nretailstack.com/common/role-matrix.html |
| SDK integration checklist | https://doc.g3nretailstack.com/common/sdk-integration-checklist.html |