Introduction

The StateSet API provides a comprehensive suite of tools to manage your supplier relationships, including creating and updating suppliers, tracking performance metrics, managing purchase orders (POs), and handling advanced shipment notices (ASNs). This quickstart guide will walk you through key operations to integrate supplier management into your application.

Prerequisites

Before you begin, make sure you have:

  1. A StateSet account with API access
  2. Your API key (found in your dashboard)
  3. Node.js 18+ installed (for SDK usage)

Installation

Install the StateSet Node SDK:

npm install stateset-node

Configuration

Set up your environment variables:

# .env file
STATESET_API_KEY=your_api_key_here
NODE_ENV=production

Initialize the StateSet client with proper error handling:

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();

Supplier Management

Creating a Supplier

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
});

Updating Supplier Performance Metrics

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;
  }
}

Finding Alternative Suppliers

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
  );
}

Purchase Order Management

Creating a Purchase Order

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();
}

Tracking Purchase Order Status

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;
}

Advanced Shipment Notice (ASN) Management

Creating an ASN

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();
}

Receiving and Reconciling ASN

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;
  }
}

Analytics and Reporting

Safety Stock Status

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
}

Supply Chain KPIs

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
  };
}

Error Handling and Monitoring

Implement comprehensive error handling and monitoring:

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'
);

Best Practices

  1. Always use environment variables for sensitive configuration
  2. Implement proper logging instead of console.log statements
  3. Handle errors gracefully with meaningful error messages
  4. Use transactions for operations that modify multiple resources
  5. Implement retry logic for transient failures
  6. Monitor API usage to stay within rate limits
  7. Cache frequently accessed data to improve performance
  8. Validate data before sending to the API
  9. Use webhook events for real-time updates instead of polling
  10. Document your integration for future maintenance

Next Steps

Now that you understand the basics of supplier management with StateSet:

  1. Explore advanced features like automated reordering and demand forecasting
  2. Set up webhooks to receive real-time updates on PO and ASN status changes
  3. Integrate with your ERP for seamless data synchronization
  4. Build custom dashboards using the analytics data
  5. Implement approval workflows for purchase orders

For more information, check out:

If you need help, contact support@stateset.com or join our Discord community.