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

413 lines (407 loc) 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.makeApiRequestAllItems = exports.makeApiRequest = void 0; exports.checkRateLimit = checkRateLimit; exports.zohoApiRequest = zohoApiRequest; exports.zohoApiRequestWithFormData = zohoApiRequestWithFormData; exports.zohoApiRequestAllItems = zohoApiRequestAllItems; exports.getItemBySku = getItemBySku; exports.handleExecutionError = handleExecutionError; 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('zohoBooksOAuth2'); 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 Books OAuth2 credentials. You can find it in Zoho Books → Settings → Organization Profile.', httpCode: '400', }); } const baseUrlMap = { 'COM': 'https://www.zohoapis.com/books/v3', 'IN': 'https://www.zohoapis.in/books/v3', 'EU': 'https://www.zohoapis.eu/books/v3', 'AU': 'https://www.zohoapis.com.au/books/v3', 'CN': 'https://www.zohoapis.com.cn/books/v3', }; const baseUrl = baseUrlMap[dataCenter] || baseUrlMap['COM']; const options = { method, uri: `${baseUrl}${endpoint}`, headers: { 'X-com-zoho-books-organizationid': organizationId, }, qs: { ...qs, organization_id: organizationId, }, json: true, }; if (Object.keys(body).length) { const isJSONStringOnly = Object.keys(body).length === 1 && body.JSONString !== undefined; const isMultipartUpload = Object.values(body).some((val) => val && typeof val === 'object' && 'value' in val && (Buffer.isBuffer(val.value) || typeof val.value !== 'undefined')); if (isJSONStringOnly) { options.form = body; } else if (isMultipartUpload) { options.formData = body; delete options.body; } else { options.body = body; } } let attempt = 0; while (attempt < RETRY_LIMIT) { try { await checkRateLimit(); const response = await this.helpers.requestOAuth2.call(this, 'zohoBooksOAuth2', options); const parsed = (typeof response === 'string') ? (() => { try { return JSON.parse(response); } catch { return response; } })() : response; if (parsed && typeof parsed === 'object' && 'code' in parsed) { if (parsed.code !== 0) { throw new n8n_workflow_1.NodeApiError(this.getNode(), createZohoError(parsed)); } } return parsed; } 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; } const errorData = error; const retryAfter = errorData.response?.headers?.['retry-after'] || errorData.response?.headers?.['Retry-After']; const rateLimitLimit = errorData.response?.headers?.['x-ratelimit-limit'] || errorData.response?.headers?.['X-RateLimit-Limit']; const rateLimitRemaining = errorData.response?.headers?.['x-ratelimit-remaining'] || errorData.response?.headers?.['X-RateLimit-Remaining']; let description = 'Too many requests. Please wait and try again.'; if (retryAfter || rateLimitLimit || rateLimitRemaining) { description += '\n\n📊 RATE LIMIT DETAILS:'; if (retryAfter) description += `\n• Retry After: ${retryAfter} seconds`; if (rateLimitLimit) description += `\n• Rate Limit: ${rateLimitLimit} requests`; if (rateLimitRemaining) description += `\n• Remaining: ${rateLimitRemaining} requests`; description += '\n\n💡 TIP: Consider adding a "Wait" node or workflow scheduling to avoid hitting rate limits.'; } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Rate limit exceeded', description, 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('/books/v3', '')}) • 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 Books URL matches your data center setting: If your Zoho Books URL is https://books.zoho.com, keep COM selected 2. Get exact Organization ID from Zoho Books 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-books-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-books-organizationid: ${organizationId}" 📚 For detailed troubleshooting: See troubleshoot-auth.md in the node package`, httpCode: '401', }); } if (isNetworkError(error)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Network error occurred while connecting to Zoho Books 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'; const zohoErrorCode = errorData.response?.data?.code; if (zohoErrorCode === 110701) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Reason required for sent invoice update', description: 'Please provide a reason when updating a sent invoice. Add the reason in the Update Fields section.', httpCode: '400', }); } 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 Books 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.salesorders) { items = responseData.salesorders; } else if (responseData.invoices) { items = responseData.invoices; } else if (responseData.items) { items = responseData.items; } else if (responseData.contacts) { items = responseData.contacts; } else if (responseData.vendors) { items = responseData.vendors; } else if (responseData.creditnotes) { items = responseData.creditnotes; } else if (responseData.bills) { items = responseData.bills; } else if (responseData.purchaseorders) { items = responseData.purchaseorders; } else if (responseData.composite_items) { items = responseData.composite_items; } else if (responseData.taxes) { items = responseData.taxes; } else if (responseData.taxgroups) { items = responseData.taxgroups; } else if (responseData.tax_exemptions) { items = responseData.tax_exemptions; } else if (responseData.taxexemptions) { items = responseData.taxexemptions; } 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 Books 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', }); } } function handleExecutionError(node, error, itemIndex, continueOnFail) { const errorMessage = error?.message || error?.toString() || 'Unknown error occurred'; const statusCode = error?.statusCode || error?.response?.status || 500; if (continueOnFail) { return [{ json: { error: errorMessage, statusCode, itemIndex, }, pairedItem: { item: itemIndex }, }]; } else { const { NodeOperationError } = require('n8n-workflow'); const nodeError = new NodeOperationError(node, errorMessage, { cause: error }); nodeError.itemIndex = itemIndex; throw nodeError; } } exports.makeApiRequest = zohoApiRequest; exports.makeApiRequestAllItems = zohoApiRequestAllItems; //# sourceMappingURL=GenericFunctions.js.map