UNPKG

syia-mcp-vessel-accounts

Version:

MCP server for vessel account management including EyeShare API integration, vessel expenses, and purchase orders

226 lines 9.7 kB
import axios from 'axios'; import { config } from './config.js'; import { logger } from './logger.js'; import { filterPurchaseOrderPoLines } from './poLinesFilter.js'; class EyeShareApi { constructor() { this.accessToken = null; this.tokenExpiry = 0; this.client = axios.create({ baseURL: config.api.baseUrl, timeout: 30000, headers: { 'Content-Type': 'application/json' } }); // Add request interceptor for logging this.client.interceptors.request.use((config) => { logger.debug('API Request', { method: config.method, url: config.url, data: config.data }); return config; }, (error) => { logger.error('API Request Error', error); return Promise.reject(error); }); // Add response interceptor for logging this.client.interceptors.response.use((response) => { logger.debug('API Response', { status: response.status, url: response.config.url, data: response.data }); return response; }, (error) => { logger.error('API Response Error', { status: error.response?.status, url: error.config?.url, data: error.response?.data }); return Promise.reject(error); }); } async getAccessToken() { // Check if we have a valid token if (this.accessToken && Date.now() < this.tokenExpiry) { return this.accessToken; } try { logger.info('Getting new access token...'); const payload = new URLSearchParams({ scope: 'api', client_id: config.api.clientId, client_secret: decodeURIComponent(config.api.clientSecret), grant_type: 'client_credentials' }); const response = await this.client.post('/auth/connect/token', payload, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); this.accessToken = response.data.access_token; // Set expiry to 50 minutes from now (token expires in 1 hour, but refresh 10 minutes early) this.tokenExpiry = Date.now() + (response.data.expires_in - 600) * 1000; logger.info('Access token obtained successfully'); return this.accessToken; } catch (error) { logger.error('Failed to get access token', error); throw new Error('Authentication failed'); } } async getAuthHeaders() { const token = await this.getAccessToken(); return { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; } async getVessels() { try { const headers = await this.getAuthHeaders(); const response = await this.client.get('/api/system/config/allclientcompany', { headers }); // Extract vessels from the hierarchical company structure const vessels = []; const extractVessels = (company) => { // If this company has children, recursively process them if (company.children && company.children.length > 0) { company.children.forEach(extractVessels); } // If this is a vessel (has a code and name, and is not a system/virtual company) if (company.code && company.name && company.CompanyType !== 'System' && company.CompanyType !== 'Virtual') { vessels.push({ code: company.code, name: company.name, id: company.Id, organizationNumber: company.organizationNumber, companyType: company.CompanyType, parent: company.Parent, implemented: company.implemented, customerRootCompany: company.customerRootCompany }); } }; // Process the root company and all its children extractVessels(response.data); return vessels; } catch (error) { logger.error('Failed to get vessels', error); throw error; } } async searchInvoices(searchRequest) { try { const headers = await this.getAuthHeaders(); const response = await this.client.post(`/api/search?companyCode=${config.api.companyCode}&module=${config.api.module}`, searchRequest, { headers }); return response.data; } catch (error) { logger.error('Failed to search invoices', error); throw error; } } async getAttachment(attachmentId, documentId, version = 0) { try { const headers = await this.getAuthHeaders(); const response = await this.client.get(`/api/attachments/${attachmentId}/${documentId}/${version}?Module=${config.api.module}&CompanyCode=${config.api.companyCode}&s=${config.api.companyCode}`, { headers, responseType: 'arraybuffer' }); return Buffer.from(response.data); } catch (error) { logger.error('Failed to get attachment', error); throw error; } } async getPurchaseOrderByInvoice(invoiceId, vesselCode) { try { const headers = await this.getAuthHeaders(); const response = await this.client.get(`/api/entity/${invoiceId}?module=purchaseorder&companyCode=${vesselCode}`, { headers }); // Filter PoLines to include only specified fields const filteredData = filterPurchaseOrderPoLines(response.data); return filteredData; } catch (error) { logger.error('Failed to get purchase order by invoice', error); throw error; } } async getPurchaseOrderByInvoiceSingle(invoiceId, vesselCode) { try { const headers = await this.getAuthHeaders(); const response = await this.client.get(`/api/entity/${invoiceId}?module=purchaseorder&companyCode=${vesselCode}`, { headers }); // Filter PoLines to include only specified fields const filteredData = filterPurchaseOrderPoLines(response.data); // Extract only PurchaseOrders array const purchaseOrders = filteredData?.PurchaseOrders || []; logger.info(`Invoice ${invoiceId}: Found ${purchaseOrders.length} purchase orders`); return { invoiceId, purchaseOrders, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error(`Failed to get purchase order for invoice ${invoiceId}:`, error); return { invoiceId, purchaseOrders: [], error: errorMessage }; } } async getPurchaseOrdersByInvoicesParallel(invoiceConfigs, maxWorkers = 5) { const startTime = Date.now(); const results = []; const errors = []; logger.info(`Starting parallel fetch for ${invoiceConfigs.length} invoices with ${maxWorkers} workers...`); // Process in batches to respect rate limits const batchSize = maxWorkers; const batches = []; for (let i = 0; i < invoiceConfigs.length; i += batchSize) { batches.push(invoiceConfigs.slice(i, i + batchSize)); } for (const batch of batches) { const batchPromises = batch.map(config => this.getPurchaseOrderByInvoiceSingle(config.invoiceId, config.vesselCode)); const batchResults = await Promise.all(batchPromises); batchResults.forEach((result, index) => { if (result.error) { errors.push({ invoiceId: result.invoiceId, error: result.error }); } else { results.push({ invoiceId: result.invoiceId, vesselCode: batch[index].vesselCode, // Add vesselCode from original config purchaseOrders: result.purchaseOrders }); } }); // Add small delay between batches to be respectful to the API if (batches.indexOf(batch) < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 100)); } } const endTime = Date.now(); const executionTimeSeconds = (endTime - startTime) / 1000; const totalPurchaseOrders = results.reduce((sum, result) => sum + result.purchaseOrders.length, 0); const summary = { totalInvoices: invoiceConfigs.length, successfulInvoices: results.length, failedInvoices: errors.length, totalPurchaseOrders, executionTimeSeconds: Math.round(executionTimeSeconds * 100) / 100, averageTimePerInvoice: Math.round((executionTimeSeconds / invoiceConfigs.length) * 100) / 100 }; logger.info('Parallel fetch completed', summary); return { results, errors, summary }; } } export const eyeShareApi = new EyeShareApi(); //# sourceMappingURL=api.js.map