UNPKG

purchase-mcp-server

Version:

Purchase and budget management server handling requisitions, purchase orders, expenses, budgets, and vendor management with ERP access for data extraction

294 lines 16.9 kB
import { UniversalPurchaseSearchKeys, UniversalBudgetSearchKeys, UniversalExpenseSearchKeys } from "../../types/index.js"; import { getTypesenseClient, processTypesenseResults } from "syia-mcp-utils"; import { logger } from "../../index.js"; export class UniversalToolHandler { constructor() { this.typesenseClient = getTypesenseClient(); } async universalPurchaseSearchHandler(arguments_) { const collection = "purchase"; const session_id = arguments_.session_id || "testing"; // Validate that only allowed properties are present const providedProperties = Object.keys(arguments_); const invalidProperties = providedProperties.filter(prop => !UniversalPurchaseSearchKeys.includes(prop)); if (invalidProperties.length > 0) { throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalPurchaseSearchKeys.join(', ')}`); } // Extract new schema parameters const args = arguments_; const query_by = args.query_by; const query_text = (args.q || "").trim() || "*"; // Validate that query_by is provided when q is specified if (args.q && args.q.trim() !== "*" && !query_by) { throw new Error("query_by parameter is required when using full-text search with q parameter"); } const filter_by = args.filter_by || ""; const sort_by = args.sort_by || "relevance"; const page = args.page || 1; const per_page = args.per_page || 50; try { let filterBy = filter_by; // Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by if (filterBy) { const dateFields = ["date", "purchaseRequisitionDate", "purchaseOrderIssuedDate", "orderReadinessDate"]; for (const dateField of dateFields) { // Handle date range operations (>=, <=, >, <, =) const dateRegexPatterns = [ { pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' }, { pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' }, { pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' }, { pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' }, { pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' } ]; for (const { pattern, operator } of dateRegexPatterns) { filterBy = filterBy.replace(pattern, (match, dateStr) => { try { const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000); return `${dateField}${operator}${timestamp}`; } catch (error) { logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error); return match; // Keep original if conversion fails } }); } } } // Apply company IMO restrictions // filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || ""); // Set up all available fields for inclusion (based on schema) const includeFields = "vesselId,imo,vesselName,purchaseRequisitionDate,prDescription," + "purchaseRequisitionStatus,poValue,purchaseOrderIssuedDate,vendorOrSupplierName," + "accountCode,invoiceStatus,invoiceValue,prStatusColor,orderReadinessDate," + "orderReadinessColor,purchaseRequisitionNumber,purchaseRequisitionLink," + "purchaseOrderNumber,purchaseOrderLink,prType,prTypeColor,invoiceStatusColor," + "scanID,scanIDLink,orderPriority,currencyCode,purchaseOrderStatus,poCreatedBy," + "orderType,invoiceAmount,invoiceApproverName,qtcNo,qtcLink,forwarderName," + "forwarderRemarks,warehouseLocation,cargoType,weight,purchaseOrderAmount," + "purchaseRequisitionType,directPo,poInvoiceDiscrepency,purchaseRequisitionSummary," + "poStatusColor,purchaseOrderStage,purchaseRequisitionDescription," + "isNewPurchaseRequisition,isPurchaseRequisitionNotOrdered," + "isPurchaseRequisitionSupplied,isPurchaseOrderReady,purchaseOverviewUpdate," + "docId,fleetManagerId,technicalSuperintendentId,ownerId"; const excludeFields = "embedding"; const query = { q: query_text, query_by: query_by, include_fields: includeFields, exclude_fields: excludeFields, page: page, per_page: per_page }; if (filterBy) { query.filter_by = filterBy; } // Handle sorting if (sort_by && sort_by !== "relevance") { query.sort_by = sort_by; } logger.debug(`[Typesense Query] ${JSON.stringify(query)}`); const results = await this.typesenseClient.collections(collection).documents().search(query); // Debug: Log the structure of the search results logger.info(`UniversalPurchaseSearchHandler - Results structure: ${JSON.stringify(Object.keys(results))}`); if (results.hits && results.hits.length > 0) { const sampleHit = results.hits[0]; logger.info(`UniversalPurchaseSearchHandler - Sample hit structure: ${JSON.stringify(Object.keys(sampleHit))}`); } if (!results || !results.hits || results.hits.length === 0) { return [{ type: "text", text: `No purchase records found for query '${query_text}' with query_by fields '${query_by}'.`, title: "No Results Found", format: "json" }]; } // Format results using the utility function let title = `Universal Purchase Search Results for '${query_text}'`; const linkHeader = `Universal purchase search result for query: '${query_text}' in fields: ${query_by}`; return await processTypesenseResults(results, "universal_purchase_search", title, session_id, linkHeader); } catch (error) { logger.error('Error performing universal purchase search:', error); throw new Error(`Error performing search: ${error.message}`); } } async universalBudgetSearchHandler(arguments_) { const collection = "budget"; const session_id = arguments_.session_id || "testing"; // Validate that only allowed properties are present const providedProperties = Object.keys(arguments_); const invalidProperties = providedProperties.filter(prop => !UniversalBudgetSearchKeys.includes(prop)); if (invalidProperties.length > 0) { throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalBudgetSearchKeys.join(', ')}`); } // Extract new schema parameters const args = arguments_; const query_by = args.query_by; const query_text = (args.q || "").trim() || "*"; // Validate that query_by is provided when q is specified if (args.q && args.q.trim() !== "*" && !query_by) { throw new Error("query_by parameter is required when using full-text search with q parameter"); } const filter_by = args.filter_by || ""; const sort_by = args.sort_by || "relevance"; const page = args.page || 1; const per_page = args.per_page || 50; try { let filterBy = filter_by; // Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by if (filterBy) { const dateFields = ["date"]; for (const dateField of dateFields) { // Handle date range operations (>=, <=, >, <, =) const dateRegexPatterns = [ { pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' }, { pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' }, { pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' }, { pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' }, { pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' } ]; for (const { pattern, operator } of dateRegexPatterns) { filterBy = filterBy.replace(pattern, (match, dateStr) => { try { const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000); return `${dateField}${operator}${timestamp}`; } catch (error) { logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error); return match; // Keep original if conversion fails } }); } } } // // Apply company IMO restrictions // filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || ""); // Set up all available fields for inclusion (based on schema) const includeFields = "imo,vesselName,category,group,period,budgetAmount,expenseAmount,date"; const excludeFields = "embedding"; const query = { q: query_text, query_by: query_by, include_fields: includeFields, exclude_fields: excludeFields, page: page, per_page: per_page }; if (filterBy) { query.filter_by = filterBy; } // Handle sorting if (sort_by && sort_by !== "relevance") { query.sort_by = sort_by; } logger.debug(`[Typesense Query] ${JSON.stringify(query)}`); const results = await this.typesenseClient.collections(collection).documents().search(query); if (!results || !results.hits || results.hits.length === 0) { return [{ type: "text", text: `No budget records found for query '${query_text}' with query_by fields '${query_by}'.`, title: "No Results Found", format: "json" }]; } // Format results using the utility function let title = `Universal Budget Search Results for '${query_text}'`; const linkHeader = `Universal budget search result for query: '${query_text}' in fields: ${query_by}`; return await processTypesenseResults(results, "universal_budget_search", title, session_id, linkHeader); } catch (error) { logger.error('Error performing universal budget search:', error); throw new Error(`Error performing search: ${error.message}`); } } async universalExpenseSearchHandler(arguments_) { const collection = "expense"; const session_id = arguments_.session_id || "testing"; // Validate that only allowed properties are present const providedProperties = Object.keys(arguments_); const invalidProperties = providedProperties.filter(prop => !UniversalExpenseSearchKeys.includes(prop)); if (invalidProperties.length > 0) { throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalExpenseSearchKeys.join(', ')}`); } // Extract new schema parameters const args = arguments_; const query_by = args.query_by; const query_text = (args.q || "").trim() || "*"; // Validate that query_by is provided when q is specified if (args.q && args.q.trim() !== "*" && !query_by) { throw new Error("query_by parameter is required when using full-text search with q parameter"); } const filter_by = args.filter_by || ""; const sort_by = args.sort_by || "relevance"; const page = args.page || 1; const per_page = args.per_page || 50; try { let filterBy = filter_by; // Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by if (filterBy) { const dateFields = ["expenseDate", "poDate"]; for (const dateField of dateFields) { // Handle date range operations (>=, <=, >, <, =) const dateRegexPatterns = [ { pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' }, { pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' }, { pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' }, { pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' }, { pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' } ]; for (const { pattern, operator } of dateRegexPatterns) { filterBy = filterBy.replace(pattern, (match, dateStr) => { try { const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000); return `${dateField}${operator}${timestamp}`; } catch (error) { logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error); return match; // Keep original if conversion fails } }); } } } // Apply company IMO restrictions // filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || ""); // Set up all available fields for inclusion (based on schema) const includeFields = "imo,vesselName,group,category,accountNo,accountDescription,expenseDate,expenseAmount,poAmount,expenseCategory"; const excludeFields = "embedding"; const query = { q: query_text, query_by: query_by, include_fields: includeFields, exclude_fields: excludeFields, page: page, per_page: per_page }; if (filterBy) { query.filter_by = filterBy; } // Handle sorting if (sort_by && sort_by !== "relevance") { query.sort_by = sort_by; } logger.debug(`[Typesense Query] ${JSON.stringify(query)}`); const results = await this.typesenseClient.collections(collection).documents().search(query); if (!results || !results.hits || results.hits.length === 0) { return [{ type: "text", text: `No expense records found for query '${query_text}' with query_by fields '${query_by}'.`, title: "No Results Found", format: "json" }]; } // Format results using the utility function let title = `Universal Expense Search Results for '${query_text}'`; const linkHeader = `Universal expense search result for query: '${query_text}' in fields: ${query_by}`; return await processTypesenseResults(results, "universal_expense_search", title, session_id, linkHeader); } catch (error) { logger.error('Error performing universal expense search:', error); throw new Error(`Error performing search: ${error.message}`); } } } //# sourceMappingURL=universalTools.js.map