From 9a8f0cb873d115880057ede68245f3d045932029 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Sun, 18 Jan 2026 03:08:22 -0600 Subject: [PATCH] feat(llm): Implement role-based chat for owner and customer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/common/credentials-provider.service.ts | 163 +++++++++++ src/llm/llm.service.ts | 162 +++++++++-- src/webhook/webhook.service.ts | 312 +++++++++++++++++++-- 3 files changed, 586 insertions(+), 51 deletions(-) diff --git a/src/common/credentials-provider.service.ts b/src/common/credentials-provider.service.ts index da4cccd..005c261 100644 --- a/src/common/credentials-provider.service.ts +++ b/src/common/credentials-provider.service.ts @@ -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 { + 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 { + 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()) { diff --git a/src/llm/llm.service.ts b/src/llm/llm.service.ts index b417828..80be435 100644 --- a/src/llm/llm.service.ts +++ b/src/llm/llm.service.ts @@ -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 { - const functions = this.getAvailableFunctions(); + private async callLlm(messages: ChatMessage[], config: LLMConfig, role: UserRole = 'customer'): Promise { + 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 { diff --git a/src/webhook/webhook.service.ts b/src/webhook/webhook.service.ts index 7681e5d..ad4da25 100644 --- a/src/webhook/webhook.service.ts +++ b/src/webhook/webhook.service.ts @@ -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 { - 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 { 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 { @@ -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 { + // 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 { + // 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 { + // 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 { + // 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 {