Distributed Order Management: Orders, Inventory, and Fulfillment Quickstart

Welcome to the Distributed Order Management (DOM) Quickstart Guide for Stateset One. This guide provides a comprehensive walkthrough for implementing the core components of a Distributed Order Management system using the Stateset API. It emphasizes Order Management, Inventory Control, and Fulfillment Orchestration across multiple locations and sales channels, ensuring efficient and scalable eCommerce operations.

Table of Contents

  1. Introduction
  2. Core Concepts
  3. Setting Up Your Environment
  4. Advanced API Usage
  5. Error Handling and Logging
  6. Real-time Order Monitoring
  7. Integration with Other Systems
  8. Performance Optimization
  9. Security Best Practices
  10. Troubleshooting and Maintenance
  11. Conclusion

Introduction

Distributed Order Management (DOM) systems are critical for managing orders, inventory, and fulfillment processes across multiple sales channels and fulfillment locations. Stateset One provides a robust REST and GraphQL API designed to streamline and enhance your DOM workflows. This guide will help you implement a scalable and efficient DOM system, addressing common challenges and leveraging advanced features to optimize your operations.

Key Components in the DOM Module

  • Order Management
  • Inventory Control
  • Fulfillment Orchestration
  • Cross-Channel Synchronization
  • Reporting and Analytics
  • Error Handling and Logging
  • Real-time Order Monitoring
  • Integration with Other Systems
  • Performance Optimization
  • Security Best Practices
  • Troubleshooting and Maintenance

Core Concepts

Before diving into the implementation, it’s essential to understand the foundational concepts that underpin Distributed Order Management systems.

Warehouse Management Processes

  1. Order Management
  2. Inventory Management
  3. Fulfillment Orchestration
  4. Cross-Channel Synchronization
  5. Reporting and Analytics

Common Challenges

  • Multi-Channel Complexity: Managing orders across various sales channels (e-commerce, marketplaces, physical stores) can be complex.
  • Inventory Synchronization: Keeping inventory levels accurate and synchronized across all channels is challenging.
  • Order Routing: Determining the optimal fulfillment location for each order can be complex, especially for businesses with multiple warehouses.
  • Order Modifications: Handling order changes or cancellations efficiently can be difficult.
  • Integration Issues: Connecting order management with other systems (ERP, WMS) can be technically challenging.
  • Peak Season Management: Handling increased order volumes during peak seasons without compromising efficiency.

Stateset’s Solutions

  • Centralized Order Management: Manage orders from all sales channels in a single platform.
  • Intelligent Order Routing: Automatically determines the best fulfillment location based on inventory levels, shipping costs, and delivery times.
  • Real-Time Inventory Sync: Keeps inventory levels updated across all channels in real-time.
  • Automated Order Processing: Streamlines order processing with customizable workflows and automation rules.
  • Order Modification Handling: Easily manage order changes, cancellations, and returns.
  • Integration Capabilities: Seamlessly connects with other business systems (ERP, WMS, CRM) for a unified operational view.
  • Scalable Architecture: Designed to handle high order volumes, especially during peak seasons.
  • Advanced Analytics: Provides detailed insights into order trends, fulfillment performance, and customer behavior.

Setting Up Your Environment

To begin leveraging Stateset’s Distributed Order Management capabilities, follow these initial setup steps.

1. Sign Up for Stateset One

2. Generate an API Key

  • After signing in, navigate to the Stateset Cloud Console to generate a new API key. This key will authenticate your API requests.

3. Install the Stateset Node.js SDK

Integrate Stateset’s SDK into your project to simplify API interactions.

Using npm:

npm install stateset-node

Using yarn:

yarn add stateset-node

4. Set Up Environment Variables

Store your API key securely using environment variables.

export STATESET_API_KEY=your_api_key_here

5. Initialize the Stateset Client

Initialize the Stateset client in your application using the generated API key.

import { stateset } from 'stateset-node';

// Initialize with your API key
const client = new stateset(process.env.STATESET_API_KEY);

// Verify connection
async function verifyConnection() {
  try {
    const status = await client.system.healthCheck();
    console.log('Connection status:', status);
  } catch (error) {
    console.error('Failed to connect:', error);
  }
}

verifyConnection();

Advanced API Usage

This section explores the advanced functionalities of Stateset’s Distributed Order Management APIs, including Order Management, Inventory Management, Fulfillment Orchestration, Cross-Channel Synchronization, and Reporting and Analytics.

Order Management

Effective order management is the backbone of any successful eCommerce operation. It encompasses the entire lifecycle of an order, from the moment a customer places it to its successful delivery and potential after-sales service.

2.1 Unified Order Capture

Implement a unified order capture function that handles orders from multiple channels:

async function captureOrder(orderData) {
  try {
    let order;
    switch(orderData.type) {
      case 'standard':
        order = await client.order.create({
          source: orderData.channel,
          channel_order_id: orderData.channelOrderId,
          customer: orderData.customer,
          line_items: orderData.lineItems,
          total_price: orderData.totalPrice,
          currency: orderData.currency,
          shipping_address: orderData.shippingAddress,
          status: 'received'
        });
        break;
      case 'pre-order':
        order = await createPreOrder(orderData);
        break;
      case 'backorder':
        order = await createBackOrder(orderData);
        break;
      case 'subscription':
        order = await createSubscriptionOrder(orderData);
        break;
      default:
        throw new Error('Unsupported order type');
    }
    return order;
  } catch (error) {
    console.error("Error capturing order:", error);
    throw error;
  }
}

2.2 Intelligent Distributed Order Routing

Determine the optimal fulfillment location based on inventory availability and other factors:

async function routeOrder(orderId) {
  try {
    const order = await client.order.retrieve(orderId);
    const fulfillmentLocations = await client.location.list({ type: 'fulfillment' });

    if (!order || !fulfillmentLocations.length) {
      throw new Error('Invalid order or no fulfillment locations available');
    }

    const historicalData = await getHistoricalFulfillmentData(order.customer.id);
    const mlPrediction = await mlModel.predict({
      order: order,
      historicalData: historicalData
    });

    const inventoryLevels = await getInventoryLevels(order.line_items.map(item => item.product_id));
    const shippingRates = await getShippingRates(order);

    const optimalLocation = determineOptimalLocation(order, fulfillmentLocations, mlPrediction, shippingRates, inventoryLevels);

    if (!optimalLocation) {
      throw new Error('Unable to determine optimal fulfillment location');
    }

    const updatedOrder = await client.order.update(orderId, { 
      assigned_location: optimalLocation.id,
      status: 'routed',
      routing_metadata: {
        prediction_confidence: mlPrediction.confidence,
        selected_shipping_rate: optimalLocation.selectedRate
      }
    });

    await logRoutingDecision(orderId, optimalLocation, mlPrediction);

    return { 
      optimalLocation, 
      updatedOrder 
    };
  } catch (error) {
    console.error(`Error routing order ${orderId}:`, error);
    await client.order.update(orderId, { status: 'routing_failed' });
    throw error;
  }
}

function determineOptimalLocation(order, locations, mlPrediction, shippingRates, inventoryLevels) {
  const scoredLocations = locations.map(location => {
    const inventoryScore = calculateInventoryScore(location, order, inventoryLevels);
    const shippingScore = calculateShippingScore(location, shippingRates);
    const mlScore = mlPrediction.locationScores[location.id] || 0;
    const fulfillmentCost = estimateFulfillmentCost(location, order);

    const totalScore = (inventoryScore * 0.4) + (shippingScore * 0.3) + (mlScore * 0.2) - (fulfillmentCost * 0.1);

    return {
      ...location,
      score: totalScore,
      selectedRate: shippingRates[location.id].cheapestRate
    };
  });

  return scoredLocations.reduce((best, current) => 
    current.score > best.score ? current : best
  , scoredLocations[0]);
}

function calculateInventoryScore(location, order, inventoryLevels) {
  return order.line_items.reduce((score, item) => {
    const availability = inventoryLevels[location.id]?.[item.product_id] || 0;
    return score + (availability >= item.quantity ? 1 : 0);
  }, 0) / order.line_items.length;
}

function calculateShippingScore(location, shippingRates) {
  const locationRates = shippingRates[location.id];
  const cheapestRate = locationRates.cheapestRate;
  const fastestRate = locationRates.fastestRate;
  return 1 - (cheapestRate.price / fastestRate.price);
}

function estimateFulfillmentCost(location, order) {
  // Simplified cost estimation
  const baseCost = location.fulfillmentCostFactor || 1;
  return baseCost * order.line_items.reduce((total, item) => total + item.quantity, 0);
}

async function getShippingRates(order) {
  try {
    const carriers = await client.carrier.list();
    const fulfillmentLocations = await client.location.list({ type: 'fulfillment' });

    const rates = {};
    for (const location of fulfillmentLocations) {
      const locationRates = await Promise.all(carriers.map(carrier => 
        carrier.getRates({
          origin: location.address,
          destination: order.shipping_address,
          packages: convertOrderToPackages(order)
        })
      ));

      const flatRates = locationRates.flatMap(rate => rate);
      rates[location.id] = {
        allRates: flatRates,
        cheapestRate: flatRates.reduce((min, rate) => rate.price < min.price ? rate : min),
        fastestRate: flatRates.reduce((fastest, rate) => rate.deliveryDays < fastest.deliveryDays ? rate : fastest)
      };
    }

    return rates;
  } catch (error) {
    console.error("Error fetching shipping rates:", error);
    throw error;
  }
}

function convertOrderToPackages(order) {
  // Simplified conversion - in reality, this would involve a more complex packing algorithm
  return [{
    weight: order.line_items.reduce((total, item) => total + (item.weight * item.quantity), 0),
    dimensions: {
      length: Math.max(...order.line_items.map(item => item.length)),
      width: Math.max(...order.line_items.map(item => item.width)),
      height: Math.max(...order.line_items.map(item => item.height))
    }
  }];
}

async function getHistoricalFulfillmentData(customerId) {
  // In a real implementation, this would query a data warehouse or analytics service
  return client.analytics.getCustomerFulfillmentHistory(customerId);
}

async function getInventoryLevels(productIds) {
  const inventoryRecords = await client.inventory.list({ product_id: { in: productIds } });
  return inventoryRecords.reduce((acc, record) => {
    acc[record.location_id] = acc[record.location_id] || {};
    acc[record.location_id][record.product_id] = record.available_quantity;
    return acc;
  }, {});
}

async function logRoutingDecision(orderId, optimalLocation, mlPrediction) {
  await client.log.create({
    type: 'order_routing',
    order_id: orderId,
    selected_location: optimalLocation.id,
    ml_confidence: mlPrediction.confidence,
    routing_score: optimalLocation.score
  });
}

2.3 Order Status Management

Update and track order status throughout its lifecycle:

async function updateOrderStatus(orderId, newStatus) {
  try {
    const updatedOrder = await client.order.update(orderId, { status: newStatus });
    console.log(`Order ${orderId} status updated to ${newStatus}`);
    return updatedOrder;
  } catch (error) {
    console.error("Error updating order status:", error);
    throw error;
  }
}

Inventory Management

Efficiently manage your inventory to maintain accuracy and optimize stock levels.

3.1 Global Inventory View

Create a function to get a global view of inventory across all locations:

async function getGlobalInventory(productId) {
  try {
    const inventoryList = await client.inventory.list({ product_id: productId });
    const globalInventory = inventoryList.reduce((acc, inv) => {
      acc[inv.location_id] = {
        quantity: inv.quantity,
        type: inv.inventory_type, // 'owned', 'dropship', or 'jit'
        safetyStock: inv.safety_stock_level
      };
      return acc;
    }, {});
    return globalInventory;
  } catch (error) {
    console.error("Error fetching global inventory:", error);
    throw error;
  }
}

3.2 Inventory Allocation

Allocate inventory for an order:

async function allocateInventory(orderId) {
  let allocatedItems = [];
  try {
    const order = await client.order.retrieve(orderId);
    const allocationResults = await Promise.all(order.line_items.map(async (item) => {
      const allocation = await determineOptimalAllocation(item, order.assigned_location);
      return { item, allocation };
    }));

    for (const { item, allocation } of allocationResults) {
      for (const alloc of allocation) {
        if (alloc.quantity > 0) {
          await client.inventory.allocate({
            product_id: item.product_id,
            location_id: alloc.location_id,
            quantity: alloc.quantity,
            allocation_type: alloc.type,
            order_id: orderId,
            item_id: item.id
          });
          allocatedItems.push({
            item_id: item.id,
            product_id: item.product_id,
            allocated_quantity: alloc.quantity,
            location_id: alloc.location_id,
            allocation_type: alloc.type
          });
        }
      }
    }

    const fullyAllocated = allocatedItems.every(ai => 
      ai.allocated_quantity === order.line_items.find(li => li.id === ai.item_id).quantity
    );

    await client.order.update(orderId, { 
      status: fullyAllocated ? 'fully_allocated' : 'partially_allocated',
      inventory_allocations: allocatedItems
    });

    await logAllocationResult(orderId, allocatedItems, fullyAllocated);

    return { orderId, allocatedItems, fullyAllocated };
  } catch (error) {
    console.error(`Error allocating inventory for order ${orderId}:`, error);
    await client.order.update(orderId, { 
      status: 'allocation_failed',
      allocation_error: error.message
    });
    throw error;
  }
}

async function determineOptimalAllocation(item, preferredLocation) {
  const inventory = await getInventoryLevels(item.product_id);
  const reservations = await getActiveReservations(item.product_id);
  const safetyStockLevels = await getSafetyStockLevels(item.product_id);

  let remainingQuantity = item.quantity;
  let allocations = [];

  // Try to allocate from the preferred location first
  if (inventory[preferredLocation]) {
    const preferredAllocation = allocateFromLocation(
      preferredLocation, 
      remainingQuantity, 
      inventory[preferredLocation], 
      reservations[preferredLocation], 
      safetyStockLevels[preferredLocation]
    );
    if (preferredAllocation.quantity > 0) {
      allocations.push(preferredAllocation);
      remainingQuantity -= preferredAllocation.quantity;
    }
  }

  // If we couldn't fully allocate from the preferred location, try others
  if (remainingQuantity > 0) {
    for (const [locationId, quantity] of Object.entries(inventory)) {
      if (locationId === preferredLocation) continue;
      
      const allocation = allocateFromLocation(
        locationId, 
        remainingQuantity, 
        quantity, 
        reservations[locationId], 
        safetyStockLevels[locationId]
      );
      
      if (allocation.quantity > 0) {
        allocations.push(allocation);
        remainingQuantity -= allocation.quantity;
        if (remainingQuantity === 0) break;
      }
    }
  }

  // If we still couldn't allocate everything, create a backorder
  if (remainingQuantity > 0) {
    allocations.push({
      location_id: 'backorder',
      quantity: remainingQuantity,
      type: 'backorder'
    });
  }

  return allocations;
}

function allocateFromLocation(locationId, requiredQuantity, availableQuantity, reservations, safetyStock) {
  const effectiveQuantity = Math.max(availableQuantity - reservations - safetyStock, 0);
  const allocatedQuantity = Math.min(requiredQuantity, effectiveQuantity);
  
  return {
    location_id: locationId,
    quantity: allocatedQuantity,
    type: allocatedQuantity > 0 ? 'standard' : 'none'
  };
}

async function getInventoryLevels(productId) {
  const inventoryRecords = await client.inventory.list({ product_id: productId });
  return inventoryRecords.reduce((acc, record) => {
    acc[record.location_id] = record.available_quantity;
    return acc;
  }, {});
}

async function getActiveReservations(productId) {
  const reservations = await client.reservation.list({ product_id: productId, status: 'active' });
  return reservations.reduce((acc, reservation) => {
    acc[reservation.location_id] = (acc[reservation.location_id] || 0) + reservation.quantity;
    return acc;
  }, {});
}

async function getSafetyStockLevels(productId) {
  const safetyStockRecords = await client.safetyStock.list({ product_id: productId });
  return safetyStockRecords.reduce((acc, record) => {
    acc[record.location_id] = record.quantity;
    return acc;
  }, {});
}

async function logAllocationResult(orderId, allocatedItems, fullyAllocated) {
  await client.log.create({
    type: 'inventory_allocation',
    order_id: orderId,
    allocated_items: allocatedItems,
    fully_allocated: fullyAllocated,
    timestamp: new Date().toISOString()
  });
}

3.3 Inventory Rebalancing

Implement a function to rebalance inventory across locations:

async function rebalanceInventory() {
  const rebalancingLog = [];
  try {
    const inventory = await client.inventory.list();
    const locations = await client.location.list();
    const salesVelocity = await getSalesVelocityByLocation();
    const seasonalTrends = await getSeasonalTrends();
    const shippingCosts = await getShippingCostMatrix();
    const storageCapacity = await getStorageCapacityByLocation();

    const rebalancingPlan = generateRebalancingPlan(
      inventory, 
      locations, 
      salesVelocity, 
      seasonalTrends, 
      shippingCosts, 
      storageCapacity
    );

    for (const transfer of rebalancingPlan) {
      try {
        await client.inventory.transfer(transfer);
        rebalancingLog.push({
          status: 'success',
          transfer: transfer
        });
      } catch (transferError) {
        console.error(`Error executing transfer:`, transferError);
        rebalancingLog.push({
          status: 'failed',
          transfer: transfer,
          error: transferError.message
        });
      }
    }

    const successfulTransfers = rebalancingLog.filter(log => log.status === 'success').length;
    console.log(`Inventory rebalancing completed. ${successfulTransfers}/${rebalancingPlan.length} transfers successful.`);

    await logRebalancingResult(rebalancingLog);

    return {
      totalTransfers: rebalancingPlan.length,
      successfulTransfers: successfulTransfers,
      rebalancingLog: rebalancingLog
    };

  } catch (error) {
    console.error("Error in inventory rebalancing process:", error);
    await logRebalancingError(error);
    throw error;
  }
}

function generateRebalancingPlan(inventory, locations, salesVelocity, seasonalTrends, shippingCosts, storageCapacity) {
  const rebalancingPlan = [];

  // Group inventory by product
  const productInventory = groupInventoryByProduct(inventory);

  for (const [productId, productLocations] of Object.entries(productInventory)) {
    const totalInventory = sum(Object.values(productLocations));
    const totalSalesVelocity = sum(Object.values(salesVelocity[productId] || {}));
    
    if (totalSalesVelocity === 0) continue; // Skip products with no sales

    const idealDistribution = calculateIdealDistribution(
      productId,
      totalInventory,
      locations,
      salesVelocity,
      seasonalTrends,
      storageCapacity
    );

    for (const [fromLocation, fromQuantity] of Object.entries(productLocations)) {
      const idealQuantity = idealDistribution[fromLocation] || 0;
      const difference = fromQuantity - idealQuantity;

      if (difference > 0) { // This location has excess inventory
        for (const [toLocation, toIdealQuantity] of Object.entries(idealDistribution)) {
          if (productLocations[toLocation] < toIdealQuantity) {
            const transferQuantity = Math.min(difference, toIdealQuantity - productLocations[toLocation]);
            if (transferQuantity > 0 && isTransferCostEffective(productId, fromLocation, toLocation, transferQuantity, shippingCosts)) {
              rebalancingPlan.push({
                product_id: productId,
                from_location: fromLocation,
                to_location: toLocation,
                quantity: transferQuantity
              });
              productLocations[fromLocation] -= transferQuantity;
              productLocations[toLocation] = (productLocations[toLocation] || 0) + transferQuantity;
            }
          }
        }
      }
    }
  }

  return rebalancingPlan;
}

function calculateIdealDistribution(productId, totalInventory, locations, salesVelocity, seasonalTrends, storageCapacity) {
  const distribution = {};
  const totalSalesVelocity = sum(Object.values(salesVelocity[productId] || {}));

  for (const location of locations) {
    const locationSalesVelocity = salesVelocity[productId]?.[location.id] || 0;
    const seasonalFactor = seasonalTrends[productId]?.[location.id] || 1;
    const adjustedSalesVelocity = locationSalesVelocity * seasonalFactor;

    let idealQuantity = Math.round((adjustedSalesVelocity / totalSalesVelocity) * totalInventory);
    idealQuantity = Math.min(idealQuantity, storageCapacity[location.id]); // Respect storage capacity

    distribution[location.id] = idealQuantity;
  }

  return distribution;
}

function isTransferCostEffective(productId, fromLocation, toLocation, quantity, shippingCosts) {
  const transferCost = shippingCosts[fromLocation]?.[toLocation] * quantity;
  const productValue = getProductValue(productId);
  const transferValueRatio = transferCost / (productValue * quantity);

  // Consider transfer cost-effective if it's less than 10% of the inventory value
  return transferValueRatio < 0.1;
}

function groupInventoryByProduct(inventory) {
  return inventory.reduce((acc, item) => {
    acc[item.product_id] = acc[item.product_id] || {};
    acc[item.product_id][item.location_id] = item.quantity;
    return acc;
  }, {});
}

async function getSalesVelocityByLocation() {
  const cacheKey = 'sales_velocity';
  let salesVelocity = cache.get(cacheKey);

  if (salesVelocity) {
    return salesVelocity;
  }

  try {
    // Assume we're fetching data for the last 90 days
    const endDate = new Date();
    const startDate = new Date(endDate.getTime() - (90 * 24 * 60 * 60 * 1000));

    const salesData = await client.analytics.getSales({
      start_date: startDate.toISOString(),
      end_date: endDate.toISOString(),
      group_by: ['product_id', 'location_id']
    });

    salesVelocity = salesData.reduce((acc, sale) => {
      acc[sale.product_id] = acc[sale.product_id] || {};
      acc[sale.product_id][sale.location_id] = sale.quantity / 90; // Daily sales velocity
      return acc;
    }, {});

    cache.set(cacheKey, salesVelocity);
    return salesVelocity;
  } catch (error) {
    console.error('Error fetching sales velocity:', error);
    throw new Error('Failed to fetch sales velocity data');
  }
}

async function getSeasonalTrends() {
  const cacheKey = 'seasonal_trends';
  let seasonalTrends = cache.get(cacheKey);

  if (seasonalTrends) {
    return seasonalTrends;
  }

  try {
    // Assume we're fetching seasonal data for the next 90 days
    const startDate = new Date();
    const endDate = new Date(startDate.getTime() + (90 * 24 * 60 * 60 * 1000));

    const trendsData = await client.analytics.getSeasonalTrends({
      start_date: startDate.toISOString(),
      end_date: endDate.toISOString(),
      group_by: ['product_id', 'location_id']
    });

    seasonalTrends = trendsData.reduce((acc, trend) => {
      acc[trend.product_id] = acc[trend.product_id] || {};
      acc[trend.product_id][trend.location_id] = trend.seasonal_factor;
      return acc;
    }, {});

    cache.set(cacheKey, seasonalTrends);
    return seasonalTrends;
  } catch (error) {
    console.error('Error fetching seasonal trends:', error);
    throw new Error('Failed to fetch seasonal trend data');
  }
}

async function getShippingCostMatrix() {
  const cacheKey = 'shipping_cost_matrix';
  let shippingCostMatrix = cache.get(cacheKey);

  if (shippingCostMatrix) {
    return shippingCostMatrix;
  }

  try {
    const locations = await client.location.list();
    shippingCostMatrix = {};

    for (const fromLocation of locations) {
      shippingCostMatrix[fromLocation.id] = {};
      for (const toLocation of locations) {
        if (fromLocation.id !== toLocation.id) {
          const shippingRate = await client.shipping.getRate({
            from: fromLocation.id,
            to: toLocation.id,
            weight: 1, // Assume 1 kg as a base weight
            volume: 1 // Assume 1 cubic meter as a base volume
          });
          shippingCostMatrix[fromLocation.id][toLocation.id] = shippingRate.cost;
        }
      }
    }

    cache.set(cacheKey, shippingCostMatrix);
    return shippingCostMatrix;
  } catch (error) {
    console.error('Error fetching shipping cost matrix:', error);
    throw new Error('Failed to fetch shipping cost data');
  }
}

async function getStorageCapacityByLocation() {
  const cacheKey = 'storage_capacity';
  let storageCapacity = cache.get(cacheKey);

  if (storageCapacity) {
    return storageCapacity;
  }

  try {
    const locations = await client.location.list();
    storageCapacity = {};

    for (const location of locations) {
      const capacityData = await client.warehouse.getCapacity(location.id);
      storageCapacity[location.id] = capacityData.available_capacity;
    }

    cache.set(cacheKey, storageCapacity);
    return storageCapacity;
  } catch (error) {
    console.error('Error fetching storage capacity:', error);
    throw new Error('Failed to fetch storage capacity data');
  }
}

function getProductValue(productId) {
  return new Promise((resolve, reject) => {
    const cacheKey = `product_value_${productId}`;
    const cachedValue = cache.get(cacheKey);

    if (cachedValue) {
      resolve(cachedValue);
    } else {
      client.product.get(productId)
        .then(product => {
          const value = product.cost || product.price || 0;
          cache.set(cacheKey, value);
          resolve(value);
        })
        .catch(error => {
          console.error(`Error fetching product value for ${productId}:`, error);
          reject(new Error(`Failed to fetch product value for ${productId}`));
        });
    }
  });
}

function sum(numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

async function logRebalancingResult(rebalancingLog) {
  await client.log.create({
    type: 'inventory_rebalancing',
    result: rebalancingLog,
    timestamp: new Date().toISOString()
  });
}

async function logRebalancingError(error) {
  await client.log.create({
    type: 'inventory_rebalancing_error',
    error: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString()
  });
}

Fulfillment Orchestration

Manage the fulfillment process from order creation to delivery, ensuring timely and accurate order processing.

4.1 Fulfillment Order Creation

Generate fulfillment orders based on allocated inventory:

async function createFulfillmentOrder(orderId) {
  try {
    const order = await client.order.retrieve(orderId);
    const fulfillmentCenter = await selectFulfillmentCenter(order);
    
    const fulfillmentOrder = await client.fulfillmentOrder.create({
      order_id: orderId,
      location_id: fulfillmentCenter.id,
      line_items: order.line_items,
      picking_method: determineOptimalPickingMethod(order, fulfillmentCenter)
    });
    
    await client.order.update(orderId, { status: 'in_fulfillment' });
    
    return fulfillmentOrder;
  } catch (error) {
    console.error("Error creating fulfillment order:", error);
    throw error;
  }
}

function determineOptimalPickingMethod(order, fulfillmentCenter) {

  const orderSize = order.line_items.length;
  const orderVolume = order.line_items.reduce((total, item) => total + item.quantity, 0);
  const uniqueProducts = new Set(order.line_items.map(item => item.product_id)).size;

  if (fulfillmentCenter.capabilities.includes('wave_picking') && orderSize > 50) {
    return 'wave_picking';
  } else if (fulfillmentCenter.capabilities.includes('zone_picking') && uniqueProducts > 20) {
    return 'zone_picking';
  } else if (fulfillmentCenter.capabilities.includes('batch_picking') && orderVolume > 100) {
    return 'batch_picking';
  } else if (fulfillmentCenter.capabilities.includes('cluster_picking') && orderSize > 10 && orderSize <= 50) {
    return 'cluster_picking';
  } else {
    return 'discrete_order_picking'; // Default to single-order picking for smaller, simpler orders
  }
}

async function selectFulfillmentCenter(order) {
  // Implement logic to select the best fulfillment center based on various factors
  const fulfillmentCenters = await client.location.list({ type: 'fulfillment' });
  
  // Calculate scores for each fulfillment center
  const scoredCenters = await Promise.all(fulfillmentCenters.map(async (center) => {
    const inventoryAvailability = await checkInventoryAvailability(center.id, order.line_items);
    const shippingCost = await estimateShippingCost(center.id, order.shipping_address);
    const processingTime = estimateProcessingTime(center, order);
    
    const score = calculateScore(inventoryAvailability, shippingCost, processingTime);
    
    return { center, score };
  }));
  
  // Sort centers by score (descending) and return the best one
  const bestCenter = scoredCenters.sort((a, b) => b.score - a.score)[0].center;
  
  return bestCenter;
}

4.2 Fulfillment Tracking

Update and track fulfillment status:

async function updateFulfillmentStatus(fulfillmentOrderId, status, trackingInfo = null) {
  try {
    const updatedFulfillment = await client.fulfillmentOrder.update(fulfillmentOrderId, { 
      status: status,
      tracking_number: trackingInfo?.trackingNumber,
      tracking_url: trackingInfo?.trackingUrl
    });
    
    if (status === 'completed') {
      const order = await client.order.retrieve(updatedFulfillment.order_id);
      await client.order.update(order.id, { status: 'fulfilled' });
      await updateInventoryPostFulfillment(order);
    }
    
    await syncFulfillmentStatusWithWMS(fulfillmentOrderId, status);
    
    return updatedFulfillment;
  } catch (error) {
    console.error("Error updating fulfillment status:", error);
    throw error;
  }
}

async function syncFulfillmentStatusWithWMS(fulfillmentOrderId, status) {
  // Implement WMS integration logic
  try {
    const wmsClient = await getWMSClient();
    const wmsStatus = mapStatesetToWMSStatus(status);
    
    await wmsClient.updateFulfillmentStatus({
      externalOrderId: fulfillmentOrderId,
      status: wmsStatus
    });

    console.log(`Successfully synced fulfillment status ${status} for order ${fulfillmentOrderId} with WMS`);
  } catch (error) {
    console.error(`Error syncing fulfillment status with WMS for order ${fulfillmentOrderId}:`, error);
    // Consider implementing retry logic or alerting mechanism here
  }
}

async function updateInventoryPostFulfillment(order) {
  for (const item of order.line_items) {
    const fulfillment = await client.fulfillmentOrder.retrieve(item.fulfillment_order_id);
    if (fulfillment.status === 'completed') {
      await client.inventory.update({
        product_id: item.product_id,
        location_id: fulfillment.location_id,
        quantity: -item.quantity,
        reason: 'fulfillment'
      });
    }
  }

  // Update any reserved inventory
  const reservations = await client.reservation.list({ order_id: order.id });
  for (const reservation of reservations) {
    await client.reservation.delete(reservation.id);
  }

  console.log(`Inventory updated for order ${order.id} after successful fulfillment`);
}

4.3 Multi-Location Fulfillment

Handle split shipments from multiple locations:

async function createSplitFulfillment(orderId) {
  try {
    const order = await client.order.retrieve(orderId);
    const splitFulfillments = [];
    
    for (const item of order.line_items) {
      const availableLocations = await findAvailableLocations(item.product_id, item.quantity);
      
      for (const location of availableLocations) {
        const fulfillmentOrder = await client.fulfillmentOrder.create({
          order_id: orderId,
          location_id: location.id,
          line_items: [{ ...item, quantity: location.availableQuantity }]
        });
        splitFulfillments.push(fulfillmentOrder);
      }
    }
    
    await client.order.update(orderId, { status: 'split_fulfillment' });
    
    return splitFulfillments;
  } catch (error) {
    console.error("Error creating split fulfillment:", error);
    throw error;
  }
}

async function findAvailableLocations(productId, requiredQuantity) {
  // Implement logic to find locations with available inventory
  const locations = await client.location.list({ type: 'fulfillment' });
  const availableLocations = [];

  for (const location of locations) {
    const inventory = await client.inventory.get({ product_id: productId, location_id: location.id });
    if (inventory.available_quantity >= requiredQuantity) {
      availableLocations.push({ id: location.id, availableQuantity: inventory.available_quantity });
    }
  }

  return availableLocations;
}

Cross-Channel Synchronization

Ensure consistency and accuracy across all sales channels by synchronizing order statuses and inventory levels.

5.1 Inventory Sync

Sync inventory levels across all sales channels:

async function syncInventoryAcrossChannels() {
  try {
    const inventory = await client.inventory.list();
    const channels = await client.channel.list();
    
    for (const item of inventory) {
      for (const channel of channels) {
        await updateChannelInventory(channel.id, item.product_id, item.quantity);
      }
    }
    
    console.log("Inventory sync completed successfully");
  } catch (error) {
    console.error("Error during inventory sync:", error);
  }
}

async function updateChannelInventory(channelId, productId, quantity) {
  // Implement channel-specific inventory update logic
  // Example: Send HTTP POST requests to channel's inventory API
  try {
    await fetch(`https://api.${channelId}.com/inventory/update`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${channelId}_API_KEY` },
      body: JSON.stringify({
        product_id: productId,
        quantity: quantity
      })
    });
    console.log(`Channel ${channelId} inventory updated for Product ${productId}`);
  } catch (error) {
    console.error(`Error updating channel ${channelId} inventory for Product ${productId}:`, error);
  }
}

5.2 Order Status Sync

Sync order statuses across channels:

async function syncOrderStatus(orderId) {
  try {
    const order = await client.order.retrieve(orderId);
    await updateChannelOrderStatus(order.channel, order.channel_order_id, order.status);
    console.log(`Order status for Order ${orderId} synchronized across channels.`);
  } catch (error) {
    console.error("Error syncing order status:", error);
    throw error;
  }
}

async function updateChannelOrderStatus(channelId, channelOrderId, status) {
  // Implement channel-specific order status update logic
  // Example: Send HTTP PUT requests to channel's order API
  try {
    await fetch(`https://api.${channelId}.com/orders/${channelOrderId}/status`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${channelId}_API_KEY` },
      body: JSON.stringify({ status: status })
    });
    console.log(`Channel ${channelId} order ${channelOrderId} status updated to ${status}`);
  } catch (error) {
    console.error(`Error updating channel ${channelId} order ${channelOrderId} status:`, error);
  }
}

Reporting and Analytics

Gain valuable insights into your order fulfillment processes and inventory management through comprehensive reporting and analytics.

6.1 Order Fulfillment Performance

Generate a report on order fulfillment performance:

async function generateFulfillmentPerformanceReport(startDate, endDate) {
  try {
    const orders = await client.order.list({
      created_at: { gte: startDate, lte: endDate },
      status: 'fulfilled'
    });

    const performance = orders.reduce((acc, order) => {
      const fulfillmentTime = calculateFulfillmentTime(order);
      const pickingEfficiency = calculatePickingEfficiency(order);
      acc.totalOrders++;
      acc.totalFulfillmentTime += fulfillmentTime;
      acc.totalPickingEfficiency += pickingEfficiency;
      acc.locations[order.assigned_location] = (acc.locations[order.assigned_location] || 0) + 1;
      return acc;
    }, { totalOrders: 0, totalFulfillmentTime: 0, totalPickingEfficiency: 0, locations: {} });

    performance.averageFulfillmentTime = performance.totalFulfillmentTime / performance.totalOrders;
    performance.averagePickingEfficiency = performance.totalPickingEfficiency / performance.totalOrders;

    return performance;
  } catch (error) {
    console.error("Error generating fulfillment performance report:", error);
    throw error;
  }
}

function calculateFulfillmentTime(order) {
  const receivedDate = new Date(order.created_at);
  const fulfilledDate = new Date(order.fulfilled_at);
  const fulfillmentTime = (fulfilledDate - receivedDate) / (1000 * 60 * 60); // Convert to hours
  return fulfillmentTime;
}

function calculatePickingEfficiency(order) {
  // Implement logic to calculate picking efficiency based on chosen picking method
  const pickingMethod = order.picking_method || 'single';
  let efficiency = 0;

  switch (pickingMethod) {
    case 'single':
      efficiency = calculateSingleOrderPickingEfficiency(order);
      break;
    case 'batch':
      efficiency = calculateBatchPickingEfficiency(order);
      break;
    case 'zone':
      efficiency = calculateZonePickingEfficiency(order);
      break;
    case 'wave':
      efficiency = calculateWavePickingEfficiency(order);
      break;
    default:
      console.warn(`Unknown picking method: ${pickingMethod}. Using default efficiency calculation.`);
      efficiency = calculateDefaultPickingEfficiency(order);
  }

  return efficiency;
}

function calculateSingleOrderPickingEfficiency(order) {
  // Placeholder for single order picking efficiency calculation
  return 1; // Assume perfect efficiency
}

function calculateBatchPickingEfficiency(order) {
  // Placeholder for batch picking efficiency calculation
  return 0.9; // Example efficiency
}

function calculateZonePickingEfficiency(order) {
  // Placeholder for zone picking efficiency calculation
  return 0.95; // Example efficiency
}

function calculateWavePickingEfficiency(order) {
  // Placeholder for wave picking efficiency calculation
  return 0.92; // Example efficiency
}

function calculateDefaultPickingEfficiency(order) {
  // Placeholder for default picking efficiency calculation
  return 0.85; // Example efficiency
}

6.2 Inventory Turnover Report

Generate a report on inventory turnover across locations:

async function generateInventoryReport(startDate, endDate) {
  try {
    const sales = await client.order.list({
      created_at: { gte: startDate, lte: endDate },
      status: 'fulfilled'
    });
    const currentInventory = await client.inventory.list();
    const historicalData = await getHistoricalInventoryData();

    const report = currentInventory.reduce((acc, inv) => {
      const sold = calculateTotalSold(sales, inv.product_id, inv.location_id);
      const turnoverRate = sold / ((inv.quantity + sold) / 2);
      const predictedDemand = predictFutureDemand(inv.product_id, historicalData);
      
      acc[inv.location_id] = acc[inv.location_id] || {};
      acc[inv.location_id][inv.product_id] = {
        currentStock: inv.quantity,
        soldQuantity: sold,
        turnoverRate: turnoverRate,
        predictedDemand: predictedDemand
      };
      return acc;
    }, {});

    return report;
  } catch (error) {
    console.error("Error generating inventory report:", error);
    throw error;
  }
}

function calculateTotalSold(sales, productId, locationId) {
  return sales.reduce((total, order) => {
    const relevantLineItems = order.line_items.filter(item => 
      item.product_id === productId && item.fulfillment_location_id === locationId
    );
    const quantitySold = relevantLineItems.reduce((sum, item) => sum + item.quantity, 0);
    return total + quantitySold;
  }, 0);
}

function predictFutureDemand(productId, historicalData) {
  // Implement demand forecasting logic using historical data
  const historicalSales = historicalData.filter(data => data.product_id === productId);
  
  if (historicalSales.length === 0) {
    return 0; // No historical data available
  }

  // Calculate average daily sales for the last 90 days
  const last90Days = historicalSales.slice(-90);
  const totalSales = last90Days.reduce((sum, day) => sum + day.quantity_sold, 0);
  const averageDailySales = totalSales / 90;

  // Apply simple exponential smoothing for short-term forecast
  const alpha = 0.3; // Smoothing factor
  let forecast = averageDailySales;
  for (let i = last90Days.length - 1; i >= 0; i--) {
    forecast = alpha * last90Days[i].quantity_sold + (1 - alpha) * forecast;
  }

  // Apply seasonal adjustment if applicable
  const seasonalIndex = calculateSeasonalIndex(historicalSales);
  forecast *= seasonalIndex;

  // Round to nearest integer and ensure non-negative
  return Math.max(Math.round(forecast), 0);
}

function calculateSeasonalIndex(historicalSales) {
  // Placeholder for seasonal index calculation
  // In reality, this would involve more sophisticated analysis
  return 1; // No seasonal adjustment
}

async function getHistoricalInventoryData() {
  // Implement logic to fetch historical inventory data
  // This might involve querying a data warehouse or analytics service
  return client.analytics.getHistoricalInventory();
}

Integration with Other Systems

Enhance your DOM workflows by integrating Stateset’s APIs with other systems such as Enterprise Resource Planning (ERP), Warehouse Management Systems (WMS), Customer Relationship Management (CRM), and Accounting Systems.

Sync with Enterprise Resource Planning (ERP) System

async function syncWithERP() {
  try {
    const fulfilledOrders = await client.order.list({ status: 'fulfilled' });
    await updateERPWithFulfilledOrders(fulfilledOrders.items);
    console.log('Synchronized fulfilled orders with ERP.');
  } catch (error) {
    console.error("Error syncing with ERP:", error);
  }
}

async function updateERPWithFulfilledOrders(orders) {
  for (const order of orders) {
    // Implement ERP-specific order update logic
    try {
      await fetch('https://erp.example.com/api/orders/update', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ERP_API_KEY` },
        body: JSON.stringify({
          order_id: order.channel_order_id,
          status: order.status,
          fulfillment_details: order.fulfillment_metadata
        })
      });
      console.log(`ERP updated for Order ${order.channel_order_id}`);
    } catch (error) {
      console.error(`Error updating ERP for Order ${order.channel_order_id}:`, error);
    }
  }
}

syncWithERP();

Sync with Warehouse Management System (WMS)

async function syncWithWMS() {
  try {
    const fulfillmentOrders = await client.fulfillmentOrder.list({ status: 'in_progress' });
    await updateWMSWithFulfillmentOrders(fulfillmentOrders.items);
    console.log('Synchronized fulfillment orders with WMS.');
  } catch (error) {
    console.error("Error syncing with WMS:", error);
  }
}

async function updateWMSWithFulfillmentOrders(fulfillmentOrders) {
  for (const fulfillment of fulfillmentOrders) {
    // Implement WMS-specific fulfillment order update logic
    try {
      await fetch('https://wms.example.com/api/fulfillment/update', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer WMS_API_KEY` },
        body: JSON.stringify({
          fulfillment_order_id: fulfillment.id,
          status: fulfillment.status,
          tracking_number: fulfillment.tracking_number
        })
      });
      console.log(`WMS updated for Fulfillment Order ${fulfillment.id}`);
    } catch (error) {
      console.error(`Error updating WMS for Fulfillment Order ${fulfillment.id}:`, error);
    }
  }
}

syncWithWMS();

Troubleshooting and Maintenance

Maintain the health of your Distributed Order Management system by addressing common issues and performing regular maintenance.

Common Issues and Solutions

  1. API Connection Failures

    • Solution: Verify API key validity, check network connectivity, and ensure the API endpoint is correct.
  2. Data Inconsistencies

    • Solution: Implement regular cycle counts and reconcile inventory data with physical stock.
  3. Order Routing Errors

    • Solution: Review routing logic and ensure fulfillment locations have sufficient inventory.
  4. Fulfillment Delays

    • Solution: Monitor fulfillment centers for performance issues and optimize pick paths.
  5. Inventory Discrepancies

    • Solution: Use cycle counts and audit logs to identify and rectify discrepancies promptly.

Regular Maintenance Tasks

  • System Updates: Keep your SDKs and dependencies up to date.
  • Data Backups: Regularly back up your data to prevent loss.
  • Performance Monitoring: Continuously monitor system performance and optimize as needed.
  • Security Audits: Perform regular security audits to identify and mitigate vulnerabilities.

Support Resources


Conclusion

By following this Distributed Order Management Quickstart Guide, you are now equipped to harness the full potential of Stateset’s DOM APIs. Implement these practices to enhance your order processing efficiency, maintain accurate inventory levels, streamline fulfillment operations, and ensure seamless integration across all your sales channels and fulfillment locations. For further assistance, refer to the support resources or engage with the Stateset community.


Feel free to customize this guide further based on your specific needs or to add more detailed sections as required. This structured approach ensures that users can seamlessly navigate through the setup, implementation, optimization, and maintenance phases of using Stateset’s Distributed Order Management APIs.