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