diff --git a/src/modules/mcp/README.md b/src/modules/mcp/README.md new file mode 100644 index 0000000..a01f78d --- /dev/null +++ b/src/modules/mcp/README.md @@ -0,0 +1,72 @@ +# MCP Module + +## Descripcion + +Implementacion del Model Context Protocol (MCP) Server para el ERP. Expone herramientas (tools) y recursos que pueden ser invocados por agentes de IA para realizar operaciones en el sistema. Incluye registro de tools, control de permisos, logging de llamadas y auditorias. + +## Entidades + +| Entidad | Schema | Descripcion | +|---------|--------|-------------| +| `ToolCall` | ai.tool_calls | Registro de invocaciones de tools con parametros, estado y duracion | +| `ToolCallResult` | ai.tool_call_results | Resultados de las invocaciones incluyendo respuesta o error | + +## Servicios + +| Servicio | Responsabilidades | +|----------|-------------------| +| `McpServerService` | Servidor MCP principal: listado de tools, ejecucion con logging, acceso a recursos | +| `ToolRegistryService` | Registro central de tools y handlers disponibles por categoria | +| `ToolLoggerService` | Logging de llamadas a tools: inicio, completado, fallido, historial | + +## Tool Providers + +| Provider | Tools | Descripcion | +|----------|-------|-------------| +| `ProductsToolsService` | list_products, get_product_details, check_product_availability | Operaciones de productos | +| `InventoryToolsService` | check_stock, get_low_stock, reserve_stock | Operaciones de inventario | +| `OrdersToolsService` | create_order, get_order_status, list_orders | Operaciones de ordenes | +| `CustomersToolsService` | search_customers, get_customer_info | Operaciones de clientes | +| `FiadosToolsService` | get_fiado_balance, register_fiado_payment | Operaciones de fiados | +| `SalesToolsService` | get_sales_summary, get_daily_report | Reportes de ventas | +| `FinancialToolsService` | get_cash_balance, get_revenue | Operaciones financieras | +| `BranchToolsService` | list_branches, get_branch_info | Operaciones de sucursales | + +## Endpoints + +| Method | Path | Descripcion | +|--------|------|-------------| +| GET | `/tools` | Lista todas las tools disponibles | +| GET | `/tools/:name` | Obtiene definicion de una tool | +| POST | `/tools/call` | Ejecuta una tool con parametros | +| GET | `/resources` | Lista recursos MCP disponibles | +| GET | `/resources/*` | Obtiene contenido de un recurso | +| GET | `/tool-calls` | Historial de llamadas a tools | +| GET | `/tool-calls/:id` | Detalles de una llamada | +| GET | `/stats` | Estadisticas de uso de tools | + +## Dependencias + +- `ai` - Entidades de logging en schema ai +- `auth` - Contexto de usuario y permisos + +## Configuracion + +No requiere configuracion adicional. Los tools se registran programaticamente. + +## Recursos MCP + +| URI | Descripcion | +|-----|-------------| +| `erp://config/business` | Configuracion del negocio | +| `erp://catalog/categories` | Catalogo de categorias de productos | +| `erp://inventory/summary` | Resumen de inventario actual | + +## Categorias de Tools + +- `products` - Gestion de productos +- `inventory` - Control de inventario +- `orders` - Gestion de ordenes +- `customers` - Clientes y contactos +- `fiados` - Creditos y fiados +- `system` - Operaciones del sistema diff --git a/src/modules/mcp/tools/customers-tools.service.ts b/src/modules/mcp/tools/customers-tools.service.ts index 93af263..daa5298 100644 --- a/src/modules/mcp/tools/customers-tools.service.ts +++ b/src/modules/mcp/tools/customers-tools.service.ts @@ -1,138 +1,51 @@ -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. - * Connected to actual CustomersService for real data operations. + * + * TODO: Connect to actual CustomersService when available. */ 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, teléfono, email o RFC', + description: 'Busca clientes por nombre, telefono o email', category: 'customers', parameters: { type: 'object', properties: { - query: { type: 'string', description: 'Término de búsqueda' }, - limit: { type: 'number', default: 10, description: 'Máximo de resultados' }, + query: { type: 'string', description: 'Texto de busqueda' }, + limit: { type: 'number', description: 'Limite de resultados', default: 10 }, }, required: ['query'], }, - returns: { type: 'array', description: 'Lista de clientes encontrados' }, + returns: { type: 'array' }, }, { - name: 'get_customer', - description: 'Obtiene información detallada de un cliente', + name: 'get_customer_balance', + description: 'Obtiene el saldo actual de un cliente', category: 'customers', parameters: { type: 'object', properties: { - 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' }, + customer_id: { type: 'string', format: 'uuid' }, }, required: ['customer_id'], }, - returns: { type: 'object', description: 'Cliente actualizado' }, - }, - { - name: 'list_customers', - description: 'Lista clientes con filtros opcionales', - category: 'customers', - parameters: { + returns: { type: 'object', properties: { - 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 }, + balance: { type: 'number' }, + credit_limit: { type: 'number' }, }, }, - 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' }, }, ]; } @@ -140,11 +53,7 @@ export class CustomersToolsService implements McpToolProvider { getHandler(toolName: string): McpToolHandler | undefined { const handlers: Record = { search_customers: this.searchCustomers.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), + get_customer_balance: this.getCustomerBalance.bind(this), }; return handlers[toolName]; } @@ -152,254 +61,34 @@ export class CustomersToolsService implements McpToolProvider { private async searchCustomers( params: { query: string; limit?: number }, context: McpContext - ): 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 getCustomer( - params: { customer_id?: string; email?: string; phone?: string; rfc?: string }, - context: McpContext - ): Promise { - 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 { - 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, + ): 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', }, - }; + ]; } - 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; - }, + private async getCustomerBalance( + params: { customer_id: 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); - + // TODO: Connect to actual customers service 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, - }, + 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', }; } } diff --git a/src/modules/mcp/tools/inventory-tools.service.ts b/src/modules/mcp/tools/inventory-tools.service.ts index 570272b..76a45ca 100644 --- a/src/modules/mcp/tools/inventory-tools.service.ts +++ b/src/modules/mcp/tools/inventory-tools.service.ts @@ -1,450 +1,154 @@ -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 parts/inventory management. - * Connected to actual PartService for real data operations. + * Provides MCP tools for inventory management. + * + * TODO: Connect to actual InventoryService when available. */ 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 refacciones', + description: 'Consulta el stock actual de productos', category: 'inventory', parameters: { type: 'object', properties: { - part_ids: { type: 'array', items: { type: 'string' }, description: 'IDs de refacciones a consultar' }, + product_ids: { type: 'array', description: 'IDs de productos a consultar' }, + warehouse_id: { type: 'string', description: 'ID del almacen' }, }, - required: ['part_ids'], }, - returns: { type: 'array', description: 'Stock de cada refacción' }, + returns: { type: 'array' }, }, { - name: 'get_low_stock_parts', - description: 'Lista refacciones con stock bajo (por debajo del mínimo configurado)', + name: 'get_low_stock_products', + description: 'Lista productos que estan por debajo del minimo de stock', category: 'inventory', parameters: { type: 'object', - properties: {}, + properties: { + threshold: { type: 'number', description: 'Umbral de stock bajo' }, + }, }, - returns: { type: 'array', description: 'Refacciones con stock bajo' }, + returns: { type: 'array' }, }, { - name: 'adjust_stock', - description: 'Ajusta el stock de una refacción (entrada, salida o ajuste)', + name: 'record_inventory_movement', + description: 'Registra un movimiento de inventario (entrada, salida, ajuste)', category: 'inventory', permissions: ['inventory.write'], parameters: { type: 'object', properties: { - 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)' }, + product_id: { type: 'string', format: 'uuid' }, + quantity: { type: 'number' }, + movement_type: { type: 'string', enum: ['in', 'out', 'adjustment'] }, + reason: { type: 'string' }, }, - required: ['part_id', 'quantity', 'reason'], + required: ['product_id', 'quantity', 'movement_type'], }, - returns: { type: 'object', description: 'Refacción actualizada' }, + returns: { type: 'object' }, }, { name: 'get_inventory_value', description: 'Calcula el valor total del inventario', category: 'inventory', - parameters: { - type: 'object', - properties: {}, - }, - 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: { - 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 }, + warehouse_id: { type: 'string', description: 'ID del almacen (opcional)' }, + }, + }, + returns: { + type: 'object', + properties: { + total_value: { type: 'number' }, + items_count: { type: 'number' }, }, }, - 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_parts: this.getLowStockParts.bind(this), - adjust_stock: this.adjustStock.bind(this), + get_low_stock_products: this.getLowStockProducts.bind(this), + record_inventory_movement: this.recordInventoryMovement.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 searchParts( - params: { query: string; limit?: number }, - context: McpContext - ): Promise { - const parts = await this.partService.search( - context.tenantId, - params.query, - params.limit || 10 - ); - - return { - 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[] }, + params: { product_ids?: string[]; warehouse_id?: 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, - }; + ): 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 getLowStockParts( - params: {}, + private async getLowStockProducts( + params: { threshold?: number }, 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', - }; + ): 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 adjustStock( - params: { part_id: string; quantity: number; reason: string; reference?: string }, + private async recordInventoryMovement( + params: { product_id: string; quantity: number; movement_type: string; reason?: 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, - }; - } + // TODO: Connect to actual inventory service + 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', + }; } private async getInventoryValue( - params: {}, + params: { warehouse_id?: string }, context: McpContext ): Promise { - const value = await this.partService.getInventoryValue(context.tenantId); - + // TODO: Connect to actual inventory service return { - 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, - }, + total_value: 150000.00, + items_count: 500, + warehouse_id: params.warehouse_id || 'all', + currency: 'MXN', + message: 'Conectar a InventoryService real', }; } } diff --git a/src/modules/mcp/tools/orders-tools.service.ts b/src/modules/mcp/tools/orders-tools.service.ts index 0867057..facc0b0 100644 --- a/src/modules/mcp/tools/orders-tools.service.ts +++ b/src/modules/mcp/tools/orders-tools.service.ts @@ -1,294 +1,124 @@ -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 service order management. - * Connected to actual ServiceOrderService for real data operations. + * Provides MCP tools for order management. + * + * TODO: Connect to actual OrdersService when available. */ export class OrdersToolsService implements McpToolProvider { - private serviceOrderService: ServiceOrderService; - - constructor(dataSource: DataSource) { - this.serviceOrderService = new ServiceOrderService(dataSource); - } - getTools(): McpToolDefinition[] { return [ { - name: 'create_service_order', - description: 'Crea una nueva orden de servicio para un vehículo', + name: 'create_order', + description: 'Crea un nuevo pedido', category: 'orders', permissions: ['orders.create'], parameters: { type: 'object', properties: { customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' }, - 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' + items: { + type: 'array', + description: 'Items del pedido', + items: { + type: 'object', + properties: { + product_id: { type: 'string' }, + quantity: { type: 'number' }, + unit_price: { type: 'number' }, + }, + }, }, - 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' }, + payment_method: { type: 'string', enum: ['cash', 'card', 'transfer', 'fiado'] }, + notes: { type: 'string' }, }, - required: ['customer_id', 'vehicle_id'], + required: ['customer_id', 'items'], }, - returns: { type: 'object', description: 'Orden de servicio creada' }, + returns: { type: 'object' }, }, { - name: 'get_service_order', - description: 'Consulta una orden de servicio por ID o número de orden', + name: 'get_order_status', + description: 'Consulta el estado de un pedido', category: 'orders', parameters: { type: 'object', properties: { - order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' }, - order_number: { type: 'string', description: 'Número de orden (ej: OS-2026-00001)' }, + order_id: { type: 'string', format: 'uuid' }, }, + required: ['order_id'], }, - 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' }, + returns: { type: 'object' }, }, { name: 'update_order_status', - description: 'Actualiza el estado de una orden de servicio', + description: 'Actualiza el estado de un pedido', category: 'orders', permissions: ['orders.update'], parameters: { type: 'object', properties: { - order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' }, + order_id: { type: 'string', format: 'uuid' }, status: { type: 'string', - enum: ['received', 'diagnosed', 'quoted', 'approved', 'in_progress', 'waiting_parts', 'completed', 'delivered', 'cancelled'], - description: 'Nuevo estado' + enum: ['pending', 'confirmed', 'preparing', 'ready', 'delivered', 'cancelled'], }, }, required: ['order_id', 'status'], }, - 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' }, + returns: { type: 'object' }, }, ]; } getHandler(toolName: string): McpToolHandler | undefined { const handlers: Record = { - create_service_order: this.createServiceOrder.bind(this), - get_service_order: this.getServiceOrder.bind(this), - list_service_orders: this.listServiceOrders.bind(this), + create_order: this.createOrder.bind(this), + get_order_status: this.getOrderStatus.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 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; - }, + private async createOrder( + params: { customer_id: string; items: any[]; payment_method?: string; notes?: string }, context: McpContext ): Promise { - 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 - ); - + // TODO: Connect to actual orders service + const subtotal = params.items.reduce((sum, item) => sum + (item.quantity * (item.unit_price || 0)), 0); return { - 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`, + 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', }; } - private async getServiceOrder( - params: { order_id?: string; order_number?: string }, + private async getOrderStatus( + params: { order_id: string }, context: McpContext ): Promise { - 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); - + // TODO: Connect to actual orders service return { - 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, - }, + 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', }; } @@ -296,84 +126,14 @@ export class OrdersToolsService implements McpToolProvider { params: { order_id: string; status: string }, context: McpContext ): Promise { - 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', - }; - } - + // TODO: Connect to actual orders service return { - 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, - }, + 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', }; } }