Inventory Synchronization Guide
Learn how to implement robust inventory synchronization across multiple channels, warehouses, and systems using the Unified Commerce Platform's inventory management capabilities.
Prerequisites
- Unified Commerce API access
- Understanding of inventory management concepts
- Webhook endpoint capability (for real-time sync)
- Basic knowledge of GraphQL subscriptions
Table of Contents
- Architecture Overview
- Initial Setup
- Inventory Tracking
- Real-Time Synchronization
- Multi-Warehouse Management
- Channel Allocation
- Reservation System
- Batch Operations
- Error Recovery
- Monitoring & Alerts
- Best Practices
- Troubleshooting
Architecture Overview
Inventory Flow Diagram
graph TD
A[Product Catalog] --> B[Inventory Service]
B --> C[Warehouse 1]
B --> D[Warehouse 2]
B --> E[Warehouse 3]
F[Orders] --> G[Reservation System]
G --> B
H[POS Systems] --> B
I[E-commerce] --> B
J[Marketplaces] --> B
B --> K[Real-time Sync]
K --> L[Webhooks]
K --> M[WebSockets]
K --> N[Polling]
Key Components
- Inventory Service - Central source of truth
- Warehouse Management - Physical location tracking
- Reservation System - Temporary holds for carts/orders
- Sync Engine - Real-time updates across channels
- Audit Trail - Complete history of changes
Initial Setup
Step 1: Configure Warehouses
mutation CreateWarehouse($input: CreateWarehouseInput!) {
createWarehouse(input: $input) {
warehouse {
id
name
code
address {
street
city
state
postalCode
country
}
timezone
isActive
priority
capabilities
}
errors {
field
message
}
}
}
# Variables
{
"input": {
"name": "Main Distribution Center",
"code": "DC-001",
"address": {
"street": "123 Warehouse Way",
"city": "Dallas",
"state": "TX",
"postalCode": "75201",
"country": "US"
},
"timezone": "America/Chicago",
"isActive": true,
"priority": 1,
"capabilities": ["SHIPPING", "RECEIVING", "STORAGE"]
}
}
Step 2: Initialize Product Inventory
import { sdk } from '@/lib/api-client';
interface InitializeInventoryInput {
productId: string;
variants?: string[];
warehouses: {
warehouseId: string;
quantity: number;
reservedQuantity?: number;
safetyStock?: number;
reorderPoint?: number;
reorderQuantity?: number;
}[];
}
export async function initializeProductInventory(
input: InitializeInventoryInput
) {
try {
// Create inventory records for each warehouse
const promises = input.warehouses.map(warehouse =>
sdk.createInventoryLevel({
input: {
productId: input.productId,
warehouseId: warehouse.warehouseId,
quantity: warehouse.quantity,
reservedQuantity: warehouse.reservedQuantity || 0,
safetyStock: warehouse.safetyStock || 10,
reorderPoint: warehouse.reorderPoint || 20,
reorderQuantity: warehouse.reorderQuantity || 100,
}
})
);
const results = await Promise.all(promises);
// Check for errors
const errors = results.flatMap(r =>
r.data?.createInventoryLevel?.errors || []
);
if (errors.length > 0) {
throw new Error(`Inventory initialization failed: ${errors[0].message}`);
}
return {
success: true,
inventoryLevels: results.map(r => r.data?.createInventoryLevel?.inventoryLevel),
};
} catch (error) {
console.error('Failed to initialize inventory:', error);
throw error;
}
}
Step 3: Set Up Sync Configuration
// config/inventory-sync.ts
export const INVENTORY_SYNC_CONFIG = {
// Sync intervals
realTimeChannels: ['POS', 'WEBSITE', 'MOBILE_APP'],
batchChannels: ['AMAZON', 'EBAY', 'WALMART'],
// Sync frequency
batchSyncInterval: 15 * 60 * 1000, // 15 minutes
// Buffer settings
safetyBuffer: 0.05, // 5% safety buffer
oversellProtection: true,
// Channel priorities (higher = more priority)
channelPriorities: {
WEBSITE: 100,
POS: 90,
MOBILE_APP: 80,
AMAZON: 70,
EBAY: 60,
WALMART: 50,
},
// Retry configuration
maxRetries: 3,
retryDelay: 1000,
retryBackoff: 2,
// Alert thresholds
alerts: {
lowStock: 10,
outOfStock: 0,
highVelocity: 100, // Units per hour
},
};
Inventory Tracking
Step 1: Track Inventory Levels
// services/inventory-tracker.ts
import { sdk } from '@/lib/api-client';
export class InventoryTracker {
private cache: Map<string, InventoryLevel> = new Map();
private subscribers: Set<(update: InventoryUpdate) => void> = new Set();
async getInventoryLevel(
productId: string,
warehouseId?: string
): Promise<InventoryLevel> {
const cacheKey = `${productId}_${warehouseId || 'all'}`;
// Check cache first
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey)!;
if (Date.now() - cached.fetchedAt < 60000) { // 1 minute cache
return cached;
}
}
// Fetch from API
const { data } = await sdk.getInventoryLevel({
productId,
warehouseId,
});
if (!data?.inventoryLevel) {
throw new Error('Inventory level not found');
}
const inventoryLevel = {
...data.inventoryLevel,
fetchedAt: Date.now(),
};
// Update cache
this.cache.set(cacheKey, inventoryLevel);
return inventoryLevel;
}
async updateInventoryLevel(
productId: string,
warehouseId: string,
adjustment: number,
reason: string
): Promise<InventoryLevel> {
const { data } = await sdk.adjustInventory({
input: {
productId,
warehouseId,
adjustment,
reason,
timestamp: new Date().toISOString(),
}
});
if (data?.adjustInventory?.errors?.length) {
throw new Error(data.adjustInventory.errors[0].message);
}
const updated = data.adjustInventory.inventoryLevel;
// Notify subscribers
this.notifySubscribers({
type: 'ADJUSTMENT',
productId,
warehouseId,
oldQuantity: updated.quantity - adjustment,
newQuantity: updated.quantity,
adjustment,
reason,
timestamp: new Date().toISOString(),
});
// Clear cache
this.clearCache(productId, warehouseId);
return updated;
}
async transferInventory(
productId: string,
fromWarehouseId: string,
toWarehouseId: string,
quantity: number
): Promise<TransferResult> {
const { data } = await sdk.transferInventory({
input: {
productId,
fromWarehouseId,
toWarehouseId,
quantity,
reason: 'WAREHOUSE_TRANSFER',
}
});
if (data?.transferInventory?.errors?.length) {
throw new Error(data.transferInventory.errors[0].message);
}
// Notify subscribers
this.notifySubscribers({
type: 'TRANSFER',
productId,
fromWarehouseId,
toWarehouseId,
quantity,
timestamp: new Date().toISOString(),
});
// Clear cache for both warehouses
this.clearCache(productId, fromWarehouseId);
this.clearCache(productId, toWarehouseId);
return data.transferInventory;
}
subscribe(callback: (update: InventoryUpdate) => void): () => void {
this.subscribers.add(callback);
return () => this.subscribers.delete(callback);
}
private notifySubscribers(update: InventoryUpdate) {
this.subscribers.forEach(callback => callback(update));
}
private clearCache(productId?: string, warehouseId?: string) {
if (productId) {
const pattern = `${productId}_${warehouseId || ''}`;
for (const key of this.cache.keys()) {
if (key.startsWith(pattern)) {
this.cache.delete(key);
}
}
} else {
this.cache.clear();
}
}
}
export const inventoryTracker = new InventoryTracker();
Step 2: Implement Audit Trail
// services/inventory-audit.ts
export class InventoryAudit {
async logInventoryChange(change: {
productId: string;
warehouseId: string;
type: 'ADJUSTMENT' | 'SALE' | 'RETURN' | 'TRANSFER' | 'DAMAGE';
quantity: number;
previousQuantity: number;
newQuantity: number;
reason: string;
userId?: string;
orderId?: string;
metadata?: Record<string, any>;
}) {
const { data } = await sdk.createInventoryAuditLog({
input: {
...change,
timestamp: new Date().toISOString(),
source: 'MANUAL', // or 'SYSTEM', 'API', 'IMPORT'
}
});
return data?.createInventoryAuditLog?.auditLog;
}
async getAuditHistory(
productId: string,
options?: {
warehouseId?: string;
startDate?: Date;
endDate?: Date;
limit?: number;
}
) {
const { data } = await sdk.getInventoryAuditLogs({
productId,
...options,
});
return data?.inventoryAuditLogs;
}
async reconcileInventory(warehouseId: string) {
// Get all products in warehouse
const { data: products } = await sdk.getWarehouseProducts({
warehouseId,
});
const discrepancies = [];
for (const product of products?.warehouseProducts || []) {
// Get system quantity
const systemQuantity = product.inventoryLevel.quantity;
// Get calculated quantity from audit logs
const auditLogs = await this.getAuditHistory(product.id, {
warehouseId,
});
const calculatedQuantity = this.calculateQuantityFromAudit(auditLogs);
if (systemQuantity !== calculatedQuantity) {
discrepancies.push({
productId: product.id,
productName: product.name,
systemQuantity,
calculatedQuantity,
difference: systemQuantity - calculatedQuantity,
});
}
}
return discrepancies;
}
private calculateQuantityFromAudit(logs: AuditLog[]): number {
return logs.reduce((quantity, log) => {
switch (log.type) {
case 'ADJUSTMENT':
case 'RETURN':
return quantity + log.quantity;
case 'SALE':
case 'DAMAGE':
return quantity - log.quantity;
case 'TRANSFER':
// Handled separately per warehouse
return quantity + (log.toWarehouseId ? log.quantity : -log.quantity);
default:
return quantity;
}
}, 0);
}
}
Real-Time Synchronization
Step 1: WebSocket Connection
// services/inventory-websocket.ts
import { io, Socket } from 'socket.io-client';
export class InventoryWebSocket {
private socket: Socket | null = null;
private listeners: Map<string, Set<Function>> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
connect(token: string) {
this.socket = io(process.env.NEXT_PUBLIC_WS_URL!, {
auth: { token },
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: this.maxReconnectAttempts,
});
this.setupEventHandlers();
}
private setupEventHandlers() {
if (!this.socket) return;
this.socket.on('connect', () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
// Subscribe to inventory updates
this.socket!.emit('subscribe', {
channel: 'inventory',
events: ['update', 'transfer', 'reservation'],
});
});
this.socket.on('inventory:update', (data) => {
this.emit('inventoryUpdate', data);
});
this.socket.on('inventory:transfer', (data) => {
this.emit('inventoryTransfer', data);
});
this.socket.on('inventory:reservation', (data) => {
this.emit('inventoryReservation', data);
});
this.socket.on('disconnect', (reason) => {
console.log('WebSocket disconnected:', reason);
if (reason === 'io server disconnect') {
// Server initiated disconnect, try to reconnect
this.reconnect();
}
});
this.socket.on('error', (error) => {
console.error('WebSocket error:', error);
});
}
private reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
this.emit('maxReconnectAttemptsReached', {});
return;
}
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => {
console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);
this.socket?.connect();
}, delay);
}
on(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return () => {
this.listeners.get(event)?.delete(callback);
};
}
private emit(event: string, data: any) {
this.listeners.get(event)?.forEach(callback => callback(data));
}
subscribeToProduct(productId: string) {
this.socket?.emit('subscribe:product', { productId });
}
unsubscribeFromProduct(productId: string) {
this.socket?.emit('unsubscribe:product', { productId });
}
subscribeToWarehouse(warehouseId: string) {
this.socket?.emit('subscribe:warehouse', { warehouseId });
}
disconnect() {
this.socket?.disconnect();
this.socket = null;
}
}
export const inventoryWebSocket = new InventoryWebSocket();
Step 2: React Hook for Real-Time Updates
// hooks/use-inventory-updates.ts
import { useEffect, useState, useCallback } from 'react';
import { inventoryWebSocket } from '@/services/inventory-websocket';
interface UseInventoryUpdatesOptions {
productIds?: string[];
warehouseIds?: string[];
onUpdate?: (update: InventoryUpdate) => void;
}
export function useInventoryUpdates({
productIds = [],
warehouseIds = [],
onUpdate,
}: UseInventoryUpdatesOptions) {
const [updates, setUpdates] = useState<InventoryUpdate[]>([]);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
// Connect to WebSocket
inventoryWebSocket.connect(localStorage.getItem('auth_token')!);
// Subscribe to products
productIds.forEach(id => inventoryWebSocket.subscribeToProduct(id));
// Subscribe to warehouses
warehouseIds.forEach(id => inventoryWebSocket.subscribeToWarehouse(id));
// Listen for updates
const unsubscribe = inventoryWebSocket.on('inventoryUpdate', (update) => {
setUpdates(prev => [...prev, update]);
onUpdate?.(update);
});
// Connection status
const unsubConnect = inventoryWebSocket.on('connect', () => {
setIsConnected(true);
});
const unsubDisconnect = inventoryWebSocket.on('disconnect', () => {
setIsConnected(false);
});
return () => {
unsubscribe();
unsubConnect();
unsubDisconnect();
// Unsubscribe from products/warehouses
productIds.forEach(id => inventoryWebSocket.unsubscribeFromProduct(id));
warehouseIds.forEach(id => inventoryWebSocket.unsubscribeFromWarehouse(id));
};
}, [productIds.join(','), warehouseIds.join(',')]);
const clearUpdates = useCallback(() => {
setUpdates([]);
}, []);
return {
updates,
isConnected,
clearUpdates,
};
}
// Usage in component
function InventoryDashboard({ productId }: { productId: string }) {
const { updates, isConnected } = useInventoryUpdates({
productIds: [productId],
onUpdate: (update) => {
console.log('Inventory updated:', update);
// Trigger UI update or notification
},
});
return (
<div>
<div className={`status ${isConnected ? 'connected' : 'disconnected'}`}>
{isConnected ? '🟢 Live' : '🔴 Offline'}
</div>
{updates.map((update, index) => (
<div key={index} className="update-notification">
Product {update.productId}: {update.oldQuantity} → {update.newQuantity}
</div>
))}
</div>
);
}
Step 3: Polling Fallback
// services/inventory-polling.ts
export class InventoryPolling {
private intervals: Map<string, NodeJS.Timeout> = new Map();
private cache: Map<string, any> = new Map();
startPolling(
key: string,
fetcher: () => Promise<any>,
callback: (data: any) => void,
interval: number = 30000 // 30 seconds default
) {
// Clear existing interval
this.stopPolling(key);
// Initial fetch
this.fetchAndCompare(key, fetcher, callback);
// Set up interval
const intervalId = setInterval(() => {
this.fetchAndCompare(key, fetcher, callback);
}, interval);
this.intervals.set(key, intervalId);
}
stopPolling(key: string) {
const intervalId = this.intervals.get(key);
if (intervalId) {
clearInterval(intervalId);
this.intervals.delete(key);
}
}
stopAllPolling() {
this.intervals.forEach(intervalId => clearInterval(intervalId));
this.intervals.clear();
}
private async fetchAndCompare(
key: string,
fetcher: () => Promise<any>,
callback: (data: any) => void
) {
try {
const data = await fetcher();
const cached = this.cache.get(key);
// Compare with cache
if (JSON.stringify(data) !== JSON.stringify(cached)) {
this.cache.set(key, data);
callback(data);
}
} catch (error) {
console.error(`Polling error for ${key}:`, error);
}
}
}
// Usage
const poller = new InventoryPolling();
// Start polling for product inventory
poller.startPolling(
`product_${productId}`,
() => sdk.getInventoryLevel({ productId }),
(data) => {
console.log('Inventory updated:', data);
updateUI(data);
},
15000 // Poll every 15 seconds
);
Multi-Warehouse Management
Step 1: Warehouse Selection Logic
// services/warehouse-selector.ts
export class WarehouseSelector {
async selectOptimalWarehouse(
productId: string,
quantity: number,
shippingAddress: Address
): Promise<WarehouseSelection> {
// Get all warehouses with inventory
const { data } = await sdk.getProductWarehouses({
productId,
minQuantity: quantity,
});
const warehouses = data?.productWarehouses || [];
if (warehouses.length === 0) {
throw new Error('Insufficient inventory across all warehouses');
}
// Calculate scores for each warehouse
const scoredWarehouses = await Promise.all(
warehouses.map(async (warehouse) => {
const score = await this.calculateWarehouseScore(
warehouse,
shippingAddress,
quantity
);
return { warehouse, score };
})
);
// Sort by score (higher is better)
scoredWarehouses.sort((a, b) => b.score - a.score);
const selected = scoredWarehouses[0].warehouse;
return {
warehouseId: selected.id,
warehouseName: selected.name,
availableQuantity: selected.inventoryLevel.available,
shippingCost: await this.calculateShippingCost(
selected,
shippingAddress
),
estimatedDelivery: await this.estimateDelivery(
selected,
shippingAddress
),
};
}
private async calculateWarehouseScore(
warehouse: Warehouse,
shippingAddress: Address,
quantity: number
): Promise<number> {
let score = 100;
// Distance factor (0-30 points)
const distance = await this.calculateDistance(
warehouse.address,
shippingAddress
);
score -= Math.min(30, distance / 100); // Lose 1 point per 100km
// Inventory availability (0-30 points)
const availableRatio = warehouse.inventoryLevel.available / quantity;
score += Math.min(30, availableRatio * 10);
// Warehouse priority (0-20 points)
score += warehouse.priority * 2;
// Processing capacity (0-20 points)
const utilizationRate = await this.getWarehouseUtilization(warehouse.id);
score += (1 - utilizationRate) * 20;
return score;
}
private async calculateDistance(
from: Address,
to: Address
): Promise<number> {
// Simple haversine distance calculation
// In production, use a proper geocoding service
const R = 6371; // Earth radius in km
const lat1 = from.latitude || 0;
const lat2 = to.latitude || 0;
const lon1 = from.longitude || 0;
const lon2 = to.longitude || 0;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
async splitOrderAcrossWarehouses(
items: OrderItem[]
): Promise<WarehouseAllocation[]> {
const allocations: WarehouseAllocation[] = [];
for (const item of items) {
// Get warehouses with inventory
const { data } = await sdk.getProductWarehouses({
productId: item.productId,
});
const warehouses = data?.productWarehouses || [];
let remainingQuantity = item.quantity;
// Allocate from warehouses in priority order
for (const warehouse of warehouses) {
if (remainingQuantity <= 0) break;
const available = warehouse.inventoryLevel.available;
const allocateQuantity = Math.min(available, remainingQuantity);
if (allocateQuantity > 0) {
allocations.push({
warehouseId: warehouse.id,
productId: item.productId,
quantity: allocateQuantity,
});
remainingQuantity -= allocateQuantity;
}
}
if (remainingQuantity > 0) {
throw new Error(
`Insufficient inventory for product ${item.productId}`
);
}
}
return this.optimizeAllocations(allocations);
}
private optimizeAllocations(
allocations: WarehouseAllocation[]
): WarehouseAllocation[] {
// Group by warehouse to minimize shipments
const grouped = allocations.reduce((acc, allocation) => {
const key = allocation.warehouseId;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(allocation);
return acc;
}, {} as Record<string, WarehouseAllocation[]>);
// Flatten back to array
return Object.values(grouped).flat();
}
}
Step 2: Cross-Warehouse Transfers
// services/warehouse-transfer.ts
export class WarehouseTransferService {
async createTransferOrder(
fromWarehouseId: string,
toWarehouseId: string,
items: TransferItem[]
): Promise<TransferOrder> {
const { data } = await sdk.createTransferOrder({
input: {
fromWarehouseId,
toWarehouseId,
items,
expectedDelivery: this.calculateExpectedDelivery(
fromWarehouseId,
toWarehouseId
),
status: 'PENDING',
}
});
if (data?.createTransferOrder?.errors?.length) {
throw new Error(data.createTransferOrder.errors[0].message);
}
const transferOrder = data.createTransferOrder.transferOrder;
// Reserve inventory at source warehouse
await this.reserveInventoryForTransfer(
fromWarehouseId,
items,
transferOrder.id
);
return transferOrder;
}
async processTransferShipment(
transferOrderId: string,
trackingNumber: string
) {
// Update transfer status
await sdk.updateTransferOrder({
id: transferOrderId,
input: {
status: 'IN_TRANSIT',
shippedAt: new Date().toISOString(),
trackingNumber,
}
});
// Deduct from source warehouse
const transfer = await this.getTransferOrder(transferOrderId);
for (const item of transfer.items) {
await sdk.adjustInventory({
input: {
productId: item.productId,
warehouseId: transfer.fromWarehouseId,
adjustment: -item.quantity,
reason: 'TRANSFER_OUT',
referenceId: transferOrderId,
}
});
}
}
async receiveTransfer(
transferOrderId: string,
receivedItems: ReceivedItem[]
) {
const transfer = await this.getTransferOrder(transferOrderId);
// Process each received item
for (const item of receivedItems) {
// Add to destination warehouse
await sdk.adjustInventory({
input: {
productId: item.productId,
warehouseId: transfer.toWarehouseId,
adjustment: item.quantityReceived,
reason: 'TRANSFER_IN',
referenceId: transferOrderId,
}
});
// Handle discrepancies
if (item.quantityReceived !== item.quantityExpected) {
await this.handleTransferDiscrepancy(
transferOrderId,
item.productId,
item.quantityExpected,
item.quantityReceived,
item.discrepancyReason
);
}
}
// Update transfer status
await sdk.updateTransferOrder({
id: transferOrderId,
input: {
status: 'COMPLETED',
receivedAt: new Date().toISOString(),
}
});
}
private async handleTransferDiscrepancy(
transferOrderId: string,
productId: string,
expected: number,
received: number,
reason?: string
) {
// Create discrepancy record
await sdk.createTransferDiscrepancy({
input: {
transferOrderId,
productId,
quantityExpected: expected,
quantityReceived: received,
difference: received - expected,
reason: reason || 'UNKNOWN',
status: 'PENDING_INVESTIGATION',
}
});
// Send alert
await this.sendDiscrepancyAlert(transferOrderId, productId, expected, received);
}
}
Channel Allocation
Step 1: Channel-Specific Inventory
// services/channel-allocation.ts
export class ChannelAllocationService {
async allocateInventoryToChannels(
productId: string,
totalQuantity: number,
strategy: AllocationStrategy = 'PROPORTIONAL'
) {
const allocations = await this.calculateAllocations(
productId,
totalQuantity,
strategy
);
// Apply allocations
for (const allocation of allocations) {
await sdk.setChannelInventory({
input: {
productId,
channelId: allocation.channelId,
allocatedQuantity: allocation.quantity,
bufferQuantity: allocation.buffer,
}
});
}
return allocations;
}
private async calculateAllocations(
productId: string,
totalQuantity: number,
strategy: AllocationStrategy
): Promise<ChannelAllocation[]> {
switch (strategy) {
case 'PROPORTIONAL':
return this.proportionalAllocation(productId, totalQuantity);
case 'PRIORITY':
return this.priorityAllocation(productId, totalQuantity);
case 'VELOCITY':
return this.velocityBasedAllocation(productId, totalQuantity);
case 'CUSTOM':
return this.customAllocation(productId, totalQuantity);
default:
throw new Error(`Unknown allocation strategy: ${strategy}`);
}
}
private async proportionalAllocation(
productId: string,
totalQuantity: number
): Promise<ChannelAllocation[]> {
// Get historical sales data
const { data } = await sdk.getProductSalesByChannel({
productId,
period: 'LAST_30_DAYS',
});
const sales = data?.productSalesByChannel || [];
const totalSales = sales.reduce((sum, s) => sum + s.quantity, 0);
if (totalSales === 0) {
// Equal distribution if no sales history
const channels = await this.getActiveChannels();
const quantityPerChannel = Math.floor(totalQuantity / channels.length);
return channels.map(channel => ({
channelId: channel.id,
channelName: channel.name,
quantity: quantityPerChannel,
buffer: Math.floor(quantityPerChannel * 0.1), // 10% buffer
}));
}
// Allocate based on sales proportion
return sales.map(sale => {
const proportion = sale.quantity / totalSales;
const allocated = Math.floor(totalQuantity * proportion);
return {
channelId: sale.channelId,
channelName: sale.channelName,
quantity: allocated,
buffer: Math.floor(allocated * 0.1),
};
});
}
private async velocityBasedAllocation(
productId: string,
totalQuantity: number
): Promise<ChannelAllocation[]> {
// Get sales velocity for each channel
const velocities = await this.calculateSalesVelocity(productId);
// Calculate days of supply for each channel
const allocations: ChannelAllocation[] = [];
let remaining = totalQuantity;
for (const velocity of velocities) {
const targetDaysOfSupply = 14; // 2 weeks
const needed = Math.ceil(velocity.dailyVelocity * targetDaysOfSupply);
const allocated = Math.min(needed, remaining);
allocations.push({
channelId: velocity.channelId,
channelName: velocity.channelName,
quantity: allocated,
buffer: Math.floor(allocated * 0.15), // 15% buffer for fast-moving
});
remaining -= allocated;
}
return allocations;
}
async reallocateUnsoldInventory() {
// Get channels with excess inventory
const { data } = await sdk.getChannelsWithExcessInventory({
thresholdDays: 30, // Items unsold for 30+ days
});
for (const channel of data?.channelsWithExcess || []) {
for (const product of channel.excessProducts) {
// Find channels that need this product
const needingChannels = await this.findChannelsNeedingProduct(
product.productId
);
if (needingChannels.length > 0) {
// Reallocate to channel with highest velocity
const targetChannel = needingChannels[0];
const reallocateQuantity = Math.min(
product.excessQuantity,
targetChannel.neededQuantity
);
await this.moveInventoryBetweenChannels(
product.productId,
channel.id,
targetChannel.id,
reallocateQuantity
);
}
}
}
}
}
Reservation System
Step 1: Cart Reservations
// services/reservation-service.ts
export class ReservationService {
async reserveForCart(
cartId: string,
items: CartItem[]
): Promise<ReservationResult> {
const reservations: Reservation[] = [];
const failures: ReservationFailure[] = [];
for (const item of items) {
try {
const reservation = await this.reserveItem(
item.productId,
item.variantId,
item.quantity,
'CART',
cartId
);
reservations.push(reservation);
} catch (error) {
failures.push({
productId: item.productId,
requestedQuantity: item.quantity,
availableQuantity: await this.getAvailableQuantity(item.productId),
reason: error.message,
});
}
}
// Set expiration for cart reservations (30 minutes)
if (reservations.length > 0) {
await this.setReservationExpiry(
reservations.map(r => r.id),
30 * 60 * 1000
);
}
return {
success: failures.length === 0,
reservations,
failures,
};
}
private async reserveItem(
productId: string,
variantId: string | null,
quantity: number,
type: 'CART' | 'ORDER' | 'TRANSFER',
referenceId: string
): Promise<Reservation> {
const { data } = await sdk.createReservation({
input: {
productId,
variantId,
quantity,
type,
referenceId,
expiresAt: this.calculateExpiry(type),
}
});
if (data?.createReservation?.errors?.length) {
throw new Error(data.createReservation.errors[0].message);
}
return data.createReservation.reservation;
}
async releaseReservation(reservationId: string) {
await sdk.releaseReservation({ id: reservationId });
}
async releaseExpiredReservations() {
const { data } = await sdk.getExpiredReservations({
before: new Date().toISOString(),
});
const expired = data?.expiredReservations || [];
for (const reservation of expired) {
await this.releaseReservation(reservation.id);
}
return expired.length;
}
async convertToOrderReservation(
cartId: string,
orderId: string
): Promise<void> {
// Get cart reservations
const { data } = await sdk.getReservationsByReference({
type: 'CART',
referenceId: cartId,
});
const reservations = data?.reservations || [];
// Convert each to order reservation
for (const reservation of reservations) {
await sdk.updateReservation({
id: reservation.id,
input: {
type: 'ORDER',
referenceId: orderId,
expiresAt: null, // Order reservations don't expire
}
});
}
}
private calculateExpiry(type: string): string {
const now = Date.now();
let expiryMs: number;
switch (type) {
case 'CART':
expiryMs = now + 30 * 60 * 1000; // 30 minutes
break;
case 'ORDER':
return null; // Orders don't expire
case 'TRANSFER':
expiryMs = now + 24 * 60 * 60 * 1000; // 24 hours
break;
default:
expiryMs = now + 60 * 60 * 1000; // 1 hour default
}
return new Date(expiryMs).toISOString();
}
// Clean up expired reservations periodically
startCleanupJob(intervalMs: number = 5 * 60 * 1000) {
setInterval(async () => {
try {
const released = await this.releaseExpiredReservations();
console.log(`Released ${released} expired reservations`);
} catch (error) {
console.error('Failed to release expired reservations:', error);
}
}, intervalMs);
}
}
Batch Operations
Step 1: Bulk Import
// services/inventory-import.ts
import { parse } from 'csv-parse';
import { Readable } from 'stream';
export class InventoryImportService {
async importFromCSV(
file: File,
options: ImportOptions = {}
): Promise<ImportResult> {
const records = await this.parseCSV(file);
const results: ImportResult = {
success: 0,
failed: 0,
errors: [],
};
// Process in batches
const batchSize = options.batchSize || 100;
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
await this.processBatch(batch, results);
}
return results;
}
private async parseCSV(file: File): Promise<InventoryRecord[]> {
const text = await file.text();
const records: InventoryRecord[] = [];
return new Promise((resolve, reject) => {
parse(text, {
columns: true,
skip_empty_lines: true,
})
.on('readable', function() {
let record;
while ((record = this.read()) !== null) {
records.push({
sku: record.SKU,
productId: record.ProductID,
warehouseCode: record.WarehouseCode,
quantity: parseInt(record.Quantity, 10),
safetyStock: parseInt(record.SafetyStock || '0', 10),
reorderPoint: parseInt(record.ReorderPoint || '0', 10),
});
}
})
.on('error', reject)
.on('end', () => resolve(records));
});
}
private async processBatch(
records: InventoryRecord[],
results: ImportResult
) {
const operations = records.map(record =>
this.processRecord(record).then(
() => results.success++,
(error) => {
results.failed++;
results.errors.push({
record,
error: error.message,
});
}
)
);
await Promise.all(operations);
}
private async processRecord(record: InventoryRecord) {
// Validate record
if (!record.productId || !record.warehouseCode || record.quantity < 0) {
throw new Error('Invalid record data');
}
// Get warehouse by code
const { data: warehouseData } = await sdk.getWarehouseByCode({
code: record.warehouseCode,
});
if (!warehouseData?.warehouse) {
throw new Error(`Warehouse not found: ${record.warehouseCode}`);
}
// Update inventory level
await sdk.setInventoryLevel({
input: {
productId: record.productId,
warehouseId: warehouseData.warehouse.id,
quantity: record.quantity,
safetyStock: record.safetyStock,
reorderPoint: record.reorderPoint,
}
});
}
async exportToCSV(): Promise<string> {
const { data } = await sdk.getAllInventoryLevels({
includeZero: true,
});
const levels = data?.inventoryLevels || [];
const csv = [
'ProductID,ProductName,SKU,WarehouseCode,Quantity,Reserved,Available,SafetyStock,ReorderPoint',
...levels.map(level =>
[
level.product.id,
level.product.name,
level.product.sku,
level.warehouse.code,
level.quantity,
level.reserved,
level.available,
level.safetyStock,
level.reorderPoint,
].join(',')
),
].join('\n');
return csv;
}
}
Error Recovery
Step 1: Sync Failure Handler
// services/sync-recovery.ts
export class SyncRecoveryService {
private retryQueue: Map<string, RetryJob> = new Map();
async handleSyncFailure(
operation: SyncOperation,
error: Error
): Promise<void> {
// Log failure
await this.logSyncError(operation, error);
// Determine if retryable
if (this.isRetryableError(error)) {
await this.queueForRetry(operation);
} else {
await this.handlePermanentFailure(operation, error);
}
}
private isRetryableError(error: Error): boolean {
const retryableErrors = [
'NETWORK_ERROR',
'TIMEOUT',
'SERVICE_UNAVAILABLE',
'RATE_LIMITED',
];
return retryableErrors.some(code =>
error.message.includes(code)
);
}
private async queueForRetry(operation: SyncOperation) {
const jobId = `${operation.type}_${operation.id}`;
if (this.retryQueue.has(jobId)) {
const job = this.retryQueue.get(jobId)!;
job.attempts++;
if (job.attempts > 3) {
await this.handlePermanentFailure(
operation,
new Error('Max retries exceeded')
);
return;
}
} else {
this.retryQueue.set(jobId, {
operation,
attempts: 1,
nextRetry: Date.now() + 5000, // 5 seconds
});
}
// Schedule retry
setTimeout(() => this.executeRetry(jobId), 5000);
}
private async executeRetry(jobId: string) {
const job = this.retryQueue.get(jobId);
if (!job) return;
try {
await this.executeOperation(job.operation);
this.retryQueue.delete(jobId);
} catch (error) {
await this.handleSyncFailure(job.operation, error);
}
}
async reconcileInventory(): Promise<ReconciliationResult> {
const discrepancies: Discrepancy[] = [];
// Get all products
const { data } = await sdk.getAllProducts();
const products = data?.products || [];
for (const product of products) {
// Get inventory from all sources
const [system, calculated] = await Promise.all([
this.getSystemInventory(product.id),
this.calculateInventoryFromTransactions(product.id),
]);
if (system !== calculated) {
discrepancies.push({
productId: product.id,
systemQuantity: system,
calculatedQuantity: calculated,
difference: system - calculated,
});
}
}
// Auto-fix small discrepancies
for (const discrepancy of discrepancies) {
if (Math.abs(discrepancy.difference) <= 5) {
await this.autoFixDiscrepancy(discrepancy);
} else {
await this.flagForManualReview(discrepancy);
}
}
return {
totalProducts: products.length,
discrepanciesFound: discrepancies.length,
autoFixed: discrepancies.filter(d => Math.abs(d.difference) <= 5).length,
requiresReview: discrepancies.filter(d => Math.abs(d.difference) > 5).length,
};
}
}
Monitoring & Alerts
Step 1: Alert System
// services/inventory-alerts.ts
export class InventoryAlertService {
async checkStockLevels() {
const alerts: Alert[] = [];
// Get all products
const { data } = await sdk.getAllProducts();
for (const product of data?.products || []) {
const inventory = await this.getInventoryLevel(product.id);
// Check for out of stock
if (inventory.available <= 0) {
alerts.push({
type: 'OUT_OF_STOCK',
severity: 'HIGH',
productId: product.id,
productName: product.name,
message: `${product.name} is out of stock`,
});
}
// Check for low stock
else if (inventory.available <= inventory.safetyStock) {
alerts.push({
type: 'LOW_STOCK',
severity: 'MEDIUM',
productId: product.id,
productName: product.name,
currentStock: inventory.available,
safetyStock: inventory.safetyStock,
message: `${product.name} is below safety stock level`,
});
}
// Check if reorder needed
else if (inventory.available <= inventory.reorderPoint) {
alerts.push({
type: 'REORDER_NEEDED',
severity: 'LOW',
productId: product.id,
productName: product.name,
currentStock: inventory.available,
reorderPoint: inventory.reorderPoint,
reorderQuantity: inventory.reorderQuantity,
message: `${product.name} needs reordering`,
});
}
}
// Send alerts
if (alerts.length > 0) {
await this.sendAlerts(alerts);
}
return alerts;
}
private async sendAlerts(alerts: Alert[]) {
// Group by severity
const highPriority = alerts.filter(a => a.severity === 'HIGH');
const mediumPriority = alerts.filter(a => a.severity === 'MEDIUM');
const lowPriority = alerts.filter(a => a.severity === 'LOW');
// Send immediate notifications for high priority
if (highPriority.length > 0) {
await this.sendImmediateNotification(highPriority);
}
// Send email digest for medium/low
if (mediumPriority.length > 0 || lowPriority.length > 0) {
await this.sendEmailDigest([...mediumPriority, ...lowPriority]);
}
}
}
Best Practices
- Always use transactions for multi-step operations
- Implement idempotency for all sync operations
- Use optimistic locking to prevent race conditions
- Cache frequently accessed data with appropriate TTL
- Implement circuit breakers for external service calls
- Use bulk operations for better performance
- Maintain audit trails for all inventory changes
- Set up monitoring for sync failures and delays
- Implement graceful degradation when sync fails
- Use event sourcing for complex inventory scenarios
Troubleshooting
Common Issues
1. Sync Delays
Problem: Inventory updates taking too long Solution: Implement parallel processing and caching
2. Race Conditions
Problem: Concurrent updates causing incorrect inventory Solution: Use database locks or optimistic concurrency control
3. Lost Updates
Problem: WebSocket disconnections losing updates Solution: Implement message queuing with acknowledgments
4. Inventory Discrepancies
Problem: System inventory doesn't match physical Solution: Regular reconciliation and audit processes
Next Steps
- Implement predictive inventory management
- Add support for bundles and kits
- Integrate with ERP systems
- Add barcode scanning support
- Implement cycle counting
- Add demand forecasting
- Support for consignment inventory
- Implement drop-shipping workflows