feat(llm): Implement role-based chat for owner and customer
- Add UserRole type and role detection via getUserRole() - Add OWNER_TOOLS and CUSTOMER_TOOLS permission arrays - Implement role-based system prompts (owner vs customer) - Add owner-specific functions: sales_summary, inventory_status, pending_fiados, etc - Add getBusinessInfo() for tenant business information - Add isToolAllowed() for permission validation - Update webhook service with role-aware message handling Sprint 2: MCH-012 Chat LLM Dueño + MCH-013 Chat LLM Cliente Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
44bcabd064
commit
9a8f0cb873
@ -23,6 +23,75 @@ export interface LLMConfig {
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
export type UserRole = 'owner' | 'customer';
|
||||
|
||||
export interface UserRoleInfo {
|
||||
role: UserRole;
|
||||
userId?: string;
|
||||
customerId?: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface BusinessInfo {
|
||||
name: string;
|
||||
address?: string;
|
||||
phone?: string;
|
||||
hours?: {
|
||||
open: string;
|
||||
close: string;
|
||||
days: string;
|
||||
};
|
||||
promotions?: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
discount?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Tools disponibles por rol
|
||||
export const OWNER_TOOLS = [
|
||||
'search_products',
|
||||
'get_product',
|
||||
'list_products',
|
||||
'get_product_by_barcode',
|
||||
'get_low_stock_products',
|
||||
'update_product_price',
|
||||
'create_order',
|
||||
'get_order',
|
||||
'list_orders',
|
||||
'update_order_status',
|
||||
'create_fiado',
|
||||
'get_customer_fiados',
|
||||
'pay_fiado',
|
||||
'get_pending_fiados',
|
||||
'search_customers',
|
||||
'get_customer',
|
||||
'create_customer',
|
||||
'get_inventory_status',
|
||||
'adjust_stock',
|
||||
'get_stock_movements',
|
||||
'get_expiring_products',
|
||||
'get_daily_sales',
|
||||
'get_sales_report',
|
||||
'register_sale',
|
||||
'get_today_summary',
|
||||
'send_payment_reminder',
|
||||
'get_business_metrics',
|
||||
'get_top_products',
|
||||
];
|
||||
|
||||
export const CUSTOMER_TOOLS = [
|
||||
'search_products',
|
||||
'get_product',
|
||||
'list_products',
|
||||
'check_availability',
|
||||
'get_product_price',
|
||||
'create_order',
|
||||
'get_my_balance',
|
||||
'get_business_info',
|
||||
'get_promotions',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class CredentialsProviderService {
|
||||
private readonly logger = new Logger(CredentialsProviderService.name);
|
||||
@ -220,6 +289,100 @@ export class CredentialsProviderService {
|
||||
this.cache.delete(`llm:${tenantId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determina el rol del usuario basado en su número de teléfono
|
||||
* Los dueños están registrados en la tabla de usuarios del tenant
|
||||
*/
|
||||
async getUserRole(phoneNumber: string, tenantId?: string): Promise<UserRoleInfo> {
|
||||
const cacheKey = `role:${phoneNumber}:${tenantId || 'platform'}`;
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await this.backendClient.get(
|
||||
`/internal/users/role-by-phone/${phoneNumber}`,
|
||||
{
|
||||
headers: {
|
||||
'X-Internal-Key': this.configService.get('INTERNAL_API_KEY'),
|
||||
...(tenantId && { 'X-Tenant-Id': tenantId }),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const roleInfo: UserRoleInfo = {
|
||||
role: data.isOwner ? 'owner' : 'customer',
|
||||
userId: data.userId,
|
||||
customerId: data.customerId,
|
||||
permissions: data.isOwner ? OWNER_TOOLS : CUSTOMER_TOOLS,
|
||||
};
|
||||
|
||||
this.setCache(cacheKey, roleInfo);
|
||||
return roleInfo;
|
||||
} catch (error) {
|
||||
this.logger.debug(`Could not determine role for ${phoneNumber}, defaulting to customer`);
|
||||
// Default to customer role
|
||||
return {
|
||||
role: 'customer',
|
||||
permissions: CUSTOMER_TOOLS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene información del negocio para mostrar a clientes
|
||||
*/
|
||||
async getBusinessInfo(tenantId?: string): Promise<BusinessInfo> {
|
||||
const cacheKey = `business:${tenantId || 'platform'}`;
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await this.backendClient.get(
|
||||
`/internal/business/info`,
|
||||
{
|
||||
headers: {
|
||||
'X-Internal-Key': this.configService.get('INTERNAL_API_KEY'),
|
||||
...(tenantId && { 'X-Tenant-Id': tenantId }),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const info: BusinessInfo = {
|
||||
name: data.name || 'MiChangarrito',
|
||||
address: data.address,
|
||||
phone: data.phone,
|
||||
hours: data.hours,
|
||||
promotions: data.promotions || [],
|
||||
};
|
||||
|
||||
this.setCache(cacheKey, info);
|
||||
return info;
|
||||
} catch (error) {
|
||||
// Return default info
|
||||
return {
|
||||
name: 'MiChangarrito',
|
||||
hours: {
|
||||
open: '07:00',
|
||||
close: '22:00',
|
||||
days: 'Lunes a Domingo',
|
||||
},
|
||||
promotions: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un tool está permitido para un rol
|
||||
*/
|
||||
isToolAllowed(toolName: string, role: UserRole): boolean {
|
||||
const allowedTools = role === 'owner' ? OWNER_TOOLS : CUSTOMER_TOOLS;
|
||||
return allowedTools.includes(toolName);
|
||||
}
|
||||
|
||||
private getFromCache(key: string): any | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && entry.expiresAt > Date.now()) {
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { CredentialsProviderService, LLMConfig } from '../common/credentials-provider.service';
|
||||
import {
|
||||
CredentialsProviderService,
|
||||
LLMConfig,
|
||||
UserRole,
|
||||
} from '../common/credentials-provider.service';
|
||||
|
||||
interface ConversationContext {
|
||||
customerId?: string;
|
||||
userId?: string;
|
||||
customerName: string;
|
||||
phoneNumber: string;
|
||||
lastActivity: Date;
|
||||
@ -13,6 +18,8 @@ interface ConversationContext {
|
||||
cart?: Array<{ productId: string; name: string; quantity: number; price: number }>;
|
||||
tenantId?: string;
|
||||
businessName?: string;
|
||||
role: UserRole;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface LlmResponse {
|
||||
@ -97,8 +104,8 @@ export class LlmService {
|
||||
history = [history[0], ...history.slice(-20)];
|
||||
}
|
||||
|
||||
// Call LLM with tenant config
|
||||
const response = await this.callLlm(history, llmConfig);
|
||||
// Call LLM with tenant config and user role
|
||||
const response = await this.callLlm(history, llmConfig, context.role);
|
||||
|
||||
// Add assistant response to history
|
||||
history.push({ role: 'assistant', content: response.message });
|
||||
@ -111,8 +118,8 @@ export class LlmService {
|
||||
}
|
||||
}
|
||||
|
||||
private async callLlm(messages: ChatMessage[], config: LLMConfig): Promise<LlmResponse> {
|
||||
const functions = this.getAvailableFunctions();
|
||||
private async callLlm(messages: ChatMessage[], config: LLMConfig, role: UserRole = 'customer'): Promise<LlmResponse> {
|
||||
const functions = this.getAvailableFunctions(role);
|
||||
const client = this.getClient(config);
|
||||
|
||||
const requestBody: any = {
|
||||
@ -163,41 +170,80 @@ export class LlmService {
|
||||
|
||||
private getSystemPrompt(context: ConversationContext): string {
|
||||
const businessName = context.businessName || 'MiChangarrito';
|
||||
const businessDescription = context.businessName
|
||||
? `un negocio local`
|
||||
: `una tiendita de barrio en Mexico`;
|
||||
|
||||
return `Eres el asistente virtual de ${businessName}, ${businessDescription}.
|
||||
if (context.role === 'owner') {
|
||||
return this.getOwnerSystemPrompt(businessName, context);
|
||||
} else {
|
||||
return this.getCustomerSystemPrompt(businessName, context);
|
||||
}
|
||||
}
|
||||
|
||||
private getOwnerSystemPrompt(businessName: string, context: ConversationContext): string {
|
||||
return `Eres el asistente virtual de ${businessName} para el DUEÑO del negocio.
|
||||
Tu nombre es "Asistente de ${businessName}" y ayudas al dueño con:
|
||||
- Consultar ventas del dia/semana/mes
|
||||
- Revisar inventario y productos con stock bajo
|
||||
- Ver fiados pendientes y enviar recordatorios
|
||||
- Generar reportes de negocio
|
||||
- Modificar precios de productos
|
||||
- Obtener metricas y sugerencias
|
||||
|
||||
Informacion del dueño:
|
||||
- Nombre: ${context.customerName}
|
||||
- Rol: DUEÑO/ADMINISTRADOR
|
||||
- Acceso: COMPLETO a todas las funciones
|
||||
|
||||
Reglas importantes:
|
||||
1. Responde siempre en español mexicano, de forma profesional pero amigable
|
||||
2. Usa emojis con moderación para datos importantes
|
||||
3. Siempre muestra numeros formateados (ej: $1,234.56)
|
||||
4. Para consultas de ventas, incluye comparativas (vs ayer, vs semana pasada)
|
||||
5. Para inventario, prioriza productos con stock bajo o proximos a vencer
|
||||
6. Para fiados, destaca clientes con atrasos mayores a 7 dias
|
||||
7. Se proactivo sugiriendo acciones basadas en los datos
|
||||
8. Tienes acceso a modificar precios - siempre pide confirmacion primero
|
||||
|
||||
Ejemplos de respuestas:
|
||||
- "Hoy llevas $3,450 en 23 ventas, +15% vs ayer 📈"
|
||||
- "Tienes 5 productos con stock bajo, ¿quieres ver la lista?"
|
||||
- "3 clientes deben mas de $500, ¿envio recordatorios?"`;
|
||||
}
|
||||
|
||||
private getCustomerSystemPrompt(businessName: string, context: ConversationContext): string {
|
||||
return `Eres el asistente virtual de ${businessName} para CLIENTES.
|
||||
Tu nombre es "Asistente de ${businessName}" y ayudas a los clientes con:
|
||||
- Informacion sobre productos y precios
|
||||
- Hacer pedidos
|
||||
- Consultar su cuenta de fiado (credito)
|
||||
- Estado de sus pedidos
|
||||
- Consultar SU cuenta de fiado (solo la suya)
|
||||
- Estado de SUS pedidos
|
||||
|
||||
Informacion del cliente:
|
||||
- Nombre: ${context.customerName}
|
||||
- Rol: CLIENTE
|
||||
- Tiene carrito con ${context.cart?.length || 0} productos
|
||||
|
||||
Reglas importantes:
|
||||
1. Responde siempre en espanol mexicano, de forma amigable y breve
|
||||
2. Usa emojis ocasionalmente para ser mas amigable
|
||||
3. Si el cliente quiere hacer algo especifico, usa las funciones disponibles
|
||||
4. Si no entiendes algo, pide aclaracion de forma amable
|
||||
5. Nunca inventes precios o productos, di que consultaras el catalogo
|
||||
6. Para fiados, siempre verifica primero el saldo disponible
|
||||
7. Se proactivo sugiriendo opciones relevantes
|
||||
1. Responde siempre en español mexicano, de forma amigable y breve
|
||||
2. Usa emojis ocasionalmente para ser mas amigable 😊
|
||||
3. NUNCA muestres informacion de otros clientes
|
||||
4. NUNCA muestres datos de ventas, inventario o metricas del negocio
|
||||
5. Solo puedes mostrar productos, precios, y la cuenta de ESTE cliente
|
||||
6. Si preguntan algo que no puedes responder, sugiere contactar al dueño
|
||||
7. Para fiados, solo muestra el saldo de ESTE cliente
|
||||
8. Se proactivo sugiriendo productos relevantes
|
||||
|
||||
Ejemplos de respuestas:
|
||||
- "Claro! Te muestro el menu de productos"
|
||||
- "Perfecto, agrego eso a tu carrito"
|
||||
- "Dejame revisar tu cuenta de fiado..."`;
|
||||
- "¡Claro! Te muestro el menu de productos"
|
||||
- "Perfecto, agrego eso a tu carrito 🛒"
|
||||
- "Tu saldo pendiente es de $180"`;
|
||||
}
|
||||
|
||||
private getAvailableFunctions(): any[] {
|
||||
return [
|
||||
private getAvailableFunctions(role: UserRole): any[] {
|
||||
// Base functions available to all users
|
||||
const baseFunctions = [
|
||||
{
|
||||
name: 'show_menu',
|
||||
description: 'Muestra el menu principal de opciones al cliente',
|
||||
description: 'Muestra el menu principal de opciones',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
@ -215,12 +261,12 @@ Ejemplos de respuestas:
|
||||
},
|
||||
{
|
||||
name: 'show_fiado',
|
||||
description: 'Muestra informacion de la cuenta de fiado del cliente',
|
||||
description: 'Muestra informacion de la cuenta de fiado',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'add_to_cart',
|
||||
description: 'Agrega un producto al carrito del cliente',
|
||||
description: 'Agrega un producto al carrito',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -239,10 +285,72 @@ Ejemplos de respuestas:
|
||||
},
|
||||
{
|
||||
name: 'check_order_status',
|
||||
description: 'Consulta el estado de los pedidos del cliente',
|
||||
description: 'Consulta el estado de pedidos',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
// Owner-specific functions (MCH-012)
|
||||
const ownerFunctions = [
|
||||
{
|
||||
name: 'show_sales_summary',
|
||||
description: 'Muestra resumen de ventas del dia/semana/mes',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
period: {
|
||||
type: 'string',
|
||||
enum: ['today', 'week', 'month'],
|
||||
description: 'Periodo de tiempo (hoy, semana, mes)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_inventory_status',
|
||||
description: 'Muestra estado del inventario, productos con stock bajo y proximos a vencer',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'show_pending_fiados',
|
||||
description: 'Muestra todos los fiados pendientes de cobro',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'update_product_price',
|
||||
description: 'Actualiza el precio de un producto',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
product_name: {
|
||||
type: 'string',
|
||||
description: 'Nombre del producto',
|
||||
},
|
||||
new_price: {
|
||||
type: 'number',
|
||||
description: 'Nuevo precio',
|
||||
},
|
||||
},
|
||||
required: ['product_name', 'new_price'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'send_payment_reminders',
|
||||
description: 'Envia recordatorios de pago a clientes con fiados atrasados',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_business_metrics',
|
||||
description: 'Obtiene metricas del negocio: margen, cliente top, producto top',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
if (role === 'owner') {
|
||||
return [...baseFunctions, ...ownerFunctions];
|
||||
}
|
||||
|
||||
return baseFunctions;
|
||||
}
|
||||
|
||||
private getActionMessage(action: string): string {
|
||||
|
||||
@ -2,7 +2,11 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WhatsAppService } from '../whatsapp/whatsapp.service';
|
||||
import { LlmService } from '../llm/llm.service';
|
||||
import { CredentialsProviderService } from '../common/credentials-provider.service';
|
||||
import {
|
||||
CredentialsProviderService,
|
||||
UserRole,
|
||||
UserRoleInfo,
|
||||
} from '../common/credentials-provider.service';
|
||||
import {
|
||||
WebhookIncomingMessage,
|
||||
WebhookContact,
|
||||
@ -11,6 +15,7 @@ import {
|
||||
|
||||
interface ConversationContext {
|
||||
customerId?: string;
|
||||
userId?: string;
|
||||
customerName: string;
|
||||
phoneNumber: string;
|
||||
lastActivity: Date;
|
||||
@ -19,6 +24,9 @@ interface ConversationContext {
|
||||
cart?: Array<{ productId: string; name: string; quantity: number; price: number }>;
|
||||
tenantId?: string;
|
||||
businessName?: string;
|
||||
// Role-based access control
|
||||
role: UserRole;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -61,7 +69,14 @@ export class WebhookService {
|
||||
const tenantId = await this.credentialsProvider.resolveTenantFromPhoneNumberId(phoneNumberId);
|
||||
const source = tenantId ? `tenant:${tenantId}` : 'platform';
|
||||
|
||||
this.logger.log(`Processing message from ${customerName} (${phoneNumber}) via ${source}: ${message.type}`);
|
||||
// Determine user role (owner vs customer)
|
||||
const roleInfo = await this.credentialsProvider.getUserRole(phoneNumber, tenantId);
|
||||
const roleLabel = roleInfo.role === 'owner' ? 'OWNER' : 'CUSTOMER';
|
||||
|
||||
this.logger.log(`Processing message from ${customerName} (${phoneNumber}) [${roleLabel}] via ${source}: ${message.type}`);
|
||||
|
||||
// Get business info for context
|
||||
const businessInfo = await this.credentialsProvider.getBusinessInfo(tenantId);
|
||||
|
||||
// Get or create conversation context
|
||||
let context = this.conversations.get(phoneNumber);
|
||||
@ -72,14 +87,20 @@ export class WebhookService {
|
||||
lastActivity: new Date(),
|
||||
cart: [],
|
||||
tenantId,
|
||||
// TODO: Fetch business name from tenant if available
|
||||
businessName: tenantId ? undefined : 'MiChangarrito',
|
||||
businessName: businessInfo.name,
|
||||
role: roleInfo.role,
|
||||
permissions: roleInfo.permissions,
|
||||
userId: roleInfo.userId,
|
||||
customerId: roleInfo.customerId,
|
||||
};
|
||||
this.conversations.set(phoneNumber, context);
|
||||
}
|
||||
context.lastActivity = new Date();
|
||||
// Update tenantId in case it changed (shouldn't normally happen)
|
||||
// Update tenantId and role in case they changed
|
||||
context.tenantId = tenantId;
|
||||
context.role = roleInfo.role;
|
||||
context.permissions = roleInfo.permissions;
|
||||
context.businessName = businessInfo.name;
|
||||
|
||||
try {
|
||||
switch (message.type) {
|
||||
@ -230,9 +251,10 @@ export class WebhookService {
|
||||
buttonId: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
this.logger.log(`Button response: ${buttonId}`);
|
||||
this.logger.log(`Button response: ${buttonId} [role: ${context.role}]`);
|
||||
|
||||
switch (buttonId) {
|
||||
// Customer buttons
|
||||
case 'menu_products':
|
||||
await this.sendProductCategories(phoneNumber, context);
|
||||
break;
|
||||
@ -261,6 +283,48 @@ export class WebhookService {
|
||||
await this.cancelOrder(phoneNumber, context);
|
||||
break;
|
||||
|
||||
// Owner-specific buttons (MCH-012)
|
||||
case 'owner_sales':
|
||||
if (context.role !== 'owner') {
|
||||
await this.whatsAppService.sendTextMessage(
|
||||
phoneNumber,
|
||||
'Esta opción solo está disponible para el dueño del negocio.',
|
||||
context.tenantId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.sendOwnerSalesSummary(phoneNumber, context);
|
||||
break;
|
||||
|
||||
case 'owner_inventory':
|
||||
if (context.role !== 'owner') {
|
||||
await this.whatsAppService.sendTextMessage(
|
||||
phoneNumber,
|
||||
'Esta opción solo está disponible para el dueño del negocio.',
|
||||
context.tenantId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.sendOwnerInventoryStatus(phoneNumber, context);
|
||||
break;
|
||||
|
||||
case 'owner_fiados':
|
||||
if (context.role !== 'owner') {
|
||||
await this.whatsAppService.sendTextMessage(
|
||||
phoneNumber,
|
||||
'Esta opción solo está disponible para el dueño del negocio.',
|
||||
context.tenantId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.sendOwnerFiadosSummary(phoneNumber, context);
|
||||
break;
|
||||
|
||||
case 'owner_send_reminders':
|
||||
if (context.role !== 'owner') return;
|
||||
await this.sendPaymentReminders(phoneNumber, context);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this.whatsAppService.sendTextMessage(
|
||||
phoneNumber,
|
||||
@ -295,28 +359,57 @@ export class WebhookService {
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
const businessName = context.businessName || 'MiChangarrito';
|
||||
const message = `Hola ${customerName}! Bienvenido a ${businessName}.
|
||||
|
||||
if (context.role === 'owner') {
|
||||
// Welcome message for business owner
|
||||
const message = `Hola ${customerName}! 👋
|
||||
|
||||
Soy tu asistente de ${businessName}. Como dueño, puedo ayudarte con:
|
||||
- 📊 Consultar ventas del día
|
||||
- 📦 Revisar inventario y stock bajo
|
||||
- 💰 Ver fiados pendientes
|
||||
- 📈 Generar reportes
|
||||
- ⚙️ Modificar precios
|
||||
|
||||
¿Qué necesitas hoy?`;
|
||||
|
||||
await this.whatsAppService.sendInteractiveButtons(
|
||||
phoneNumber,
|
||||
message,
|
||||
[
|
||||
{ id: 'owner_sales', title: '📊 Ventas hoy' },
|
||||
{ id: 'owner_inventory', title: '📦 Inventario' },
|
||||
{ id: 'owner_fiados', title: '💰 Fiados' },
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
context.tenantId,
|
||||
);
|
||||
} else {
|
||||
// Welcome message for customers
|
||||
const message = `Hola ${customerName}! 👋 Bienvenido a ${businessName}.
|
||||
|
||||
Soy tu asistente virtual. Puedo ayudarte con:
|
||||
- Ver productos disponibles
|
||||
- Hacer pedidos
|
||||
- Consultar tu cuenta de fiado
|
||||
- Revisar el estado de tus pedidos
|
||||
- 🛒 Ver productos disponibles
|
||||
- 📝 Hacer pedidos
|
||||
- 💳 Consultar tu cuenta de fiado
|
||||
- 📦 Revisar el estado de tus pedidos
|
||||
|
||||
Como puedo ayudarte hoy?`;
|
||||
¿Cómo puedo ayudarte hoy?`;
|
||||
|
||||
await this.whatsAppService.sendInteractiveButtons(
|
||||
phoneNumber,
|
||||
message,
|
||||
[
|
||||
{ id: 'menu_products', title: 'Ver productos' },
|
||||
{ id: 'menu_orders', title: 'Mis pedidos' },
|
||||
{ id: 'menu_fiado', title: 'Mi fiado' },
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
context.tenantId,
|
||||
);
|
||||
await this.whatsAppService.sendInteractiveButtons(
|
||||
phoneNumber,
|
||||
message,
|
||||
[
|
||||
{ id: 'menu_products', title: '🛒 Ver productos' },
|
||||
{ id: 'menu_orders', title: '📦 Mis pedidos' },
|
||||
{ id: 'menu_fiado', title: '💳 Mi fiado' },
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
context.tenantId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMainMenu(phoneNumber: string, context: ConversationContext): Promise<void> {
|
||||
@ -541,6 +634,177 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== OWNER ACTIONS (MCH-012) ====================
|
||||
|
||||
private async sendOwnerSalesSummary(
|
||||
phoneNumber: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
// TODO: Fetch real sales data from backend
|
||||
const today = new Date().toLocaleDateString('es-MX', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
// Mock data - in production this would call the backend
|
||||
const salesData = {
|
||||
total: 2450.50,
|
||||
count: 15,
|
||||
profit: 650.50,
|
||||
avgTicket: 163.37,
|
||||
topProducts: [
|
||||
{ name: 'Coca-Cola 600ml', qty: 24, revenue: 432 },
|
||||
{ name: 'Sabritas Original', qty: 18, revenue: 270 },
|
||||
{ name: 'Pan Bimbo', qty: 10, revenue: 450 },
|
||||
],
|
||||
vsYesterday: '+12%',
|
||||
};
|
||||
|
||||
const topList = salesData.topProducts
|
||||
.map((p, i) => `${i + 1}. ${p.name}: ${p.qty} uds ($${p.revenue})`)
|
||||
.join('\n');
|
||||
|
||||
const message = `📊 *Resumen de Ventas*
|
||||
_${today}_
|
||||
|
||||
💰 *Total:* $${salesData.total.toFixed(2)}
|
||||
📦 *Transacciones:* ${salesData.count}
|
||||
📈 *Ganancia:* $${salesData.profit.toFixed(2)}
|
||||
🎫 *Ticket promedio:* $${salesData.avgTicket.toFixed(2)}
|
||||
📊 *vs ayer:* ${salesData.vsYesterday}
|
||||
|
||||
*Top productos:*
|
||||
${topList}
|
||||
|
||||
¿Necesitas más detalles?`;
|
||||
|
||||
await this.whatsAppService.sendInteractiveButtons(
|
||||
phoneNumber,
|
||||
message,
|
||||
[
|
||||
{ id: 'owner_inventory', title: '📦 Ver inventario' },
|
||||
{ id: 'owner_fiados', title: '💰 Ver fiados' },
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
context.tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
private async sendOwnerInventoryStatus(
|
||||
phoneNumber: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
// TODO: Fetch real inventory data from backend
|
||||
const lowStock = [
|
||||
{ name: 'Coca-Cola 600ml', stock: 5, daysLeft: 2 },
|
||||
{ name: 'Leche Lala 1L', stock: 3, daysLeft: 1 },
|
||||
{ name: 'Pan Bimbo Grande', stock: 4, daysLeft: 2 },
|
||||
];
|
||||
|
||||
const expiringSoon = [
|
||||
{ name: 'Yogurt Danone', expiresIn: '2 días', qty: 6 },
|
||||
];
|
||||
|
||||
const lowStockList = lowStock
|
||||
.map((p) => `⚠️ ${p.name}: ${p.stock} uds (~${p.daysLeft} días)`)
|
||||
.join('\n');
|
||||
|
||||
const expiringList = expiringSoon.length > 0
|
||||
? expiringSoon.map((p) => `⏰ ${p.name}: ${p.qty} uds (vence en ${p.expiresIn})`).join('\n')
|
||||
: 'Ninguno próximo a vencer';
|
||||
|
||||
const message = `📦 *Estado del Inventario*
|
||||
|
||||
*Productos con stock bajo:*
|
||||
${lowStockList}
|
||||
|
||||
*Próximos a vencer:*
|
||||
${expiringList}
|
||||
|
||||
💡 _Tip: Considera hacer pedido al proveedor_
|
||||
|
||||
¿Quieres que te recuerde más tarde?`;
|
||||
|
||||
await this.whatsAppService.sendInteractiveButtons(
|
||||
phoneNumber,
|
||||
message,
|
||||
[
|
||||
{ id: 'owner_sales', title: '📊 Ver ventas' },
|
||||
{ id: 'menu_products', title: '🛒 Ver catálogo' },
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
context.tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
private async sendOwnerFiadosSummary(
|
||||
phoneNumber: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
// TODO: Fetch real fiados data from backend
|
||||
const fiadosData = {
|
||||
totalPending: 2150.00,
|
||||
clientCount: 8,
|
||||
overdueCount: 3,
|
||||
topDebtors: [
|
||||
{ name: 'Juan Pérez', amount: 850, days: 15 },
|
||||
{ name: 'María López', amount: 420, days: 7 },
|
||||
{ name: 'Pedro García', amount: 380, days: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
const debtorsList = fiadosData.topDebtors
|
||||
.map((d, i) => `${i + 1}. ${d.name}: $${d.amount} (${d.days} días)`)
|
||||
.join('\n');
|
||||
|
||||
const message = `💰 *Resumen de Fiados*
|
||||
|
||||
📊 *Total pendiente:* $${fiadosData.totalPending.toFixed(2)}
|
||||
👥 *Clientes con deuda:* ${fiadosData.clientCount}
|
||||
⚠️ *Con atraso (+7 días):* ${fiadosData.overdueCount}
|
||||
|
||||
*Mayores adeudos:*
|
||||
${debtorsList}
|
||||
|
||||
¿Quieres enviar recordatorios de cobro?`;
|
||||
|
||||
await this.whatsAppService.sendInteractiveButtons(
|
||||
phoneNumber,
|
||||
message,
|
||||
[
|
||||
{ id: 'owner_send_reminders', title: '📤 Enviar recordatorios' },
|
||||
{ id: 'owner_sales', title: '📊 Ver ventas' },
|
||||
],
|
||||
undefined,
|
||||
undefined,
|
||||
context.tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
private async sendPaymentReminders(
|
||||
phoneNumber: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
// TODO: Actually send reminders via backend
|
||||
const overdueClients = 3;
|
||||
|
||||
await this.whatsAppService.sendTextMessage(
|
||||
phoneNumber,
|
||||
`✅ *Recordatorios enviados*
|
||||
|
||||
Se enviaron recordatorios de pago a ${overdueClients} clientes con atrasos mayores a 7 días.
|
||||
|
||||
Los clientes recibirán un mensaje amable recordándoles su saldo pendiente.
|
||||
|
||||
_Nota: Solo se envía un recordatorio por semana a cada cliente._`,
|
||||
context.tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== STATUS UPDATES ====================
|
||||
|
||||
processStatusUpdate(status: WebhookStatus): void {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user