Keep your product stock levels synchronized across platforms
const QuickbutikAPI = require('./quickbutik-api');
class InventoryManager {
constructor(apiKey) {
this.api = new QuickbutikAPI(apiKey);
}
async updateProductStock(sku, newQuantity, location = 'main') {
try {
const result = await this.api.updateProducts({
sku: sku,
stock: newQuantity,
qty_location: location
});
console.log(`Updated ${sku} stock to ${newQuantity} at ${location}`);
return result;
} catch (error) {
console.error(`Failed to update stock for ${sku}:`, error);
throw error;
}
}
async bulkUpdateInventory(inventoryUpdates) {
const results = [];
const batchSize = 10;
for (let i = 0; i < inventoryUpdates.length; i += batchSize) {
const batch = inventoryUpdates.slice(i, i + batchSize);
const batchPromises = batch.map(update =>
this.updateProductStock(update.sku, update.quantity, update.location)
.catch(error => ({ error: error.message, sku: update.sku }))
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Small delay to avoid rate limiting
if (i + batchSize < inventoryUpdates.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return results;
}
}
// Usage
const inventory = new InventoryManager(process.env.QUICKBUTIK_API_KEY);
// Update single product
await inventory.updateProductStock('SHIRT-123', 45);
// Bulk update
await inventory.bulkUpdateInventory([
{ sku: 'SHIRT-123', quantity: 45, location: 'warehouse-1' },
{ sku: 'PANTS-456', quantity: 12, location: 'warehouse-1' },
{ sku: 'HAT-789', quantity: 0, location: 'warehouse-2' }
]);
app.post('/webhooks/quickbutik', async (req, res) => {
const { event_type, product_id } = req.query;
// Acknowledge webhook immediately
res.status(200).send('OK');
if (event_type === 'product.update') {
await handleProductUpdate(product_id);
}
});
async function handleProductUpdate(productId) {
try {
// Fetch updated product details
const products = await api.getProducts({
product_id: productId,
include_details: true
});
if (!products || products.length === 0) {
throw new Error(`Product ${productId} not found`);
}
const product = products[0];
// Check if stock changed
if (await hasStockChanged(product)) {
await syncInventoryToExternalSystem(product);
}
} catch (error) {
console.error(`Failed to handle product update for ${productId}:`, error);
}
}
async function hasStockChanged(product) {
// Compare with your local inventory database
const localInventory = await getLocalInventory(product.sku);
return localInventory.quantity !== parseInt(product.qty || 0);
}
async function syncInventoryToExternalSystem(product) {
try {
const inventoryUpdate = {
sku: product.sku,
quantity: parseInt(product.qty || 0),
location: product.qty_location || 'default',
updated_at: new Date().toISOString()
};
const response = await fetch('https://your-system.com/api/inventory', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EXTERNAL_API_TOKEN}`
},
body: JSON.stringify(inventoryUpdate)
});
if (!response.ok) {
throw new Error(`External API error: ${response.status}`);
}
console.log(`Synced inventory for ${product.sku}: ${product.qty} units`);
} catch (error) {
console.error(`Failed to sync inventory for ${product.sku}:`, error);
// Add to retry queue
await addToRetryQueue({
type: 'inventory_sync',
product,
timestamp: new Date()
});
}
}
class LowStockMonitor {
constructor(apiKey, thresholds = {}) {
this.api = new QuickbutikAPI(apiKey);
this.defaultThreshold = thresholds.default || 10;
this.customThresholds = thresholds.custom || {};
}
async checkLowStock() {
try {
const products = await this.api.getProducts({
include_details: true,
limit: 500
});
const lowStockProducts = [];
for (const product of products) {
if (product.variants && product.variants.length > 0) {
// Check variants
for (const variant of product.variants) {
if (this.isLowStock(variant)) {
lowStockProducts.push({
...variant,
parent_sku: product.sku,
parent_title: product.title
});
}
}
} else {
// Check main product
if (this.isLowStock(product)) {
lowStockProducts.push(product);
}
}
}
if (lowStockProducts.length > 0) {
await this.handleLowStockAlert(lowStockProducts);
}
return lowStockProducts;
} catch (error) {
console.error('Failed to check low stock:', error);
throw error;
}
}
isLowStock(product) {
const currentStock = parseInt(product.qty || 0);
const threshold = this.customThresholds[product.sku] || this.defaultThreshold;
return currentStock > 0 && currentStock <= threshold;
}
async handleLowStockAlert(lowStockProducts) {
// Send alert notification
await this.sendLowStockNotification(lowStockProducts);
// Auto-reorder if configured
const autoReorderProducts = lowStockProducts.filter(p =>
this.shouldAutoReorder(p)
);
if (autoReorderProducts.length > 0) {
await this.createReorderRequests(autoReorderProducts);
}
}
async sendLowStockNotification(products) {
const message = `π¨ Low Stock Alert!\n\n${products.map(p =>
`${p.title || p.parent_title} (${p.sku}): ${p.qty} units remaining`
).join('\n')}`;
// Send to Slack
if (process.env.SLACK_WEBHOOK_URL) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
// Send email
if (process.env.EMAIL_WEBHOOK_URL) {
await fetch(process.env.EMAIL_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: process.env.INVENTORY_EMAIL,
subject: 'Low Stock Alert',
body: message
})
});
}
}
shouldAutoReorder(product) {
// Add your auto-reorder logic here
return product.supplier_sku && product.auto_reorder_enabled;
}
async createReorderRequests(products) {
for (const product of products) {
try {
const reorderQuantity = this.calculateReorderQuantity(product);
await this.createPurchaseOrder({
sku: product.sku,
supplier_sku: product.supplier_sku,
quantity: reorderQuantity,
supplier: product.supplier_name
});
console.log(`Created reorder for ${product.sku}: ${reorderQuantity} units`);
} catch (error) {
console.error(`Failed to create reorder for ${product.sku}:`, error);
}
}
}
calculateReorderQuantity(product) {
// Simple reorder logic - you can make this more sophisticated
const currentStock = parseInt(product.qty || 0);
const threshold = this.customThresholds[product.sku] || this.defaultThreshold;
const targetStock = threshold * 3; // Reorder to 3x threshold
return Math.max(targetStock - currentStock, threshold);
}
}
// Usage
const monitor = new LowStockMonitor(process.env.QUICKBUTIK_API_KEY, {
default: 10,
custom: {
'POPULAR-ITEM-123': 50,
'SEASONAL-456': 5
}
});
// Run every hour
setInterval(async () => {
try {
const lowStockProducts = await monitor.checkLowStock();
console.log(`Found ${lowStockProducts.length} low stock products`);
} catch (error) {
console.error('Low stock check failed:', error);
}
}, 60 * 60 * 1000);
class MultiLocationInventory {
constructor(apiKey) {
this.api = new QuickbutikAPI(apiKey);
this.locations = {
'warehouse-1': { name: 'Main Warehouse', priority: 1 },
'warehouse-2': { name: 'Secondary Warehouse', priority: 2 },
'store-front': { name: 'Physical Store', priority: 3 }
};
}
async getInventoryByLocation(sku) {
try {
const products = await this.api.getProducts({
product_id: sku,
include_details: true
});
if (!products || products.length === 0) {
return null;
}
const product = products[0];
const inventory = {};
// If product has variants, aggregate by location
if (product.variants && product.variants.length > 0) {
for (const variant of product.variants) {
const location = variant.qty_location || 'default';
const quantity = parseInt(variant.qty || 0);
inventory[location] = (inventory[location] || 0) + quantity;
}
} else {
const location = product.qty_location || 'default';
inventory[location] = parseInt(product.qty || 0);
}
return {
sku: product.sku,
title: product.title,
total_quantity: Object.values(inventory).reduce((sum, qty) => sum + qty, 0),
locations: inventory
};
} catch (error) {
console.error(`Failed to get inventory for ${sku}:`, error);
throw error;
}
}
async transferStock(sku, fromLocation, toLocation, quantity) {
try {
// Get current inventory
const inventory = await this.getInventoryByLocation(sku);
if (!inventory || !inventory.locations[fromLocation]) {
throw new Error(`No inventory found for ${sku} at ${fromLocation}`);
}
if (inventory.locations[fromLocation] < quantity) {
throw new Error(`Insufficient stock at ${fromLocation}. Available: ${inventory.locations[fromLocation]}, Requested: ${quantity}`);
}
// Update source location (decrease)
await this.updateLocationStock(sku, fromLocation, inventory.locations[fromLocation] - quantity);
// Update destination location (increase)
const currentDestStock = inventory.locations[toLocation] || 0;
await this.updateLocationStock(sku, toLocation, currentDestStock + quantity);
console.log(`Transferred ${quantity} units of ${sku} from ${fromLocation} to ${toLocation}`);
return {
sku,
quantity,
from: fromLocation,
to: toLocation,
timestamp: new Date().toISOString()
};
} catch (error) {
console.error(`Failed to transfer stock for ${sku}:`, error);
throw error;
}
}
async updateLocationStock(sku, location, newQuantity) {
return this.api.updateProducts({
sku,
stock: newQuantity,
qty_location: location
});
}
async getOptimalFulfillmentLocation(sku, requestedQuantity) {
const inventory = await this.getInventoryByLocation(sku);
if (!inventory) {
return null;
}
// Sort locations by priority and availability
const availableLocations = Object.entries(inventory.locations)
.filter(([location, quantity]) => quantity >= requestedQuantity)
.map(([location, quantity]) => ({
location,
quantity,
priority: this.locations[location]?.priority || 999
}))
.sort((a, b) => a.priority - b.priority);
return availableLocations.length > 0 ? availableLocations[0] : null;
}
}
// Usage
const multiLocation = new MultiLocationInventory(process.env.QUICKBUTIK_API_KEY);
// Get inventory across all locations
const inventory = await multiLocation.getInventoryByLocation('SHIRT-123');
console.log('Inventory:', inventory);
// Transfer stock between locations
await multiLocation.transferStock('SHIRT-123', 'warehouse-1', 'store-front', 5);
// Find optimal fulfillment location
const optimal = await multiLocation.getOptimalFulfillmentLocation('SHIRT-123', 10);
console.log('Optimal location:', optimal);
class OversellProtection {
constructor(apiKey) {
this.api = new QuickbutikAPI(apiKey);
this.safetyBuffer = 1; // Keep 1 unit as safety buffer
this.checkInterval = 5 * 60 * 1000; // Check every 5 minutes
}
async enableOversellProtection() {
// Start monitoring
setInterval(() => {
this.checkAndProtectInventory();
}, this.checkInterval);
console.log('Oversell protection enabled');
}
async checkAndProtectInventory() {
try {
const products = await this.api.getProducts({
include_details: true,
limit: 500
});
const protectionActions = [];
for (const product of products) {
// Skip if minus quantity is already disabled
if (product.disable_minusqty === '1') {
continue;
}
const currentStock = parseInt(product.qty || 0);
if (currentStock <= this.safetyBuffer) {
protectionActions.push({
sku: product.sku,
title: product.title,
currentStock,
action: 'disable_minus_qty'
});
await this.protectProduct(product.sku);
}
}
if (protectionActions.length > 0) {
await this.notifyProtectionActions(protectionActions);
}
} catch (error) {
console.error('Failed to check oversell protection:', error);
}
}
async protectProduct(sku) {
try {
await this.api.updateProducts({
sku,
disable_minusqty: '1'
});
console.log(`Enabled oversell protection for ${sku}`);
} catch (error) {
console.error(`Failed to protect product ${sku}:`, error);
}
}
async unprotectProduct(sku) {
try {
await this.api.updateProducts({
sku,
disable_minusqty: '0'
});
console.log(`Disabled oversell protection for ${sku}`);
} catch (error) {
console.error(`Failed to unprotect product ${sku}:`, error);
}
}
async notifyProtectionActions(actions) {
const message = `π‘οΈ Oversell Protection Activated!\n\n${actions.map(action =>
`${action.title} (${action.sku}): ${action.currentStock} units remaining - Protection enabled`
).join('\n')}`;
// Send notification
if (process.env.SLACK_WEBHOOK_URL) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
}
async manualStockCheck(sku) {
try {
const products = await this.api.getProducts({
product_id: sku,
include_details: true
});
if (!products || products.length === 0) {
throw new Error(`Product ${sku} not found`);
}
const product = products[0];
const currentStock = parseInt(product.qty || 0);
const isProtected = product.disable_minusqty === '1';
return {
sku: product.sku,
title: product.title,
currentStock,
isProtected,
recommendedAction: this.getRecommendedAction(currentStock, isProtected)
};
} catch (error) {
console.error(`Failed manual stock check for ${sku}:`, error);
throw error;
}
}
getRecommendedAction(currentStock, isProtected) {
if (currentStock <= this.safetyBuffer && !isProtected) {
return 'ENABLE_PROTECTION';
} else if (currentStock > this.safetyBuffer && isProtected) {
return 'DISABLE_PROTECTION';
} else {
return 'NO_ACTION_NEEDED';
}
}
}
// Usage
const protection = new OversellProtection(process.env.QUICKBUTIK_API_KEY);
// Enable automatic protection
await protection.enableOversellProtection();
// Manual check for specific product
const status = await protection.manualStockCheck('SHIRT-123');
console.log('Stock status:', status);
if (status.recommendedAction === 'ENABLE_PROTECTION') {
await protection.protectProduct(status.sku);
} else if (status.recommendedAction === 'DISABLE_PROTECTION') {
await protection.unprotectProduct(status.sku);
}