Distributed Order Management Quickstart Guide
Welcome to the Stateset One Distributed Order Management (DOM) Quickstart Guide! This guide will walk you through setting up your environment and using Stateset to manage your orders, inventory, and fulfillment processes across multiple sales channels and locations.
Imagine you’re an e-commerce business selling products through your online store, marketplaces, and physical locations. You need to efficiently manage orders, keep track of inventory, and make sure your customers get their products on time, no matter where they ordered them from. Stateset helps you achieve this with its powerful DOM features.
Table of Contents
- Introduction
- Getting Started
- Core Concepts of DOM
- API Walkthrough: End-to-End DOM Workflow
- Integration with Other Systems
- Real-Time Order Monitoring
- Error Handling and Logging
- Troubleshooting and Maintenance
- Support Resources
- Conclusion
Introduction
Distributed Order Management (DOM) is crucial for businesses managing orders across multiple channels and fulfillment locations. Stateset One provides a powerful API that simplifies DOM workflows, allowing you to manage your entire operation efficiently.
What You’ll Learn:
- How to set up your Stateset environment and use the SDK.
- Core DOM concepts, including order management, inventory control, and fulfillment.
- How to use the Stateset API to manage your order process from start to finish.
- Strategies for cross-channel synchronization and real-time monitoring.
- How to handle errors and integrate with other systems.
Getting Started
Let’s get your environment ready.
1. Sign Up for Stateset One
2. Generate an API Key
- After signing in, go 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:
Using yarn:
4. Set Up Environment Variables
Store your API key securely using environment variables. (Note: This example is for bash shell, adapt as needed for your environment)
5. Initialize the Stateset Client
Initialize the Stateset client in your application using the generated API key. (Note: Requires Node.js 16 or higher)
Note: The above example uses async/await
which requires a Javascript engine that supports it.
Core Concepts of DOM
Before diving into the API, it’s important to understand the key concepts behind a Distributed Order Management system. Here is a simplified view of the process:
- Order Management: This encompasses the entire lifecycle of an order, from the moment a customer places it to its successful delivery. This includes capturing the order, routing, tracking status, and handling any modifications. Why is it important? Centralized order management improves efficiency by managing all orders in one platform. It also enhances the customer experience by providing accurate and timely information.
- Inventory Management: This involves tracking and managing inventory levels across multiple locations. Accurate inventory data is key to ensuring products are available when they are needed, minimizing stockouts or overstocks. Why is it important? Proper inventory control is needed to fulfill orders and make sure your products are available. It also helps prevent delays in fulfillment.
- Fulfillment Orchestration: This is the process of coordinating and executing the fulfillment of orders. It includes selecting the right fulfillment locations, picking and packing items, and shipping them to the customer. Why is it important? Proper fulfillment orchestration helps ensure products get to your customer quickly and efficiently.
- Cross-Channel Synchronization: This ensures that order status and inventory levels are consistent across all sales channels. This will help create a seamless experience for the customers. Why is it important? Without it, inconsistencies will frustrate customers. For example, a customer could order an item online and find out it is out of stock at the physical store.
- Reporting and Analytics: This provides insights into order trends, fulfillment performance, and inventory management. It enables data-driven decision-making and optimization of the overall process. Why is it important? Analysis of your data will allow you to make more informed decisions about your business.
Understanding these core components will be essential as we explore the Stateset API and its functionalities.
API Walkthrough: End-to-End DOM Workflow
Let’s dive into the end-to-end workflow with the Stateset API.
Workflow 1: Order Capture and Routing
In this section, we’ll cover capturing orders from various sources and intelligently routing them to the best fulfillment location.
Example 1: Capture an Order
This function demonstrates how to capture an order using the client.order.create()
method. The order details include customer information, line items, shipping address, and the source channel. This captures data from different types of orders, and can be easily expanded to include different order scenarios.
Example 2: Route an Order
async function routeOrder(orderId) {
try {
const order = await client.order.get(orderId);
const fulfillmentLocations = await client.location.list({ type: 'fulfillment' });
if (!order || !fulfillmentLocations.length) {
throw new Error('Invalid order or no fulfillment locations available');
}
const mlPrediction = {
confidence: 0.95,
locationScores: fulfillmentLocations.reduce((acc, location) => {
acc[location.id] = Math.random();
return acc;
}, {})
}
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);
console.log("Order Routed:", updatedOrder);
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 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
});
}
const routedOrder = await routeOrder(order.id);
console.log("Routed Order:", routedOrder)
This function illustrates a more complex process, using a combination of inventory data, shipping rates, and an ML prediction (placeholder) to determine the optimal fulfillment location for the order. It includes placeholders for you to expand to include your own logic. This helps to understand the decision making process that goes into routing.
Workflow 2: Inventory Management and Allocation
This section focuses on managing inventory levels, allocating stock to orders, and rebalancing inventory across locations.
Example 1: Get a Global Inventory View
This function retrieves a global view of inventory for a specific product across all locations, using the client.inventory.list()
method. This will give you a snapshot of all the inventory, including the type and safety stock levels.
Example 2: Allocate Inventory for an Order
async function allocateInventory(orderId) {
let allocatedItems = [];
try {
const order = await client.order.get(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);
console.log("Allocation Results:", { 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 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()
});
}
const allocatedOrder = await allocateInventory(order.id)
console.log("Allocated order:", allocatedOrder)
This code shows how inventory is allocated to an order, based on the preferred location and available inventory. This function also demonstrates how to allocate from other locations if the primary location does not have sufficient stock.
Example 3: Rebalance Inventory
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';
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']
});
let 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;
}, {});
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';
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']
});
let 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;
}, {});
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';
try {
const locations = await client.location.list();
let 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;
}
}
}
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';
try {
const locations = await client.location.list();
let storageCapacity = {};
for (const location of locations) {
const capacityData = await client.warehouse.getCapacity(location.id);
storageCapacity[location.id] = capacityData.available_capacity;
}
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}`;
client.product.get(productId)
.then(product => {
const value = product.cost || product.price || 0;
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()
});
}
const rebalancedInventory = await rebalanceInventory();
console.log("Rebalanced Inventory:", rebalancedInventory)
This function shows how to rebalance your inventory across multiple locations using a combination of sales data, seasonal trends, and shipping costs. The code has placeholders to highlight where you would implement specific real-world data sources.
Workflow 3: Fulfillment
This section focuses on creating fulfillment orders and tracking their status.
Example 1: Create a Fulfillment Order
This function demonstrates how to create a fulfillment order, selecting a fulfillment center and determining a picking method based on order details.
Example 2: Track Fulfillment Status
This function retrieves the status of a fulfillment order using the client.fulfillmentorder.get()
method, showcasing how to track the progress of fulfillment.
Example 3: Update Fulfillment Status
This example showcases how to update the status of a fulfillment order, which is essential for keeping track of order progress and providing updates to customers. This example also includes tracking information.
Workflow 4: Cross-Channel Synchronization
This section demonstrates how to synchronize order and inventory data across different channels.
Example 1: Sync Order Status Across Channels
This function demonstrates how to synchronize order status from Stateset to an external sales channel, using a mapping function to translate between the two status types.
Example 2: Sync Inventory Levels Across Channels
This example illustrates how to sync inventory levels from Stateset to different sales channels, ensuring inventory data is consistent across all platforms.
Workflow 5: Reporting and Analytics
This section will demonstrate how to retrieve reports and analytics from the Stateset API.
Example 1: Get Sales Reports
This function retrieves sales reports, providing insights into overall sales trends and performance.
Example 2: Get Inventory Reports
This function retrieves inventory reports, allowing you to monitor inventory levels across different locations and products.
This function retrieves fulfillment performance reports, providing data on the efficiency and speed of your fulfillment processes.
Integration with Other Systems
Stateset’s API is designed to be flexible and integrate seamlessly with your existing systems. You can integrate it with:
- E-commerce platforms such as Shopify, Magento, and WooCommerce,
- Marketplaces like Amazon, eBay, and Etsy,
- Warehouse management systems (WMS) like NetSuite,
- Enterprise resource planning (ERP) systems such as SAP and Oracle,
- Carrier APIs from FedEx, UPS, and USPS, and more.
Implementing this will require you to build logic and data mappings between the systems based on your unique needs.
Real-Time Order Monitoring
Stateset’s API provides webhook functionality that can allow for you to receive real-time updates on order status, inventory changes, and fulfillment progress. The real-time capability allows for your systems to receive updates without having to poll for information.
Error Handling and Logging
Proper error handling and logging are essential for any robust application. Here’s how you can handle errors and log them:
- Catch Errors: Implement
try-catch
blocks around all API calls to handle any errors that might occur.
- Log Errors: Use Stateset’s Log API to record all errors for analysis and debugging. Include relevant context like the order ID, product ID, timestamp, and a detailed error message.
- Retry Logic: Implement retry logic for transient errors, such as network issues or API timeouts. Use exponential backoff to avoid overwhelming the system.
- Alerting: Set up alerts for critical errors. This will notify you when issues occur that require immediate attention.
Troubleshooting and Maintenance
- API Documentation: Refer to the comprehensive Stateset API documentation for details on each endpoint and its parameters.
- Logging: Review logs regularly to identify issues early. Log everything from API calls to any internal process issues.
- Monitoring: Set up performance monitoring to ensure smooth operation. Watch for performance bottlenecks and make the required adjustments.
- Maintenance: Plan regular maintenance and updates to keep your application running efficiently and securely.
Support Resources
If you encounter issues or have questions, here are some resources that can help you:
Conclusion
Congratulations on completing this quickstart guide! You’ve taken a major step in understanding and using Stateset One for Distributed Order Management. You now have the tools to manage orders, inventory, and fulfillment across multiple channels and locations using a robust API. Continue to explore the Stateset API documentation and other support resources to deepen your expertise.