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
- Introduction
- Core Concepts
- Setting Up Your Environment
- Advanced API Usage
- Error Handling and Logging
- Real-time Order Monitoring
- Integration with Other Systems
- Performance Optimization
- Security Best Practices
- Troubleshooting and Maintenance
- 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
- Order Management
- Inventory Management
- Fulfillment Orchestration
- Cross-Channel Synchronization
- 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:
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';
const client = new stateset(process.env.STATESET_API_KEY);
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) {
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) {
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) {
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,
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 = [];
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 (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 (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 = [];
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;
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) {
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]);
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);
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 {
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;
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 {
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,
volume: 1
});
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';
}
}
async function selectFulfillmentCenter(order) {
const fulfillmentCenters = await client.location.list({ type: 'fulfillment' });
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 };
}));
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) {
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);
}
}
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'
});
}
}
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) {
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) {
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) {
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.
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);
return fulfillmentTime;
}
function calculatePickingEfficiency(order) {
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) {
return 1;
}
function calculateBatchPickingEfficiency(order) {
return 0.9;
}
function calculateZonePickingEfficiency(order) {
return 0.95;
}
function calculateWavePickingEfficiency(order) {
return 0.92;
}
function calculateDefaultPickingEfficiency(order) {
return 0.85;
}
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) {
const historicalSales = historicalData.filter(data => data.product_id === productId);
if (historicalSales.length === 0) {
return 0;
}
const last90Days = historicalSales.slice(-90);
const totalSales = last90Days.reduce((sum, day) => sum + day.quantity_sold, 0);
const averageDailySales = totalSales / 90;
const alpha = 0.3;
let forecast = averageDailySales;
for (let i = last90Days.length - 1; i >= 0; i--) {
forecast = alpha * last90Days[i].quantity_sold + (1 - alpha) * forecast;
}
const seasonalIndex = calculateSeasonalIndex(historicalSales);
forecast *= seasonalIndex;
return Math.max(Math.round(forecast), 0);
}
function calculateSeasonalIndex(historicalSales) {
return 1;
}
async function getHistoricalInventoryData() {
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) {
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) {
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
-
API Connection Failures
- Solution: Verify API key validity, check network connectivity, and ensure the API endpoint is correct.
-
Data Inconsistencies
- Solution: Implement regular cycle counts and reconcile inventory data with physical stock.
-
Order Routing Errors
- Solution: Review routing logic and ensure fulfillment locations have sufficient inventory.
-
Fulfillment Delays
- Solution: Monitor fulfillment centers for performance issues and optimize pick paths.
-
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.