[MCP] feat: Connect MCP tools to actual services
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
669c0c2911
commit
d5f1453492
@ -1,51 +1,138 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
McpToolProvider,
|
McpToolProvider,
|
||||||
McpToolDefinition,
|
McpToolDefinition,
|
||||||
McpToolHandler,
|
McpToolHandler,
|
||||||
McpContext,
|
McpContext,
|
||||||
} from '../interfaces';
|
} 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
|
* Customers Tools Service
|
||||||
* Provides MCP tools for customer management.
|
* 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 {
|
export class CustomersToolsService implements McpToolProvider {
|
||||||
|
private customersService: CustomersService;
|
||||||
|
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
this.customersService = new CustomersService(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
getTools(): McpToolDefinition[] {
|
getTools(): McpToolDefinition[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'search_customers',
|
name: 'search_customers',
|
||||||
description: 'Busca clientes por nombre, telefono o email',
|
description: 'Busca clientes por nombre, teléfono, email o RFC',
|
||||||
category: 'customers',
|
category: 'customers',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: 'string', description: 'Texto de busqueda' },
|
query: { type: 'string', description: 'Término de búsqueda' },
|
||||||
limit: { type: 'number', description: 'Limite de resultados', default: 10 },
|
limit: { type: 'number', default: 10, description: 'Máximo de resultados' },
|
||||||
},
|
},
|
||||||
required: ['query'],
|
required: ['query'],
|
||||||
},
|
},
|
||||||
returns: { type: 'array' },
|
returns: { type: 'array', description: 'Lista de clientes encontrados' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_customer_balance',
|
name: 'get_customer',
|
||||||
description: 'Obtiene el saldo actual de un cliente',
|
description: 'Obtiene información detallada de un cliente',
|
||||||
category: 'customers',
|
category: 'customers',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
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'],
|
required: ['customer_id'],
|
||||||
},
|
},
|
||||||
returns: {
|
returns: { type: 'object', description: 'Cliente actualizado' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list_customers',
|
||||||
|
description: 'Lista clientes con filtros opcionales',
|
||||||
|
category: 'customers',
|
||||||
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
balance: { type: 'number' },
|
customer_type: {
|
||||||
credit_limit: { type: 'number' },
|
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 {
|
getHandler(toolName: string): McpToolHandler | undefined {
|
||||||
const handlers: Record<string, McpToolHandler> = {
|
const handlers: Record<string, McpToolHandler> = {
|
||||||
search_customers: this.searchCustomers.bind(this),
|
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];
|
return handlers[toolName];
|
||||||
}
|
}
|
||||||
@ -61,34 +152,254 @@ export class CustomersToolsService implements McpToolProvider {
|
|||||||
private async searchCustomers(
|
private async searchCustomers(
|
||||||
params: { query: string; limit?: number },
|
params: { query: string; limit?: number },
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any[]> {
|
): Promise<any> {
|
||||||
// TODO: Connect to actual customers service
|
const customers = await this.customersService.search(
|
||||||
return [
|
context.tenantId,
|
||||||
{
|
params.query,
|
||||||
id: 'customer-1',
|
params.limit || 10
|
||||||
name: 'Juan Perez',
|
);
|
||||||
phone: '+52 55 1234 5678',
|
|
||||||
email: 'juan@example.com',
|
return {
|
||||||
balance: 500.00,
|
success: true,
|
||||||
credit_limit: 5000.00,
|
customers: customers.map(c => ({
|
||||||
message: 'Conectar a CustomersService real',
|
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(
|
private async getCustomer(
|
||||||
params: { customer_id: string },
|
params: { customer_id?: string; email?: string; phone?: string; rfc?: string },
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 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 {
|
return {
|
||||||
customer_id: params.customer_id,
|
success: true,
|
||||||
customer_name: 'Cliente ejemplo',
|
customer: {
|
||||||
balance: 500.00,
|
id: customer.id,
|
||||||
credit_limit: 5000.00,
|
name: customer.name,
|
||||||
available_credit: 4500.00,
|
legal_name: customer.legalName,
|
||||||
last_purchase: new Date().toISOString(),
|
customer_type: customer.customerType,
|
||||||
message: 'Conectar a CustomersService real',
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,93 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
McpToolProvider,
|
McpToolProvider,
|
||||||
McpToolDefinition,
|
McpToolDefinition,
|
||||||
McpToolHandler,
|
McpToolHandler,
|
||||||
McpContext,
|
McpContext,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
import { PartService, CreatePartDto, StockAdjustmentDto } from '../../parts-management/services/part.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inventory Tools Service
|
* Inventory Tools Service
|
||||||
* Provides MCP tools for inventory management.
|
* Provides MCP tools for parts/inventory management.
|
||||||
*
|
* Connected to actual PartService for real data operations.
|
||||||
* TODO: Connect to actual InventoryService when available.
|
|
||||||
*/
|
*/
|
||||||
export class InventoryToolsService implements McpToolProvider {
|
export class InventoryToolsService implements McpToolProvider {
|
||||||
|
private partService: PartService;
|
||||||
|
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
this.partService = new PartService(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
getTools(): McpToolDefinition[] {
|
getTools(): McpToolDefinition[] {
|
||||||
return [
|
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',
|
name: 'check_stock',
|
||||||
description: 'Consulta el stock actual de productos',
|
description: 'Consulta el stock actual de refacciones',
|
||||||
category: 'inventory',
|
category: 'inventory',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
product_ids: { type: 'array', description: 'IDs de productos a consultar' },
|
part_ids: { type: 'array', items: { type: 'string' }, description: 'IDs de refacciones a consultar' },
|
||||||
warehouse_id: { type: 'string', description: 'ID del almacen' },
|
|
||||||
},
|
},
|
||||||
|
required: ['part_ids'],
|
||||||
},
|
},
|
||||||
returns: { type: 'array' },
|
returns: { type: 'array', description: 'Stock de cada refacción' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'get_low_stock_products',
|
name: 'get_low_stock_parts',
|
||||||
description: 'Lista productos que estan por debajo del minimo de stock',
|
description: 'Lista refacciones con stock bajo (por debajo del mínimo configurado)',
|
||||||
category: 'inventory',
|
category: 'inventory',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {},
|
||||||
threshold: { type: 'number', description: 'Umbral de stock bajo' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
returns: { type: 'array' },
|
returns: { type: 'array', description: 'Refacciones con stock bajo' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'record_inventory_movement',
|
name: 'adjust_stock',
|
||||||
description: 'Registra un movimiento de inventario (entrada, salida, ajuste)',
|
description: 'Ajusta el stock de una refacción (entrada, salida o ajuste)',
|
||||||
category: 'inventory',
|
category: 'inventory',
|
||||||
permissions: ['inventory.write'],
|
permissions: ['inventory.write'],
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
product_id: { type: 'string', format: 'uuid' },
|
part_id: { type: 'string', format: 'uuid', description: 'ID de la refacción' },
|
||||||
quantity: { type: 'number' },
|
quantity: { type: 'number', description: 'Cantidad a ajustar (positivo=entrada, negativo=salida)' },
|
||||||
movement_type: { type: 'string', enum: ['in', 'out', 'adjustment'] },
|
reason: { type: 'string', description: 'Razón del ajuste' },
|
||||||
reason: { type: 'string' },
|
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',
|
name: 'get_inventory_value',
|
||||||
@ -62,93 +95,356 @@ export class InventoryToolsService implements McpToolProvider {
|
|||||||
category: 'inventory',
|
category: 'inventory',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {},
|
||||||
warehouse_id: { type: 'string', description: 'ID del almacen (opcional)' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
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',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
total_value: { type: 'number' },
|
sku: { type: 'string', description: 'Código SKU único' },
|
||||||
items_count: { type: 'number' },
|
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 {
|
getHandler(toolName: string): McpToolHandler | undefined {
|
||||||
const handlers: Record<string, McpToolHandler> = {
|
const handlers: Record<string, McpToolHandler> = {
|
||||||
|
search_parts: this.searchParts.bind(this),
|
||||||
|
get_part_details: this.getPartDetails.bind(this),
|
||||||
check_stock: this.checkStock.bind(this),
|
check_stock: this.checkStock.bind(this),
|
||||||
get_low_stock_products: this.getLowStockProducts.bind(this),
|
get_low_stock_parts: this.getLowStockParts.bind(this),
|
||||||
record_inventory_movement: this.recordInventoryMovement.bind(this),
|
adjust_stock: this.adjustStock.bind(this),
|
||||||
get_inventory_value: this.getInventoryValue.bind(this),
|
get_inventory_value: this.getInventoryValue.bind(this),
|
||||||
|
create_part: this.createPart.bind(this),
|
||||||
|
list_parts: this.listParts.bind(this),
|
||||||
};
|
};
|
||||||
return handlers[toolName];
|
return handlers[toolName];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkStock(
|
private async searchParts(
|
||||||
params: { product_ids?: string[]; warehouse_id?: string },
|
params: { query: string; limit?: number },
|
||||||
context: McpContext
|
|
||||||
): Promise<any[]> {
|
|
||||||
// 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<any[]> {
|
|
||||||
// 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 },
|
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// TODO: Connect to actual inventory service
|
const parts = await this.partService.search(
|
||||||
|
context.tenantId,
|
||||||
|
params.query,
|
||||||
|
params.limit || 10
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
movement_id: 'mov-' + Date.now(),
|
success: true,
|
||||||
product_id: params.product_id,
|
parts: parts.map(part => ({
|
||||||
quantity: params.quantity,
|
id: part.id,
|
||||||
movement_type: params.movement_type,
|
sku: part.sku,
|
||||||
reason: params.reason,
|
name: part.name,
|
||||||
recorded_by: context.userId,
|
brand: part.brand,
|
||||||
recorded_at: new Date().toISOString(),
|
price: part.price,
|
||||||
message: 'Conectar a InventoryService real',
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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(
|
private async getInventoryValue(
|
||||||
params: { warehouse_id?: string },
|
params: {},
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// TODO: Connect to actual inventory service
|
const value = await this.partService.getInventoryValue(context.tenantId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total_value: 150000.00,
|
success: true,
|
||||||
items_count: 500,
|
inventory: {
|
||||||
warehouse_id: params.warehouse_id || 'all',
|
total_cost_value: value.totalCostValue,
|
||||||
currency: 'MXN',
|
total_sale_value: value.totalSaleValue,
|
||||||
message: 'Conectar a InventoryService real',
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,124 +1,294 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
McpToolProvider,
|
McpToolProvider,
|
||||||
McpToolDefinition,
|
McpToolDefinition,
|
||||||
McpToolHandler,
|
McpToolHandler,
|
||||||
McpContext,
|
McpContext,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
import { ServiceOrderService, CreateServiceOrderDto } from '../../service-management/services/service-order.service';
|
||||||
|
import { ServiceOrderStatus } from '../../service-management/entities/service-order.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orders Tools Service
|
* Orders Tools Service
|
||||||
* Provides MCP tools for order management.
|
* Provides MCP tools for service order management.
|
||||||
*
|
* Connected to actual ServiceOrderService for real data operations.
|
||||||
* TODO: Connect to actual OrdersService when available.
|
|
||||||
*/
|
*/
|
||||||
export class OrdersToolsService implements McpToolProvider {
|
export class OrdersToolsService implements McpToolProvider {
|
||||||
|
private serviceOrderService: ServiceOrderService;
|
||||||
|
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
this.serviceOrderService = new ServiceOrderService(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
getTools(): McpToolDefinition[] {
|
getTools(): McpToolDefinition[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'create_order',
|
name: 'create_service_order',
|
||||||
description: 'Crea un nuevo pedido',
|
description: 'Crea una nueva orden de servicio para un vehículo',
|
||||||
category: 'orders',
|
category: 'orders',
|
||||||
permissions: ['orders.create'],
|
permissions: ['orders.create'],
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' },
|
customer_id: { type: 'string', format: 'uuid', description: 'ID del cliente' },
|
||||||
items: {
|
vehicle_id: { type: 'string', format: 'uuid', description: 'ID del vehículo' },
|
||||||
type: 'array',
|
customer_symptoms: { type: 'string', description: 'Síntomas reportados por el cliente' },
|
||||||
description: 'Items del pedido',
|
priority: {
|
||||||
items: {
|
type: 'string',
|
||||||
type: 'object',
|
enum: ['low', 'normal', 'high', 'urgent'],
|
||||||
properties: {
|
description: 'Prioridad de la orden'
|
||||||
product_id: { type: 'string' },
|
|
||||||
quantity: { type: 'number' },
|
|
||||||
unit_price: { type: 'number' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
payment_method: { type: 'string', enum: ['cash', 'card', 'transfer', 'fiado'] },
|
assigned_to: { type: 'string', format: 'uuid', description: 'ID del técnico asignado' },
|
||||||
notes: { type: 'string' },
|
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',
|
name: 'get_service_order',
|
||||||
description: 'Consulta el estado de un pedido',
|
description: 'Consulta una orden de servicio por ID o número de orden',
|
||||||
category: 'orders',
|
category: 'orders',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
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',
|
name: 'update_order_status',
|
||||||
description: 'Actualiza el estado de un pedido',
|
description: 'Actualiza el estado de una orden de servicio',
|
||||||
category: 'orders',
|
category: 'orders',
|
||||||
permissions: ['orders.update'],
|
permissions: ['orders.update'],
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
order_id: { type: 'string', format: 'uuid' },
|
order_id: { type: 'string', format: 'uuid', description: 'ID de la orden' },
|
||||||
status: {
|
status: {
|
||||||
type: 'string',
|
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'],
|
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 {
|
getHandler(toolName: string): McpToolHandler | undefined {
|
||||||
const handlers: Record<string, McpToolHandler> = {
|
const handlers: Record<string, McpToolHandler> = {
|
||||||
create_order: this.createOrder.bind(this),
|
create_service_order: this.createServiceOrder.bind(this),
|
||||||
get_order_status: this.getOrderStatus.bind(this),
|
get_service_order: this.getServiceOrder.bind(this),
|
||||||
|
list_service_orders: this.listServiceOrders.bind(this),
|
||||||
update_order_status: this.updateOrderStatus.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];
|
return handlers[toolName];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createOrder(
|
private async createServiceOrder(
|
||||||
params: { customer_id: string; items: any[]; payment_method?: string; notes?: string },
|
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
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// TODO: Connect to actual orders service
|
const dto: CreateServiceOrderDto = {
|
||||||
const subtotal = params.items.reduce((sum, item) => sum + (item.quantity * (item.unit_price || 0)), 0);
|
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 {
|
return {
|
||||||
order_id: 'order-' + Date.now(),
|
success: true,
|
||||||
customer_id: params.customer_id,
|
order: {
|
||||||
items: params.items,
|
id: order.id,
|
||||||
subtotal,
|
order_number: order.orderNumber,
|
||||||
tax: subtotal * 0.16,
|
status: order.status,
|
||||||
total: subtotal * 1.16,
|
priority: order.priority,
|
||||||
payment_method: params.payment_method || 'cash',
|
customer_id: order.customerId,
|
||||||
status: 'pending',
|
vehicle_id: order.vehicleId,
|
||||||
created_by: context.userId,
|
received_at: order.receivedAt,
|
||||||
created_at: new Date().toISOString(),
|
created_by: order.createdBy,
|
||||||
message: 'Conectar a OrdersService real',
|
},
|
||||||
|
message: `Orden de servicio ${order.orderNumber} creada exitosamente`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getOrderStatus(
|
private async getServiceOrder(
|
||||||
params: { order_id: string },
|
params: { order_id?: string; order_number?: string },
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 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 {
|
return {
|
||||||
order_id: params.order_id,
|
success: true,
|
||||||
status: 'pending',
|
order: {
|
||||||
customer_name: 'Cliente ejemplo',
|
id: order.id,
|
||||||
total: 1160.00,
|
order_number: order.orderNumber,
|
||||||
items_count: 3,
|
status: order.status,
|
||||||
created_at: new Date().toISOString(),
|
priority: order.priority,
|
||||||
message: 'Conectar a OrdersService real',
|
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<any> {
|
||||||
|
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 },
|
params: { order_id: string; status: string },
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 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 {
|
return {
|
||||||
order_id: params.order_id,
|
success: true,
|
||||||
previous_status: 'pending',
|
order: {
|
||||||
new_status: params.status,
|
id: order.id,
|
||||||
updated_by: context.userId,
|
order_number: order.orderNumber,
|
||||||
updated_at: new Date().toISOString(),
|
previous_status: order.status,
|
||||||
message: 'Conectar a OrdersService real',
|
new_status: params.status,
|
||||||
|
updated_at: order.updatedAt,
|
||||||
|
},
|
||||||
|
message: `Estado actualizado a ${params.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrdersKanban(
|
||||||
|
params: {},
|
||||||
|
context: McpContext
|
||||||
|
): Promise<any> {
|
||||||
|
const grouped = await this.serviceOrderService.getOrdersByStatus(context.tenantId);
|
||||||
|
|
||||||
|
// Transform to simpler format for AI consumption
|
||||||
|
const kanban: Record<string, any[]> = {};
|
||||||
|
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<any> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user