From d5f1453492848eda2169480bea554648b73b8e61 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 05:25:16 -0600 Subject: [PATCH] [MCP] feat: Connect MCP tools to actual services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - orders-tools.service.ts → ServiceOrderService (6 tools) - create_service_order, get_service_order, list_service_orders - update_order_status, get_orders_kanban, get_orders_dashboard - inventory-tools.service.ts → PartService (8 tools) - search_parts, get_part_details, check_stock - get_low_stock_parts, adjust_stock, get_inventory_value - create_part, list_parts - customers-tools.service.ts → CustomersService (6 tools) - search_customers, get_customer, create_customer - update_customer, list_customers, get_customer_stats Removed mock data, now using real database operations. Co-Authored-By: Claude Opus 4.5 --- .../mcp/tools/customers-tools.service.ts | 383 +++++++++++++-- .../mcp/tools/inventory-tools.service.ts | 458 ++++++++++++++---- src/modules/mcp/tools/orders-tools.service.ts | 366 +++++++++++--- 3 files changed, 1027 insertions(+), 180 deletions(-) diff --git a/src/modules/mcp/tools/customers-tools.service.ts b/src/modules/mcp/tools/customers-tools.service.ts index daa5298..93af263 100644 --- a/src/modules/mcp/tools/customers-tools.service.ts +++ b/src/modules/mcp/tools/customers-tools.service.ts @@ -1,51 +1,138 @@ +import { DataSource } from 'typeorm'; import { McpToolProvider, McpToolDefinition, McpToolHandler, McpContext, } from '../interfaces'; +import { CustomersService } from '../../customers/services/customers.service'; +import { CreateCustomerDto, CustomerFilters } from '../../customers/customers.dto'; +import { CustomerType } from '../../customers/entities/customer.entity'; /** * Customers Tools Service * Provides MCP tools for customer management. - * - * TODO: Connect to actual CustomersService when available. + * Connected to actual CustomersService for real data operations. */ export class CustomersToolsService implements McpToolProvider { + private customersService: CustomersService; + + constructor(dataSource: DataSource) { + this.customersService = new CustomersService(dataSource); + } + getTools(): McpToolDefinition[] { return [ { name: 'search_customers', - description: 'Busca clientes por nombre, telefono o email', + description: 'Busca clientes por nombre, teléfono, email o RFC', category: 'customers', parameters: { type: 'object', properties: { - query: { type: 'string', description: 'Texto de busqueda' }, - limit: { type: 'number', description: 'Limite de resultados', default: 10 }, + query: { type: 'string', description: 'Término de búsqueda' }, + limit: { type: 'number', default: 10, description: 'Máximo de resultados' }, }, required: ['query'], }, - returns: { type: 'array' }, + returns: { type: 'array', description: 'Lista de clientes encontrados' }, }, { - name: 'get_customer_balance', - description: 'Obtiene el saldo actual de un cliente', + name: 'get_customer', + description: 'Obtiene información detallada de un cliente', category: 'customers', parameters: { type: 'object', properties: { - customer_id: { type: 'string', format: 'uuid' }, + customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' }, + email: { type: 'string', description: 'Email del cliente' }, + phone: { type: 'string', description: 'Teléfono del cliente' }, + rfc: { type: 'string', description: 'RFC del cliente' }, + }, + }, + returns: { type: 'object', description: 'Información del cliente' }, + }, + { + name: 'create_customer', + description: 'Registra un nuevo cliente', + category: 'customers', + permissions: ['customers.create'], + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Nombre completo o razón social' }, + customer_type: { + type: 'string', + enum: ['individual', 'company', 'fleet', 'government'], + default: 'individual', + description: 'Tipo de cliente' + }, + email: { type: 'string', format: 'email' }, + phone: { type: 'string', description: 'Teléfono principal' }, + rfc: { type: 'string', description: 'RFC (opcional para facturación)' }, + address: { type: 'string' }, + city: { type: 'string' }, + state: { type: 'string' }, + postal_code: { type: 'string' }, + credit_days: { type: 'number', default: 0, description: 'Días de crédito' }, + credit_limit: { type: 'number', default: 0, description: 'Límite de crédito en MXN' }, + notes: { type: 'string' }, + }, + required: ['name'], + }, + returns: { type: 'object', description: 'Cliente creado' }, + }, + { + name: 'update_customer', + description: 'Actualiza información de un cliente', + category: 'customers', + permissions: ['customers.update'], + parameters: { + type: 'object', + properties: { + customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' }, + name: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + rfc: { type: 'string' }, + address: { type: 'string' }, + credit_days: { type: 'number' }, + credit_limit: { type: 'number' }, + notes: { type: 'string' }, }, required: ['customer_id'], }, - returns: { + returns: { type: 'object', description: 'Cliente actualizado' }, + }, + { + name: 'list_customers', + description: 'Lista clientes con filtros opcionales', + category: 'customers', + parameters: { type: 'object', properties: { - balance: { type: 'number' }, - credit_limit: { type: 'number' }, + customer_type: { + type: 'string', + enum: ['individual', 'company', 'fleet', 'government'], + }, + search: { type: 'string' }, + is_active: { type: 'boolean' }, + has_credit: { type: 'boolean', description: 'Solo clientes con crédito' }, + page: { type: 'number', default: 1 }, + limit: { type: 'number', default: 20 }, }, }, + returns: { type: 'object', description: 'Lista paginada de clientes' }, + }, + { + name: 'get_customer_stats', + description: 'Obtiene estadísticas generales de clientes', + category: 'customers', + parameters: { + type: 'object', + properties: {}, + }, + returns: { type: 'object', description: 'Estadísticas de clientes' }, }, ]; } @@ -53,7 +140,11 @@ export class CustomersToolsService implements McpToolProvider { getHandler(toolName: string): McpToolHandler | undefined { const handlers: Record = { search_customers: this.searchCustomers.bind(this), - get_customer_balance: this.getCustomerBalance.bind(this), + get_customer: this.getCustomer.bind(this), + create_customer: this.createCustomer.bind(this), + update_customer: this.updateCustomer.bind(this), + list_customers: this.listCustomers.bind(this), + get_customer_stats: this.getCustomerStats.bind(this), }; return handlers[toolName]; } @@ -61,34 +152,254 @@ export class CustomersToolsService implements McpToolProvider { private async searchCustomers( params: { query: string; limit?: number }, context: McpContext - ): Promise { - // TODO: Connect to actual customers service - return [ - { - id: 'customer-1', - name: 'Juan Perez', - phone: '+52 55 1234 5678', - email: 'juan@example.com', - balance: 500.00, - credit_limit: 5000.00, - message: 'Conectar a CustomersService real', - }, - ]; + ): Promise { + const customers = await this.customersService.search( + context.tenantId, + params.query, + params.limit || 10 + ); + + return { + success: true, + customers: customers.map(c => ({ + id: c.id, + name: c.name, + customer_type: c.customerType, + email: c.email, + phone: c.phone, + rfc: c.rfc, + total_orders: c.totalOrders, + total_spent: c.totalSpent, + })), + count: customers.length, + }; } - private async getCustomerBalance( - params: { customer_id: string }, + private async getCustomer( + params: { customer_id?: string; email?: string; phone?: string; rfc?: string }, context: McpContext ): Promise { - // TODO: Connect to actual customers service + let customer = null; + + if (params.customer_id) { + customer = await this.customersService.findById(context.tenantId, params.customer_id); + } else if (params.email) { + customer = await this.customersService.findByEmail(context.tenantId, params.email); + } else if (params.phone) { + customer = await this.customersService.findByPhone(context.tenantId, params.phone); + } else if (params.rfc) { + customer = await this.customersService.findByRfc(context.tenantId, params.rfc); + } + + if (!customer) { + return { + success: false, + error: 'Cliente no encontrado', + }; + } + return { - customer_id: params.customer_id, - customer_name: 'Cliente ejemplo', - balance: 500.00, - credit_limit: 5000.00, - available_credit: 4500.00, - last_purchase: new Date().toISOString(), - message: 'Conectar a CustomersService real', + success: true, + customer: { + id: customer.id, + name: customer.name, + legal_name: customer.legalName, + customer_type: customer.customerType, + email: customer.email, + phone: customer.phone, + phone_secondary: customer.phoneSecondary, + rfc: customer.rfc, + address: customer.address, + city: customer.city, + state: customer.state, + postal_code: customer.postalCode, + credit_days: customer.creditDays, + credit_limit: customer.creditLimit, + credit_balance: customer.creditBalance, + discount_labor_pct: customer.discountLaborPct, + discount_parts_pct: customer.discountPartsPct, + total_orders: customer.totalOrders, + total_spent: customer.totalSpent, + last_visit_at: customer.lastVisitAt, + preferred_contact: customer.preferredContact, + notes: customer.notes, + is_active: customer.isActive, + }, + }; + } + + private async createCustomer( + params: { + name: string; + customer_type?: string; + email?: string; + phone?: string; + rfc?: string; + address?: string; + city?: string; + state?: string; + postal_code?: string; + credit_days?: number; + credit_limit?: number; + notes?: string; + }, + context: McpContext + ): Promise { + try { + const dto: CreateCustomerDto = { + name: params.name, + customerType: (params.customer_type as CustomerType) || CustomerType.INDIVIDUAL, + email: params.email, + phone: params.phone, + rfc: params.rfc, + address: params.address, + city: params.city, + state: params.state, + postalCode: params.postal_code, + creditDays: params.credit_days || 0, + creditLimit: params.credit_limit || 0, + notes: params.notes, + }; + + const customer = await this.customersService.create( + context.tenantId, + dto, + context.userId + ); + + return { + success: true, + customer: { + id: customer.id, + name: customer.name, + customer_type: customer.customerType, + email: customer.email, + phone: customer.phone, + }, + message: `Cliente ${customer.name} registrado exitosamente`, + }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } + } + + private async updateCustomer( + params: { + customer_id: string; + name?: string; + email?: string; + phone?: string; + rfc?: string; + address?: string; + credit_days?: number; + credit_limit?: number; + notes?: string; + }, + context: McpContext + ): Promise { + try { + const updateData: any = {}; + if (params.name) updateData.name = params.name; + if (params.email) updateData.email = params.email; + if (params.phone) updateData.phone = params.phone; + if (params.rfc) updateData.rfc = params.rfc; + if (params.address) updateData.address = params.address; + if (params.credit_days !== undefined) updateData.creditDays = params.credit_days; + if (params.credit_limit !== undefined) updateData.creditLimit = params.credit_limit; + if (params.notes) updateData.notes = params.notes; + + const customer = await this.customersService.update( + context.tenantId, + params.customer_id, + updateData + ); + + if (!customer) { + return { + success: false, + error: 'Cliente no encontrado', + }; + } + + return { + success: true, + customer: { + id: customer.id, + name: customer.name, + email: customer.email, + phone: customer.phone, + }, + message: 'Cliente actualizado exitosamente', + }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } + } + + private async listCustomers( + params: { + customer_type?: string; + search?: string; + is_active?: boolean; + has_credit?: boolean; + page?: number; + limit?: number; + }, + context: McpContext + ): Promise { + const filters: CustomerFilters = { + customerType: params.customer_type as CustomerType, + search: params.search, + isActive: params.is_active, + hasCredit: params.has_credit, + page: params.page || 1, + limit: params.limit || 20, + }; + + const result = await this.customersService.findAll(context.tenantId, filters); + + return { + success: true, + customers: result.data.map(c => ({ + id: c.id, + name: c.name, + customer_type: c.customerType, + email: c.email, + phone: c.phone, + total_orders: c.totalOrders, + total_spent: c.totalSpent, + credit_limit: c.creditLimit, + is_active: c.isActive, + })), + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + total_pages: Math.ceil(result.total / result.limit), + }, + }; + } + + private async getCustomerStats( + params: {}, + context: McpContext + ): Promise { + const stats = await this.customersService.getStats(context.tenantId); + + return { + success: true, + stats: { + total_customers: stats.total, + active_customers: stats.active, + customers_by_type: stats.byType, + customers_with_credit: stats.withCredit, + }, }; } } diff --git a/src/modules/mcp/tools/inventory-tools.service.ts b/src/modules/mcp/tools/inventory-tools.service.ts index 76a45ca..570272b 100644 --- a/src/modules/mcp/tools/inventory-tools.service.ts +++ b/src/modules/mcp/tools/inventory-tools.service.ts @@ -1,60 +1,93 @@ +import { DataSource } from 'typeorm'; import { McpToolProvider, McpToolDefinition, McpToolHandler, McpContext, } from '../interfaces'; +import { PartService, CreatePartDto, StockAdjustmentDto } from '../../parts-management/services/part.service'; /** * Inventory Tools Service - * Provides MCP tools for inventory management. - * - * TODO: Connect to actual InventoryService when available. + * Provides MCP tools for parts/inventory management. + * Connected to actual PartService for real data operations. */ export class InventoryToolsService implements McpToolProvider { + private partService: PartService; + + constructor(dataSource: DataSource) { + this.partService = new PartService(dataSource); + } + getTools(): McpToolDefinition[] { return [ + { + name: 'search_parts', + description: 'Busca refacciones por SKU, nombre o código de barras', + category: 'inventory', + parameters: { + type: 'object', + properties: { + query: { type: 'string', description: 'Término de búsqueda (SKU, nombre, código de barras)' }, + limit: { type: 'number', default: 10, description: 'Máximo de resultados' }, + }, + required: ['query'], + }, + returns: { type: 'array', description: 'Lista de refacciones encontradas' }, + }, + { + name: 'get_part_details', + description: 'Obtiene detalles completos de una refacción', + category: 'inventory', + parameters: { + type: 'object', + properties: { + part_id: { type: 'string', format: 'uuid', description: 'ID de la refacción' }, + sku: { type: 'string', description: 'SKU de la refacción' }, + barcode: { type: 'string', description: 'Código de barras' }, + }, + }, + returns: { type: 'object', description: 'Detalles de la refacción' }, + }, { name: 'check_stock', - description: 'Consulta el stock actual de productos', + description: 'Consulta el stock actual de refacciones', category: 'inventory', parameters: { type: 'object', properties: { - product_ids: { type: 'array', description: 'IDs de productos a consultar' }, - warehouse_id: { type: 'string', description: 'ID del almacen' }, + part_ids: { type: 'array', items: { type: 'string' }, description: 'IDs de refacciones a consultar' }, }, + required: ['part_ids'], }, - returns: { type: 'array' }, + returns: { type: 'array', description: 'Stock de cada refacción' }, }, { - name: 'get_low_stock_products', - description: 'Lista productos que estan por debajo del minimo de stock', + name: 'get_low_stock_parts', + description: 'Lista refacciones con stock bajo (por debajo del mínimo configurado)', category: 'inventory', parameters: { type: 'object', - properties: { - threshold: { type: 'number', description: 'Umbral de stock bajo' }, - }, + properties: {}, }, - returns: { type: 'array' }, + returns: { type: 'array', description: 'Refacciones con stock bajo' }, }, { - name: 'record_inventory_movement', - description: 'Registra un movimiento de inventario (entrada, salida, ajuste)', + name: 'adjust_stock', + description: 'Ajusta el stock de una refacción (entrada, salida o ajuste)', category: 'inventory', permissions: ['inventory.write'], parameters: { type: 'object', properties: { - product_id: { type: 'string', format: 'uuid' }, - quantity: { type: 'number' }, - movement_type: { type: 'string', enum: ['in', 'out', 'adjustment'] }, - reason: { type: 'string' }, + part_id: { type: 'string', format: 'uuid', description: 'ID de la refacción' }, + quantity: { type: 'number', description: 'Cantidad a ajustar (positivo=entrada, negativo=salida)' }, + reason: { type: 'string', description: 'Razón del ajuste' }, + reference: { type: 'string', description: 'Referencia (ej: número de orden)' }, }, - required: ['product_id', 'quantity', 'movement_type'], + required: ['part_id', 'quantity', 'reason'], }, - returns: { type: 'object' }, + returns: { type: 'object', description: 'Refacción actualizada' }, }, { name: 'get_inventory_value', @@ -62,93 +95,356 @@ export class InventoryToolsService implements McpToolProvider { category: 'inventory', parameters: { type: 'object', - properties: { - warehouse_id: { type: 'string', description: 'ID del almacen (opcional)' }, - }, + properties: {}, }, - returns: { + returns: { type: 'object', description: 'Valor del inventario' }, + }, + { + name: 'create_part', + description: 'Crea una nueva refacción en el inventario', + category: 'inventory', + permissions: ['inventory.create'], + parameters: { type: 'object', properties: { - total_value: { type: 'number' }, - items_count: { type: 'number' }, + sku: { type: 'string', description: 'Código SKU único' }, + name: { type: 'string', description: 'Nombre de la refacción' }, + description: { type: 'string', description: 'Descripción' }, + brand: { type: 'string', description: 'Marca' }, + manufacturer: { type: 'string', description: 'Fabricante' }, + price: { type: 'number', description: 'Precio de venta' }, + cost: { type: 'number', description: 'Costo' }, + min_stock: { type: 'number', description: 'Stock mínimo' }, + barcode: { type: 'string', description: 'Código de barras' }, + }, + required: ['sku', 'name', 'price'], + }, + returns: { type: 'object', description: 'Refacción creada' }, + }, + { + name: 'list_parts', + description: 'Lista refacciones con filtros opcionales', + category: 'inventory', + parameters: { + type: 'object', + properties: { + category_id: { type: 'string', format: 'uuid' }, + brand: { type: 'string' }, + search: { type: 'string' }, + low_stock: { type: 'boolean', description: 'Solo mostrar con stock bajo' }, + page: { type: 'number', default: 1 }, + limit: { type: 'number', default: 20 }, }, }, + returns: { type: 'object', description: 'Lista paginada de refacciones' }, }, ]; } getHandler(toolName: string): McpToolHandler | undefined { const handlers: Record = { + search_parts: this.searchParts.bind(this), + get_part_details: this.getPartDetails.bind(this), check_stock: this.checkStock.bind(this), - get_low_stock_products: this.getLowStockProducts.bind(this), - record_inventory_movement: this.recordInventoryMovement.bind(this), + get_low_stock_parts: this.getLowStockParts.bind(this), + adjust_stock: this.adjustStock.bind(this), get_inventory_value: this.getInventoryValue.bind(this), + create_part: this.createPart.bind(this), + list_parts: this.listParts.bind(this), }; return handlers[toolName]; } - private async checkStock( - params: { product_ids?: string[]; warehouse_id?: string }, - context: McpContext - ): Promise { - // TODO: Connect to actual inventory service - return [ - { - product_id: 'sample-1', - product_name: 'Producto ejemplo', - stock: 100, - warehouse_id: params.warehouse_id || 'default', - message: 'Conectar a InventoryService real', - }, - ]; - } - - private async getLowStockProducts( - params: { threshold?: number }, - context: McpContext - ): Promise { - // TODO: Connect to actual inventory service - const threshold = params.threshold || 10; - return [ - { - product_id: 'low-stock-1', - product_name: 'Producto bajo stock', - current_stock: 5, - min_stock: threshold, - shortage: threshold - 5, - message: 'Conectar a InventoryService real', - }, - ]; - } - - private async recordInventoryMovement( - params: { product_id: string; quantity: number; movement_type: string; reason?: string }, + private async searchParts( + params: { query: string; limit?: number }, context: McpContext ): Promise { - // TODO: Connect to actual inventory service + const parts = await this.partService.search( + context.tenantId, + params.query, + params.limit || 10 + ); + return { - movement_id: 'mov-' + Date.now(), - product_id: params.product_id, - quantity: params.quantity, - movement_type: params.movement_type, - reason: params.reason, - recorded_by: context.userId, - recorded_at: new Date().toISOString(), - message: 'Conectar a InventoryService real', + success: true, + parts: parts.map(part => ({ + id: part.id, + sku: part.sku, + name: part.name, + brand: part.brand, + price: part.price, + current_stock: part.currentStock, + available_stock: part.currentStock - part.reservedStock, + })), + count: parts.length, }; } + private async getPartDetails( + params: { part_id?: string; sku?: string; barcode?: string }, + context: McpContext + ): Promise { + let part = null; + + if (params.part_id) { + part = await this.partService.findById(context.tenantId, params.part_id); + } else if (params.sku) { + part = await this.partService.findBySku(context.tenantId, params.sku); + } else if (params.barcode) { + part = await this.partService.findByBarcode(context.tenantId, params.barcode); + } + + if (!part) { + return { + success: false, + error: 'Refacción no encontrada', + }; + } + + return { + success: true, + part: { + id: part.id, + sku: part.sku, + name: part.name, + description: part.description, + brand: part.brand, + manufacturer: part.manufacturer, + compatible_engines: part.compatibleEngines, + unit: part.unit, + cost: part.cost, + price: part.price, + current_stock: part.currentStock, + reserved_stock: part.reservedStock, + available_stock: part.currentStock - part.reservedStock, + min_stock: part.minStock, + max_stock: part.maxStock, + reorder_point: part.reorderPoint, + barcode: part.barcode, + is_active: part.isActive, + }, + }; + } + + private async checkStock( + params: { part_ids: string[] }, + context: McpContext + ): Promise { + const stockInfo = await Promise.all( + params.part_ids.map(async (id) => { + const part = await this.partService.findById(context.tenantId, id); + if (!part) { + return { + part_id: id, + found: false, + error: 'No encontrada', + }; + } + return { + part_id: id, + found: true, + sku: part.sku, + name: part.name, + current_stock: part.currentStock, + reserved_stock: part.reservedStock, + available_stock: part.currentStock - part.reservedStock, + min_stock: part.minStock, + is_low_stock: part.currentStock <= part.minStock, + }; + }) + ); + + return { + success: true, + stock: stockInfo, + }; + } + + private async getLowStockParts( + params: {}, + context: McpContext + ): Promise { + const parts = await this.partService.getLowStockParts(context.tenantId); + + return { + success: true, + parts: parts.map(part => ({ + id: part.id, + sku: part.sku, + name: part.name, + brand: part.brand, + current_stock: part.currentStock, + min_stock: part.minStock, + shortage: part.minStock - part.currentStock, + price: part.price, + })), + count: parts.length, + alert: parts.length > 0 ? `${parts.length} refacciones con stock bajo` : 'Stock OK', + }; + } + + private async adjustStock( + params: { part_id: string; quantity: number; reason: string; reference?: string }, + context: McpContext + ): Promise { + try { + const dto: StockAdjustmentDto = { + quantity: params.quantity, + reason: params.reason, + reference: params.reference, + }; + + const part = await this.partService.adjustStock( + context.tenantId, + params.part_id, + dto + ); + + if (!part) { + return { + success: false, + error: 'Refacción no encontrada', + }; + } + + return { + success: true, + part: { + id: part.id, + sku: part.sku, + name: part.name, + previous_stock: part.currentStock - params.quantity, + adjustment: params.quantity, + new_stock: part.currentStock, + reason: params.reason, + }, + message: params.quantity > 0 + ? `Entrada de ${params.quantity} unidades registrada` + : `Salida de ${Math.abs(params.quantity)} unidades registrada`, + }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } + } + private async getInventoryValue( - params: { warehouse_id?: string }, + params: {}, context: McpContext ): Promise { - // TODO: Connect to actual inventory service + const value = await this.partService.getInventoryValue(context.tenantId); + return { - total_value: 150000.00, - items_count: 500, - warehouse_id: params.warehouse_id || 'all', - currency: 'MXN', - message: 'Conectar a InventoryService real', + success: true, + inventory: { + total_cost_value: value.totalCostValue, + total_sale_value: value.totalSaleValue, + total_items: value.totalItems, + low_stock_count: value.lowStockCount, + margin: value.totalSaleValue - value.totalCostValue, + margin_percent: value.totalCostValue > 0 + ? ((value.totalSaleValue - value.totalCostValue) / value.totalCostValue * 100).toFixed(2) + : 0, + currency: 'MXN', + }, + }; + } + + private async createPart( + params: { + sku: string; + name: string; + description?: string; + brand?: string; + manufacturer?: string; + price: number; + cost?: number; + min_stock?: number; + barcode?: string; + }, + context: McpContext + ): Promise { + try { + const dto: CreatePartDto = { + sku: params.sku, + name: params.name, + description: params.description, + brand: params.brand, + manufacturer: params.manufacturer, + price: params.price, + cost: params.cost, + minStock: params.min_stock, + barcode: params.barcode, + }; + + const part = await this.partService.create(context.tenantId, dto); + + return { + success: true, + part: { + id: part.id, + sku: part.sku, + name: part.name, + brand: part.brand, + price: part.price, + cost: part.cost, + current_stock: part.currentStock, + }, + message: `Refacción ${part.sku} creada exitosamente`, + }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } + } + + private async listParts( + params: { + category_id?: string; + brand?: string; + search?: string; + low_stock?: boolean; + page?: number; + limit?: number; + }, + context: McpContext + ): Promise { + const result = await this.partService.findAll( + context.tenantId, + { + categoryId: params.category_id, + brand: params.brand, + search: params.search, + lowStock: params.low_stock, + }, + { + page: params.page || 1, + limit: params.limit || 20, + } + ); + + return { + success: true, + parts: result.data.map(part => ({ + id: part.id, + sku: part.sku, + name: part.name, + brand: part.brand, + price: part.price, + current_stock: part.currentStock, + available_stock: part.currentStock - part.reservedStock, + is_low_stock: part.currentStock <= part.minStock, + })), + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + total_pages: result.totalPages, + }, }; } } diff --git a/src/modules/mcp/tools/orders-tools.service.ts b/src/modules/mcp/tools/orders-tools.service.ts index facc0b0..0867057 100644 --- a/src/modules/mcp/tools/orders-tools.service.ts +++ b/src/modules/mcp/tools/orders-tools.service.ts @@ -1,124 +1,294 @@ +import { DataSource } from 'typeorm'; import { McpToolProvider, McpToolDefinition, McpToolHandler, McpContext, } from '../interfaces'; +import { ServiceOrderService, CreateServiceOrderDto } from '../../service-management/services/service-order.service'; +import { ServiceOrderStatus } from '../../service-management/entities/service-order.entity'; /** * Orders Tools Service - * Provides MCP tools for order management. - * - * TODO: Connect to actual OrdersService when available. + * Provides MCP tools for service order management. + * Connected to actual ServiceOrderService for real data operations. */ export class OrdersToolsService implements McpToolProvider { + private serviceOrderService: ServiceOrderService; + + constructor(dataSource: DataSource) { + this.serviceOrderService = new ServiceOrderService(dataSource); + } + getTools(): McpToolDefinition[] { return [ { - name: 'create_order', - description: 'Crea un nuevo pedido', + name: 'create_service_order', + description: 'Crea una nueva orden de servicio para un vehículo', category: 'orders', permissions: ['orders.create'], parameters: { type: 'object', properties: { customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' }, - items: { - type: 'array', - description: 'Items del pedido', - items: { - type: 'object', - properties: { - product_id: { type: 'string' }, - quantity: { type: 'number' }, - unit_price: { type: 'number' }, - }, - }, + vehicle_id: { type: 'string', format: 'uuid', description: 'ID del vehículo' }, + customer_symptoms: { type: 'string', description: 'Síntomas reportados por el cliente' }, + priority: { + type: 'string', + enum: ['low', 'normal', 'high', 'urgent'], + description: 'Prioridad de la orden' }, - payment_method: { type: 'string', enum: ['cash', 'card', 'transfer', 'fiado'] }, - notes: { type: 'string' }, + assigned_to: { type: 'string', format: 'uuid', description: 'ID del técnico asignado' }, + bay_id: { type: 'string', format: 'uuid', description: 'ID de la bahía de trabajo' }, + odometer_in: { type: 'number', description: 'Kilometraje de entrada' }, + internal_notes: { type: 'string', description: 'Notas internas' }, }, - required: ['customer_id', 'items'], + required: ['customer_id', 'vehicle_id'], }, - returns: { type: 'object' }, + returns: { type: 'object', description: 'Orden de servicio creada' }, }, { - name: 'get_order_status', - description: 'Consulta el estado de un pedido', + name: 'get_service_order', + description: 'Consulta una orden de servicio por ID o número de orden', category: 'orders', parameters: { type: 'object', properties: { - order_id: { type: 'string', format: 'uuid' }, + order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' }, + order_number: { type: 'string', description: 'Número de orden (ej: OS-2026-00001)' }, }, - required: ['order_id'], }, - returns: { type: 'object' }, + returns: { type: 'object', description: 'Detalle de la orden de servicio' }, + }, + { + name: 'list_service_orders', + description: 'Lista órdenes de servicio con filtros opcionales', + category: 'orders', + parameters: { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['received', 'diagnosed', 'quoted', 'approved', 'in_progress', 'waiting_parts', 'completed', 'delivered', 'cancelled'], + description: 'Filtrar por estado' + }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] }, + customer_id: { type: 'string', format: 'uuid' }, + vehicle_id: { type: 'string', format: 'uuid' }, + assigned_to: { type: 'string', format: 'uuid' }, + search: { type: 'string', description: 'Buscar en número de orden o síntomas' }, + page: { type: 'number', default: 1 }, + limit: { type: 'number', default: 20 }, + }, + }, + returns: { type: 'object', description: 'Lista paginada de órdenes' }, }, { name: 'update_order_status', - description: 'Actualiza el estado de un pedido', + description: 'Actualiza el estado de una orden de servicio', category: 'orders', permissions: ['orders.update'], parameters: { type: 'object', properties: { - order_id: { type: 'string', format: 'uuid' }, + order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' }, status: { type: 'string', - enum: ['pending', 'confirmed', 'preparing', 'ready', 'delivered', 'cancelled'], + enum: ['received', 'diagnosed', 'quoted', 'approved', 'in_progress', 'waiting_parts', 'completed', 'delivered', 'cancelled'], + description: 'Nuevo estado' }, }, required: ['order_id', 'status'], }, - returns: { type: 'object' }, + returns: { type: 'object', description: 'Orden actualizada' }, + }, + { + name: 'get_orders_kanban', + description: 'Obtiene órdenes agrupadas por estado para vista Kanban', + category: 'orders', + parameters: { + type: 'object', + properties: {}, + }, + returns: { type: 'object', description: 'Órdenes agrupadas por estado' }, + }, + { + name: 'get_orders_dashboard', + description: 'Obtiene estadísticas del dashboard de órdenes', + category: 'orders', + parameters: { + type: 'object', + properties: {}, + }, + returns: { type: 'object', description: 'Estadísticas del dashboard' }, }, ]; } getHandler(toolName: string): McpToolHandler | undefined { const handlers: Record = { - create_order: this.createOrder.bind(this), - get_order_status: this.getOrderStatus.bind(this), + create_service_order: this.createServiceOrder.bind(this), + get_service_order: this.getServiceOrder.bind(this), + list_service_orders: this.listServiceOrders.bind(this), update_order_status: this.updateOrderStatus.bind(this), + get_orders_kanban: this.getOrdersKanban.bind(this), + get_orders_dashboard: this.getOrdersDashboard.bind(this), }; return handlers[toolName]; } - private async createOrder( - params: { customer_id: string; items: any[]; payment_method?: string; notes?: string }, + private async createServiceOrder( + params: { + customer_id: string; + vehicle_id: string; + customer_symptoms?: string; + priority?: string; + assigned_to?: string; + bay_id?: string; + odometer_in?: number; + internal_notes?: string; + }, context: McpContext ): Promise { - // TODO: Connect to actual orders service - const subtotal = params.items.reduce((sum, item) => sum + (item.quantity * (item.unit_price || 0)), 0); + const dto: CreateServiceOrderDto = { + customerId: params.customer_id, + vehicleId: params.vehicle_id, + customerSymptoms: params.customer_symptoms, + priority: params.priority as any, + assignedTo: params.assigned_to, + bayId: params.bay_id, + odometerIn: params.odometer_in, + internalNotes: params.internal_notes, + }; + + const order = await this.serviceOrderService.create( + context.tenantId, + dto, + context.userId + ); + return { - order_id: 'order-' + Date.now(), - customer_id: params.customer_id, - items: params.items, - subtotal, - tax: subtotal * 0.16, - total: subtotal * 1.16, - payment_method: params.payment_method || 'cash', - status: 'pending', - created_by: context.userId, - created_at: new Date().toISOString(), - message: 'Conectar a OrdersService real', + success: true, + order: { + id: order.id, + order_number: order.orderNumber, + status: order.status, + priority: order.priority, + customer_id: order.customerId, + vehicle_id: order.vehicleId, + received_at: order.receivedAt, + created_by: order.createdBy, + }, + message: `Orden de servicio ${order.orderNumber} creada exitosamente`, }; } - private async getOrderStatus( - params: { order_id: string }, + private async getServiceOrder( + params: { order_id?: string; order_number?: string }, context: McpContext ): Promise { - // TODO: Connect to actual orders service + let order = null; + + if (params.order_id) { + order = await this.serviceOrderService.findById(context.tenantId, params.order_id); + } else if (params.order_number) { + order = await this.serviceOrderService.findByOrderNumber(context.tenantId, params.order_number); + } + + if (!order) { + return { + success: false, + error: 'Orden de servicio no encontrada', + }; + } + + // Get order items + const items = await this.serviceOrderService.getItems(order.id); + return { - order_id: params.order_id, - status: 'pending', - customer_name: 'Cliente ejemplo', - total: 1160.00, - items_count: 3, - created_at: new Date().toISOString(), - message: 'Conectar a OrdersService real', + success: true, + order: { + id: order.id, + order_number: order.orderNumber, + status: order.status, + priority: order.priority, + customer_id: order.customerId, + vehicle_id: order.vehicleId, + customer_symptoms: order.customerSymptoms, + assigned_to: order.assignedTo, + bay_id: order.bayId, + odometer_in: order.odometerIn, + odometer_out: order.odometerOut, + labor_total: order.laborTotal, + parts_total: order.partsTotal, + discount_percent: order.discountPercent, + discount_amount: order.discountAmount, + tax: order.tax, + grand_total: order.grandTotal, + received_at: order.receivedAt, + started_at: order.startedAt, + completed_at: order.completedAt, + delivered_at: order.deliveredAt, + items: items.map(item => ({ + id: item.id, + type: item.itemType, + description: item.description, + quantity: item.quantity, + unit_price: item.unitPrice, + subtotal: item.subtotal, + status: item.status, + })), + }, + }; + } + + private async listServiceOrders( + params: { + status?: string; + priority?: string; + customer_id?: string; + vehicle_id?: string; + assigned_to?: string; + search?: string; + page?: number; + limit?: number; + }, + context: McpContext + ): Promise { + const result = await this.serviceOrderService.findAll( + context.tenantId, + { + status: params.status as ServiceOrderStatus, + priority: params.priority as any, + customerId: params.customer_id, + vehicleId: params.vehicle_id, + assignedTo: params.assigned_to, + search: params.search, + }, + { + page: params.page || 1, + limit: params.limit || 20, + } + ); + + return { + success: true, + orders: result.data.map(order => ({ + id: order.id, + order_number: order.orderNumber, + status: order.status, + priority: order.priority, + customer_id: order.customerId, + vehicle_id: order.vehicleId, + grand_total: order.grandTotal, + received_at: order.receivedAt, + })), + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + total_pages: result.totalPages, + }, }; } @@ -126,14 +296,84 @@ export class OrdersToolsService implements McpToolProvider { params: { order_id: string; status: string }, context: McpContext ): Promise { - // TODO: Connect to actual orders service + const order = await this.serviceOrderService.update( + context.tenantId, + params.order_id, + { status: params.status as ServiceOrderStatus } + ); + + if (!order) { + return { + success: false, + error: 'Orden de servicio no encontrada', + }; + } + return { - order_id: params.order_id, - previous_status: 'pending', - new_status: params.status, - updated_by: context.userId, - updated_at: new Date().toISOString(), - message: 'Conectar a OrdersService real', + success: true, + order: { + id: order.id, + order_number: order.orderNumber, + previous_status: order.status, + new_status: params.status, + updated_at: order.updatedAt, + }, + message: `Estado actualizado a ${params.status}`, + }; + } + + private async getOrdersKanban( + params: {}, + context: McpContext + ): Promise { + const grouped = await this.serviceOrderService.getOrdersByStatus(context.tenantId); + + // Transform to simpler format for AI consumption + const kanban: Record = {}; + for (const [status, orders] of Object.entries(grouped)) { + kanban[status] = orders.map(order => ({ + id: order.id, + order_number: order.orderNumber, + priority: order.priority, + customer_id: order.customerId, + vehicle_id: order.vehicleId, + grand_total: order.grandTotal, + received_at: order.receivedAt, + })); + } + + return { + success: true, + kanban, + summary: { + received: grouped[ServiceOrderStatus.RECEIVED]?.length || 0, + diagnosed: grouped[ServiceOrderStatus.DIAGNOSED]?.length || 0, + quoted: grouped[ServiceOrderStatus.QUOTED]?.length || 0, + approved: grouped[ServiceOrderStatus.APPROVED]?.length || 0, + in_progress: grouped[ServiceOrderStatus.IN_PROGRESS]?.length || 0, + waiting_parts: grouped[ServiceOrderStatus.WAITING_PARTS]?.length || 0, + completed: grouped[ServiceOrderStatus.COMPLETED]?.length || 0, + delivered: grouped[ServiceOrderStatus.DELIVERED]?.length || 0, + }, + }; + } + + private async getOrdersDashboard( + params: {}, + context: McpContext + ): Promise { + const stats = await this.serviceOrderService.getDashboardStats(context.tenantId); + + return { + success: true, + dashboard: { + total_orders: stats.totalOrders, + pending_orders: stats.pendingOrders, + in_progress_orders: stats.inProgressOrders, + completed_today: stats.completedToday, + total_revenue: stats.totalRevenue, + average_ticket: stats.averageTicket, + }, }; } }