// @ts-check
/**
* Inflow Inventory API Client
* Handles all HTTP requests to the Inflow API
*/
import axios from 'axios';
export class InflowClient {
/**
* @param {Object} config
* @param {string} config.apiKey - Inflow API key
* @param {string} config.companyId - Inflow company ID
* @param {string} [config.apiUrl] - Base API URL
* @param {string} [config.apiVersion] - API version
*/
constructor(config) {
this.config = config;
this.apiUrl = config.apiUrl || 'https://cloudapi.inflowinventory.com';
this.apiVersion = config.apiVersion || '2025-06-24';
// Create axios instance with default config
this.client = axios.create({
baseURL: this.apiUrl,
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
'Accept': `application/json;version=${this.apiVersion}`
}
});
}
/**
* List all products with optional filters
* @param {Object} [options]
* @param {string} [options.name] - Filter by name
* @param {string} [options.description] - Filter by description
* @param {boolean} [options.isActive] - Filter by active status
* @param {string} [options.barcode] - Filter by barcode
* @param {string} [options.smart] - Full-text search on name, description, category, barcode, SKU
* @param {string} [options.include] - Related entities to include (e.g., 'inventoryLines,defaultImage')
* @param {number} [options.limit] - Max results to return
* @returns {Promise<Object>}
*/
async listProducts(options = {}) {
try {
const params = new URLSearchParams();
// Add filters
if (options.name) params.append('filter[name]', options.name);
if (options.description) params.append('filter[description]', options.description);
if (options.isActive !== undefined) params.append('filter[isActive]', options.isActive.toString());
if (options.barcode) params.append('filter[barcode]', options.barcode);
if (options.smart) params.append('filter[smart]', options.smart);
// Add includes
if (options.include) params.append('include', options.include);
// Add limit
if (options.limit) params.append('limit', options.limit.toString());
const response = await this.client.get(
`/${this.config.companyId}/products?${params.toString()}`
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'listProducts');
}
}
/**
* Get a specific product by ID
* @param {string} productId
* @param {string} [include] - Related entities to include
* @returns {Promise<Object>}
*/
async getProduct(productId, include = null) {
try {
const params = include ? `?include=${include}` : '';
const response = await this.client.get(
`/${this.config.companyId}/products/${productId}${params}`
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'getProduct');
}
}
/**
* Get product inventory summary
* @param {string} productId
* @returns {Promise<Object>}
*/
async getProductSummary(productId) {
try {
const response = await this.client.get(
`/${this.config.companyId}/products/${productId}/summary`
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'getProductSummary');
}
}
/**
* Create or update a product
* @param {Object} product - Product object with productId
* @returns {Promise<Object>}
*/
async upsertProduct(product) {
try {
if (!product.productId) {
return {
success: false,
error: 'productId is required for upsert'
};
}
const response = await this.client.put(
`/${this.config.companyId}/products`,
product
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'upsertProduct');
}
}
/**
* List stock adjustments with optional filters
* @param {Object} [options]
* @param {string} [options.adjustmentNumber] - Filter by adjustment number
* @param {string} [options.include] - Related entities to include
* @param {number} [options.limit] - Max results to return
* @returns {Promise<Object>}
*/
async listStockAdjustments(options = {}) {
try {
const params = new URLSearchParams();
if (options.adjustmentNumber) params.append('filter[adjustmentNumber]', options.adjustmentNumber);
if (options.include) params.append('include', options.include);
if (options.limit) params.append('limit', options.limit.toString());
const response = await this.client.get(
`/${this.config.companyId}/stock-adjustments?${params.toString()}`
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'listStockAdjustments');
}
}
/**
* Get a specific stock adjustment by ID
* @param {string} stockAdjustmentId
* @param {string} [include] - Related entities to include
* @returns {Promise<Object>}
*/
async getStockAdjustment(stockAdjustmentId, include = null) {
try {
const params = include ? `?include=${include}` : '';
const response = await this.client.get(
`/${this.config.companyId}/stock-adjustments/${stockAdjustmentId}${params}`
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'getStockAdjustment');
}
}
/**
* Create or update a stock adjustment
* @param {Object} stockAdjustment - Stock adjustment object with stockAdjustmentId
* @returns {Promise<Object>}
*/
async upsertStockAdjustment(stockAdjustment) {
try {
if (!stockAdjustment.stockAdjustmentId) {
return {
success: false,
error: 'stockAdjustmentId is required for upsert'
};
}
const response = await this.client.put(
`/${this.config.companyId}/stock-adjustments`,
stockAdjustment
);
return {
success: true,
data: response.data
};
} catch (error) {
return this._handleError(error, 'upsertStockAdjustment');
}
}
/**
* Handle API errors
* @private
*/
_handleError(error, operation) {
console.error(`Inflow API Error (${operation}):`, error.message);
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
return {
success: false,
error: error.response.data?.message || error.message,
status: error.response.status,
details: error.response.data
};
} else if (error.request) {
// The request was made but no response was received
return {
success: false,
error: 'No response received from Inflow API',
details: error.message
};
} else {
// Something happened in setting up the request
return {
success: false,
error: error.message
};
}
}
}