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

  1. Architecture Overview
  2. Initial Setup
  3. Inventory Tracking
  4. Real-Time Synchronization
  5. Multi-Warehouse Management
  6. Channel Allocation
  7. Reservation System
  8. Batch Operations
  9. Error Recovery
  10. Monitoring & Alerts
  11. Best Practices
  12. 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

  1. Inventory Service - Central source of truth
  2. Warehouse Management - Physical location tracking
  3. Reservation System - Temporary holds for carts/orders
  4. Sync Engine - Real-time updates across channels
  5. 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

  1. Always use transactions for multi-step operations
  2. Implement idempotency for all sync operations
  3. Use optimistic locking to prevent race conditions
  4. Cache frequently accessed data with appropriate TTL
  5. Implement circuit breakers for external service calls
  6. Use bulk operations for better performance
  7. Maintain audit trails for all inventory changes
  8. Set up monitoring for sync failures and delays
  9. Implement graceful degradation when sync fails
  10. 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

Resources

Was this page helpful?