UNPKG

@arathron/n8n-nodes-zoho-books

Version:

n8n community nodes for Zoho Books and Zoho Inventory API integration - Complete CRUD operations for Sales Orders, Invoices, Items, Vendors, Credit Notes, Payments, Purchase Orders, Bills, Composite Items, Tax Management, Tax Groups, and Tax Exemptions

316 lines (311 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkRateLimit = checkRateLimit; exports.zohoApiRequest = zohoApiRequest; exports.zohoApiRequestWithFormData = zohoApiRequestWithFormData; exports.zohoApiRequestAllItems = zohoApiRequestAllItems; exports.getItemBySku = getItemBySku; const n8n_workflow_1 = require("n8n-workflow"); const DEFAULT_RATE_LIMIT_PER_MINUTE = 100; const DEFAULT_DAILY_LIMIT = 1000; const RETRY_LIMIT = 3; const BACKOFF_BASE = 1000; const rateLimiter = { requests: [], dailyRequests: 0, lastReset: Date.now(), lastRequest: 0, }; async function checkRateLimit(rateLimitPerMinute = DEFAULT_RATE_LIMIT_PER_MINUTE, dailyLimit = DEFAULT_DAILY_LIMIT) { const now = Date.now(); const oneMinuteAgo = now - 60000; if (now - rateLimiter.lastReset > 86400000) { rateLimiter.dailyRequests = 0; rateLimiter.lastReset = now; } rateLimiter.requests = rateLimiter.requests.filter(timestamp => timestamp > oneMinuteAgo); if (rateLimiter.dailyRequests >= dailyLimit) { throw new Error('Daily API limit exceeded'); } if (rateLimiter.requests.length >= rateLimitPerMinute) { const waitTime = 60000 - (now - rateLimiter.requests[0]); if (waitTime > 0) { await new Promise(resolve => setTimeout(resolve, waitTime)); } } rateLimiter.requests.push(now); rateLimiter.dailyRequests++; rateLimiter.lastRequest = now; } function createZohoError(errorResponse) { const code = errorResponse.code || 0; const message = errorResponse.message || 'Unknown error'; const errorMessages = { 1001: { message: 'Resource not found', description: 'The requested resource does not exist or has been deleted.', }, 1002: { message: 'Invalid data provided', description: 'Please check the required fields and data formats.', }, 1003: { message: 'Duplicate entry', description: 'A record with the same information already exists.', }, 4000: { message: 'Rate limit exceeded', description: 'Too many requests. Please wait and try again.', }, 1006: { message: 'Invalid organization', description: 'The organization ID is invalid or inactive.', }, }; const errorInfo = errorMessages[code] || { message: 'Unknown error occurred', description: message || 'Unknown error occurred', }; return { message: errorInfo.message, description: errorInfo.description, httpCode: code >= 4000 ? '429' : '400', }; } function isRateLimitError(error) { return (error?.response?.status === 429 || error?.code === 4000 || (error?.message && error.message.toLowerCase().includes('rate limit'))); } function isAuthError(error) { return error?.response?.status === 401 || error?.httpCode === 401; } function isNetworkError(error) { return (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT' || error?.message?.includes('Network error')); } function isValidationError(error) { return error?.response?.status === 400 || error?.httpCode === 400; } async function zohoApiRequest(method, endpoint, body = {}, qs = {}) { const credentials = await this.getCredentials('zohoInventoryOAuth2'); const dataCenter = credentials.dataCenter; const organizationId = credentials.organizationId; if (!organizationId || organizationId.trim() === '') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Organization ID is missing', description: 'Please configure Organization ID in your Zoho Inventory OAuth2 credentials. You can find it in Zoho Inventory → Settings → Organization Profile.', httpCode: '400', }); } const baseUrlMap = { 'COM': 'https://www.zohoapis.com/inventory/v1', 'IN': 'https://www.zohoapis.in/inventory/v1', 'EU': 'https://www.zohoapis.eu/inventory/v1', 'AU': 'https://www.zohoapis.com.au/inventory/v1', 'CN': 'https://www.zohoapis.com.cn/inventory/v1', }; const baseUrl = baseUrlMap[dataCenter] || baseUrlMap['COM']; const options = { method, uri: `${baseUrl}${endpoint}`, headers: {}, body, qs: { ...qs, organization_id: organizationId, }, json: true, }; let attempt = 0; while (attempt < RETRY_LIMIT) { try { await checkRateLimit(); const response = await this.helpers.requestOAuth2.call(this, 'zohoInventoryOAuth2', options); if (response && typeof response === 'object' && 'code' in response) { if (response.code !== 0) { throw new n8n_workflow_1.NodeApiError(this.getNode(), createZohoError(response)); } } return response; } catch (error) { if (isRateLimitError(error)) { attempt++; if (attempt < RETRY_LIMIT) { const backoffTime = BACKOFF_BASE * Math.pow(2, attempt - 1); await new Promise((resolve) => setTimeout(resolve, backoffTime)); continue; } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Rate limit exceeded', description: 'Too many requests. Please wait and try again.', httpCode: '429', }); } if (isAuthError(error)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Authorization failed - please check your credentials', description: `Authentication failed. This is most commonly due to data center mismatch. 🌍 CURRENT CONFIGURATION: • Data Center: ${dataCenter} (${baseUrl.replace('/inventory/v1', '')}) • Organization ID: ${organizationId} (${organizationId.match(/^\d+$/) ? '✅ Valid format' : '❌ Invalid format'}) ⚠️ COMMON CAUSES: • Data center mismatch (#1 cause of 401 errors) • Invalid Organization ID format • OAuth2 app configured in wrong data center • Insufficient API permissions ✅ IMMEDIATE FIXES: 1. Verify your Zoho Inventory URL matches your data center setting: If your Zoho Inventory URL is https://inventory.zoho.com, keep COM selected 2. Get exact Organization ID from Zoho Inventory Settings > Organization Profile 3. Ensure OAuth2 app exists in https://api-console.zoho.com 4. Restart n8n if you recently updated the node 🔍 DEBUG INFORMATION: • Request Method: ${method} • Full URL: ${options.uri} • Data Center: ${dataCenter} • Organization ID Header: X-com-zoho-inventory-organizationid: ${organizationId} • Expected OAuth2 Bearer Token: Authorization: Bearer [hidden] 🧪 MANUAL TEST: curl -X ${method} "${options.uri}" \\ -H "Authorization: Bearer [YOUR_ACCESS_TOKEN]" \\ -H "X-com-zoho-inventory-organizationid: ${organizationId}"`, httpCode: '401', }); } if (isNetworkError(error)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Network error occurred while connecting to Zoho Inventory API.', description: 'Please check your internet connection and try again.', httpCode: '0', }); } if (isValidationError(error)) { const errorData = error; const zohoErrorMessage = errorData.response?.data?.message || errorData.message || 'Unknown validation error'; throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Bad request - please check your parameters', description: zohoErrorMessage, httpCode: '400', }); } if (error instanceof n8n_workflow_1.NodeApiError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('rate limit') || errorMessage.includes('Rate limit')) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'The service is receiving too many requests from you', description: 'Please wait and try again.', httpCode: '429', }); } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'The service was not able to process your request', description: errorMessage, }); } } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Zoho Inventory API Error: Rate limited', description: 'Failed to complete the request after multiple attempts. Please try again later.', }); } async function zohoApiRequestWithFormData(method, endpoint, formData = {}, qs = {}) { return zohoApiRequest.call(this, method, endpoint, formData, qs); } async function zohoApiRequestAllItems(method, endpoint, body = {}, query = {}) { const returnData = []; let page = 1; const perPage = 200; do { const qs = { ...query, page, per_page: perPage }; const responseData = await zohoApiRequest.call(this, method, endpoint, body, qs); if (!responseData || typeof responseData !== 'object') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Invalid response format from Zoho API', description: 'Expected object response but received invalid data', }); } let items = []; if (responseData.items) { items = responseData.items; } else if (responseData.composite_items) { items = responseData.composite_items; } else if (responseData.assemblies) { items = responseData.assemblies; } else if (responseData.item_groups) { items = responseData.item_groups; } else if (responseData.warehouses) { items = responseData.warehouses; } else if (Array.isArray(responseData)) { items = responseData; } else { break; } returnData.push(...items); if (items.length < perPage) { break; } page++; } while (true); return returnData; } async function getItemBySku(sku) { if (!sku || sku.trim() === '') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'SKU code is required', description: 'Please provide a valid SKU code to search for items.', httpCode: '400', }); } try { const response = await zohoApiRequest.call(this, 'GET', '/items', {}, { search_text: sku.trim(), per_page: 200 }); if (!response || !response.items || !Array.isArray(response.items)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Invalid response from Zoho Inventory API', description: 'Expected items array in response but received invalid data.', httpCode: '500', }); } const exactMatches = response.items.filter((item) => item.sku && item.sku.toLowerCase() === sku.toLowerCase()); if (exactMatches.length === 0) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Item with SKU '${sku}' not found`, description: 'No item found with the specified SKU code. Please verify the SKU is correct.', httpCode: '404', }); } if (exactMatches.length > 1) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Multiple items found with SKU '${sku}'`, description: `Found ${exactMatches.length} items with the same SKU. Please ensure SKU codes are unique.`, httpCode: '409', }); } return exactMatches[0]; } catch (error) { if (error instanceof n8n_workflow_1.NodeApiError) { throw error; } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Failed to retrieve item by SKU', description: `Error occurred while searching for item with SKU '${sku}': ${error instanceof Error ? error.message : String(error)}`, httpCode: '500', }); } } //# sourceMappingURL=GenericFunctions.js.map