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
JavaScript
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