Learn how to manage suppliers and purchase orders with the StateSet API
npm install stateset-node
# .env file
STATESET_API_KEY=your_api_key_here
NODE_ENV=production
import { StateSetClient } from 'stateset-node';
class SupplierService {
constructor() {
this.client = new StateSetClient({
apiKey: process.env.STATESET_API_KEY,
environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
timeout: 30000,
retries: 3
});
this.logger = this.setupLogger();
}
setupLogger() {
// Use your preferred logging service
return {
info: (message, data) => {
if (process.env.NODE_ENV !== 'production') {
console.info(`[INFO] ${message}`, data);
}
// Send to logging service
},
error: (message, error) => {
console.error(`[ERROR] ${message}`, error);
// Send to error tracking service
},
debug: (message, data) => {
if (process.env.NODE_ENV === 'development') {
console.debug(`[DEBUG] ${message}`, data);
}
}
};
}
async checkConnection() {
try {
const health = await this.client.health.check();
this.logger.info('StateSet API connection established', { status: health.status });
return health;
} catch (error) {
this.logger.error('Failed to connect to StateSet API', error);
throw new Error('Unable to establish API connection');
}
}
}
const supplierService = new SupplierService();
async function createSupplier(supplierData) {
try {
const supplier = await supplierService.client.suppliers.create({
name: supplierData.name,
contact_email: supplierData.contact_email,
contact_phone: supplierData.contact_phone,
address: supplierData.address,
payment_terms: supplierData.payment_terms || 'NET30',
lead_time_days: supplierData.lead_time_days || 7,
minimum_order_value: supplierData.minimum_order_value || 0,
preferred: supplierData.preferred || false,
active: true,
metadata: {
category: supplierData.category,
rating: supplierData.rating
}
});
supplierService.logger.info('Supplier created', {
supplierId: supplier.id,
name: supplier.name
});
return {
success: true,
supplier
};
} catch (error) {
supplierService.logger.error('Failed to create supplier', error);
throw error;
}
}
// Example usage
const newSupplier = await createSupplier({
name: 'Acme Manufacturing Co.',
contact_email: 'orders@acmemfg.com',
contact_phone: '+1-555-0123',
address: {
street: '123 Industrial Way',
city: 'Detroit',
state: 'MI',
postal_code: '48201',
country: 'US'
},
payment_terms: 'NET30',
lead_time_days: 14,
minimum_order_value: 500,
category: 'electronics',
rating: 4.5
});
async function updateSupplierMetrics(supplierId, metrics) {
try {
const updated = await supplierService.client.suppliers.update(supplierId, {
performance_metrics: {
on_time_delivery_rate: metrics.on_time_delivery_rate,
quality_rating: metrics.quality_rating,
response_time_hours: metrics.response_time_hours,
defect_rate: metrics.defect_rate,
last_evaluated: new Date().toISOString()
}
});
supplierService.logger.info('Supplier metrics updated', {
supplierId,
metrics: updated.performance_metrics
});
return updated;
} catch (error) {
supplierService.logger.error('Failed to update supplier metrics', error);
throw error;
}
}
async function findAlternativeSuppliers(criteria) {
try {
const alternatives = await supplierService.client.suppliers.list({
filter: {
active: true,
'metadata.category': criteria.category,
'performance_metrics.quality_rating': { $gte: criteria.min_quality_rating || 3.5 },
'performance_metrics.on_time_delivery_rate': { $gte: criteria.min_delivery_rate || 0.85 }
},
sort: '-performance_metrics.quality_rating',
limit: criteria.limit || 5
});
supplierService.logger.info('Alternative suppliers found', {
partNumber: criteria.part_number,
count: alternatives.data.length
});
// Calculate scores for each supplier
const scoredSuppliers = alternatives.data.map(supplier => ({
...supplier,
score: calculateSupplierScore(supplier, criteria)
}));
return scoredSuppliers.sort((a, b) => b.score - a.score);
} catch (error) {
supplierService.logger.error('Failed to find alternative suppliers', error);
throw error;
}
}
function calculateSupplierScore(supplier, criteria) {
const weights = {
quality: 0.3,
delivery: 0.3,
price: 0.2,
leadTime: 0.2
};
const scores = {
quality: (supplier.performance_metrics?.quality_rating || 0) / 5,
delivery: supplier.performance_metrics?.on_time_delivery_rate || 0,
price: 1 - (supplier.metadata?.price_index || 1), // Lower price = higher score
leadTime: Math.max(0, 1 - (supplier.lead_time_days / 30))
};
return Object.keys(weights).reduce((total, key) =>
total + (weights[key] * scores[key]), 0
);
}
async function createPurchaseOrder(poData) {
try {
// Validate supplier exists and is active
const supplier = await supplierService.client.suppliers.get(poData.supplier_id);
if (!supplier.active) {
throw new Error('Cannot create PO for inactive supplier');
}
// Calculate totals
const subtotal = poData.line_items.reduce((sum, item) =>
sum + (item.quantity * item.unit_price), 0
);
const tax = subtotal * (poData.tax_rate || 0);
const total = subtotal + tax + (poData.shipping_cost || 0);
const purchaseOrder = await supplierService.client.purchaseOrders.create({
supplier_id: poData.supplier_id,
po_number: generatePONumber(),
order_date: new Date().toISOString(),
expected_delivery_date: calculateExpectedDelivery(supplier.lead_time_days),
line_items: poData.line_items.map(item => ({
...item,
subtotal: item.quantity * item.unit_price
})),
subtotal,
tax,
shipping_cost: poData.shipping_cost || 0,
total,
status: 'pending',
payment_terms: supplier.payment_terms,
shipping_address: poData.shipping_address,
notes: poData.notes
});
supplierService.logger.info('Purchase order created', {
poId: purchaseOrder.id,
poNumber: purchaseOrder.po_number,
supplierId: purchaseOrder.supplier_id,
total: purchaseOrder.total
});
// Send notification to supplier
await notifySupplier(purchaseOrder);
return purchaseOrder;
} catch (error) {
supplierService.logger.error('Failed to create purchase order', error);
throw error;
}
}
function generatePONumber() {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substr(2, 5);
return `PO-${timestamp}-${random}`.toUpperCase();
}
function calculateExpectedDelivery(leadTimeDays) {
const date = new Date();
date.setDate(date.getDate() + leadTimeDays);
return date.toISOString();
}
async function getPurchaseOrderStatus(poId) {
try {
const po = await supplierService.client.purchaseOrders.get(poId);
const statusInfo = {
current_status: po.status,
status_history: po.status_history || [],
expected_delivery: po.expected_delivery_date,
days_until_delivery: calculateDaysUntilDelivery(po.expected_delivery_date),
is_overdue: isOrderOverdue(po),
completion_percentage: calculateCompletionPercentage(po)
};
supplierService.logger.info('Purchase order status retrieved', {
poId,
status: statusInfo.current_status
});
return statusInfo;
} catch (error) {
supplierService.logger.error('Failed to get purchase order status', error);
throw error;
}
}
function calculateDaysUntilDelivery(expectedDate) {
const now = new Date();
const delivery = new Date(expectedDate);
const diffTime = delivery - now;
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
function isOrderOverdue(po) {
return po.status !== 'delivered' &&
new Date(po.expected_delivery_date) < new Date();
}
function calculateCompletionPercentage(po) {
const statusProgress = {
'pending': 0,
'confirmed': 20,
'in_production': 40,
'shipped': 80,
'delivered': 100
};
return statusProgress[po.status] || 0;
}
async function createASN(asnData) {
try {
// Validate PO exists and is in correct status
const po = await supplierService.client.purchaseOrders.get(asnData.po_id);
if (!['confirmed', 'in_production'].includes(po.status)) {
throw new Error(`Cannot create ASN for PO in ${po.status} status`);
}
const asn = await supplierService.client.asns.create({
po_id: asnData.po_id,
po_number: po.po_number,
supplier_id: po.supplier_id,
asn_number: generateASNNumber(),
ship_date: asnData.ship_date || new Date().toISOString(),
expected_arrival_date: asnData.expected_arrival_date,
carrier: asnData.carrier,
tracking_number: asnData.tracking_number,
line_items: asnData.line_items.map(item => ({
po_line_item_id: item.po_line_item_id,
part_number: item.part_number,
description: item.description,
quantity_shipped: item.quantity_shipped,
unit_of_measure: item.unit_of_measure
})),
packing_list_number: asnData.packing_list_number,
total_weight: asnData.total_weight,
total_packages: asnData.total_packages,
status: 'in_transit',
notes: asnData.notes
});
// Update PO status
await supplierService.client.purchaseOrders.update(po.id, {
status: 'shipped',
tracking_info: {
carrier: asn.carrier,
tracking_number: asn.tracking_number,
ship_date: asn.ship_date
}
});
supplierService.logger.info('ASN created', {
asnId: asn.id,
asnNumber: asn.asn_number,
poNumber: asn.po_number,
trackingNumber: asn.tracking_number
});
return asn;
} catch (error) {
supplierService.logger.error('Failed to create ASN', error);
throw error;
}
}
function generateASNNumber() {
const date = new Date();
const dateStr = date.toISOString().split('T')[0].replace(/-/g, '');
const random = Math.random().toString(36).substr(2, 4);
return `ASN-${dateStr}-${random}`.toUpperCase();
}
async function receiveASN(asnId, receivingData) {
try {
const asn = await supplierService.client.asns.get(asnId);
// Record receiving details
const receiving = await supplierService.client.asns.update(asnId, {
status: 'received',
received_date: new Date().toISOString(),
received_by: receivingData.received_by,
receiving_notes: receivingData.notes,
line_items: asn.line_items.map(item => {
const receivedItem = receivingData.items.find(
r => r.po_line_item_id === item.po_line_item_id
);
return {
...item,
quantity_received: receivedItem?.quantity_received || 0,
condition: receivedItem?.condition || 'good',
discrepancy_notes: receivedItem?.discrepancy_notes
};
})
});
// Reconcile with PO
const reconciliationResult = await reconcileASNWithPO(asn.id, asn.po_id);
supplierService.logger.info('ASN received and reconciled', {
asnId,
poId: asn.po_id,
hasDiscrepancies: reconciliationResult.has_discrepancies
});
return {
asn: receiving,
reconciliation: reconciliationResult
};
} catch (error) {
supplierService.logger.error('Failed to receive ASN', error);
throw error;
}
}
async function reconcileASNWithPO(asnId, poId) {
try {
const [asn, po] = await Promise.all([
supplierService.client.asns.get(asnId),
supplierService.client.purchaseOrders.get(poId)
]);
const discrepancies = [];
// Check each line item
asn.line_items.forEach(asnItem => {
const poItem = po.line_items.find(
p => p.id === asnItem.po_line_item_id
);
if (!poItem) {
discrepancies.push({
type: 'item_not_in_po',
part_number: asnItem.part_number,
message: 'Item in ASN not found in PO'
});
return;
}
const quantityDiff = asnItem.quantity_received - poItem.quantity;
if (quantityDiff !== 0) {
discrepancies.push({
type: 'quantity_mismatch',
part_number: asnItem.part_number,
po_quantity: poItem.quantity,
received_quantity: asnItem.quantity_received,
difference: quantityDiff
});
}
if (asnItem.condition !== 'good') {
discrepancies.push({
type: 'condition_issue',
part_number: asnItem.part_number,
condition: asnItem.condition,
notes: asnItem.discrepancy_notes
});
}
});
const result = {
asn_id: asnId,
po_id: poId,
has_discrepancies: discrepancies.length > 0,
discrepancies,
reconciled_at: new Date().toISOString()
};
// Update PO status based on reconciliation
const newStatus = discrepancies.length > 0 ? 'received_with_issues' : 'received';
await supplierService.client.purchaseOrders.update(poId, {
status: newStatus,
reconciliation_result: result
});
return result;
} catch (error) {
supplierService.logger.error('Failed to reconcile ASN with PO', error);
throw error;
}
}
async function getSafetyStockStatus() {
try {
const inventory = await supplierService.client.inventory.list({
filter: {
quantity: { $lte: 'reorder_point' }
},
include: ['supplier']
});
const criticalItems = inventory.data.map(item => ({
part_number: item.part_number,
description: item.description,
current_quantity: item.quantity,
reorder_point: item.reorder_point,
safety_stock: item.safety_stock,
days_of_supply: calculateDaysOfSupply(item),
supplier: item.supplier,
urgency: calculateUrgency(item)
}));
supplierService.logger.info('Safety stock status retrieved', {
criticalItemsCount: criticalItems.length
});
return criticalItems.sort((a, b) => b.urgency - a.urgency);
} catch (error) {
supplierService.logger.error('Failed to get safety stock status', error);
throw error;
}
}
function calculateDaysOfSupply(item) {
if (!item.average_daily_usage || item.average_daily_usage === 0) return Infinity;
return Math.floor(item.quantity / item.average_daily_usage);
}
function calculateUrgency(item) {
const stockPercentage = item.quantity / item.reorder_point;
const daysOfSupply = calculateDaysOfSupply(item);
if (stockPercentage <= 0.25 || daysOfSupply <= 3) return 5; // Critical
if (stockPercentage <= 0.5 || daysOfSupply <= 7) return 4; // High
if (stockPercentage <= 0.75 || daysOfSupply <= 14) return 3; // Medium
if (stockPercentage <= 1 || daysOfSupply <= 21) return 2; // Low
return 1; // Normal
}
async function getSupplyChainKPIs(params = {}) {
try {
const timeframe = params.timeframe || '30d';
const startDate = calculateStartDate(timeframe);
const [suppliers, purchaseOrders, asns] = await Promise.all([
supplierService.client.suppliers.list({ filter: { active: true } }),
supplierService.client.purchaseOrders.list({
filter: { created_at: { $gte: startDate } }
}),
supplierService.client.asns.list({
filter: { created_at: { $gte: startDate } }
})
]);
const kpis = {
timeframe,
supplier_performance: calculateSupplierPerformanceKPIs(suppliers.data),
order_metrics: calculateOrderMetrics(purchaseOrders.data),
delivery_metrics: calculateDeliveryMetrics(asns.data),
cost_metrics: calculateCostMetrics(purchaseOrders.data),
generated_at: new Date().toISOString()
};
supplierService.logger.info('Supply chain KPIs calculated', {
timeframe,
supplierCount: suppliers.data.length
});
return kpis;
} catch (error) {
supplierService.logger.error('Failed to calculate supply chain KPIs', error);
throw error;
}
}
function calculateSupplierPerformanceKPIs(suppliers) {
const activeSuppliers = suppliers.filter(s => s.active);
const avgMetrics = activeSuppliers.reduce((acc, supplier) => {
const metrics = supplier.performance_metrics || {};
return {
quality_rating: acc.quality_rating + (metrics.quality_rating || 0),
on_time_delivery: acc.on_time_delivery + (metrics.on_time_delivery_rate || 0),
defect_rate: acc.defect_rate + (metrics.defect_rate || 0)
};
}, { quality_rating: 0, on_time_delivery: 0, defect_rate: 0 });
const count = activeSuppliers.length || 1;
return {
average_quality_rating: (avgMetrics.quality_rating / count).toFixed(2),
average_on_time_delivery: (avgMetrics.on_time_delivery / count * 100).toFixed(1) + '%',
average_defect_rate: (avgMetrics.defect_rate / count * 100).toFixed(2) + '%',
total_active_suppliers: activeSuppliers.length
};
}
class SupplierAPIError extends Error {
constructor(message, code, details) {
super(message);
this.name = 'SupplierAPIError';
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
}
async function withErrorHandling(operation, context) {
const startTime = Date.now();
try {
const result = await operation();
// Track successful operations
supplierService.logger.info('Operation completed', {
context,
duration: Date.now() - startTime
});
return result;
} catch (error) {
// Categorize and handle different error types
if (error.response?.status === 429) {
throw new SupplierAPIError(
'Rate limit exceeded',
'RATE_LIMIT',
{ retryAfter: error.response.headers['retry-after'] }
);
}
if (error.response?.status === 404) {
throw new SupplierAPIError(
'Resource not found',
'NOT_FOUND',
{ resource: context }
);
}
// Log and re-throw
supplierService.logger.error('Operation failed', {
context,
error: error.message,
duration: Date.now() - startTime
});
throw error;
}
}
// Usage example
const supplier = await withErrorHandling(
() => createSupplier(supplierData),
'create_supplier'
);