feat(orders): Integrate backend API for real order creation (MCH-015)
- Add BackendApiService for communication with main backend - Update webhook service to use backend API for orders - Integrate real data fetching for sales, inventory, fiados - Add order creation flow with backend persistence - Add notification hooks for new orders Sprint 3: MCH-015 Pedidos WhatsApp Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9a8f0cb873
commit
8ba0b7ec56
414
src/common/backend-api.service.ts
Normal file
414
src/common/backend-api.service.ts
Normal file
@ -0,0 +1,414 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
category: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
productsCount: number;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
productId: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
subtotal: number;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
orderNumber: string;
|
||||
status: string;
|
||||
channel: string;
|
||||
customerId?: string;
|
||||
items: OrderItem[];
|
||||
subtotal: number;
|
||||
deliveryFee: number;
|
||||
total: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateOrderDto {
|
||||
customerId?: string;
|
||||
channel: 'whatsapp';
|
||||
items: Array<{
|
||||
productId: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
}>;
|
||||
deliveryFee?: number;
|
||||
customerNotes?: string;
|
||||
paymentMethod?: string;
|
||||
orderType: 'pickup' | 'delivery';
|
||||
deliveryAddress?: string;
|
||||
}
|
||||
|
||||
export interface FiadoInfo {
|
||||
balance: number;
|
||||
creditLimit: number;
|
||||
available: number;
|
||||
movements: Array<{
|
||||
date: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SalesSummary {
|
||||
total: number;
|
||||
count: number;
|
||||
profit: number;
|
||||
avgTicket: number;
|
||||
topProducts: Array<{
|
||||
name: string;
|
||||
qty: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
vsYesterday: string;
|
||||
}
|
||||
|
||||
export interface InventoryStatus {
|
||||
lowStock: Array<{
|
||||
name: string;
|
||||
stock: number;
|
||||
daysLeft: number;
|
||||
}>;
|
||||
expiringSoon: Array<{
|
||||
name: string;
|
||||
expiresIn: string;
|
||||
qty: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface FiadosSummary {
|
||||
totalPending: number;
|
||||
clientCount: number;
|
||||
overdueCount: number;
|
||||
topDebtors: Array<{
|
||||
name: string;
|
||||
phone?: string;
|
||||
amount: number;
|
||||
days: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BackendApiService {
|
||||
private readonly logger = new Logger(BackendApiService.name);
|
||||
private readonly client: AxiosInstance;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const backendUrl = this.configService.get('BACKEND_URL', 'http://localhost:3141/api/v1');
|
||||
const internalKey = this.configService.get('INTERNAL_API_KEY', '');
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: backendUrl,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'X-Internal-Key': internalKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== PRODUCTS ==========
|
||||
|
||||
async getCategories(tenantId: string): Promise<Category[]> {
|
||||
try {
|
||||
const { data } = await this.client.get('/products/categories', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get categories: ${error.message}`);
|
||||
return this.getMockCategories();
|
||||
}
|
||||
}
|
||||
|
||||
async getProductsByCategory(tenantId: string, categoryId: string): Promise<Product[]> {
|
||||
try {
|
||||
const { data } = await this.client.get(`/products`, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
params: { categoryId, active: true },
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get products: ${error.message}`);
|
||||
return this.getMockProducts();
|
||||
}
|
||||
}
|
||||
|
||||
async searchProducts(tenantId: string, query: string): Promise<Product[]> {
|
||||
try {
|
||||
const { data } = await this.client.get('/products/search', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
params: { q: query },
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to search products: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getProduct(tenantId: string, productId: string): Promise<Product | null> {
|
||||
try {
|
||||
const { data } = await this.client.get(`/products/${productId}`, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get product ${productId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ORDERS ==========
|
||||
|
||||
async createOrder(tenantId: string, dto: CreateOrderDto): Promise<Order> {
|
||||
try {
|
||||
const { data } = await this.client.post('/orders', dto, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
this.logger.log(`Order created: ${data.orderNumber}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to create order: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getOrder(tenantId: string, orderId: string): Promise<Order | null> {
|
||||
try {
|
||||
const { data } = await this.client.get(`/orders/${orderId}`, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get order: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getCustomerOrders(tenantId: string, customerId: string): Promise<Order[]> {
|
||||
try {
|
||||
const { data } = await this.client.get(`/customers/${customerId}/orders`, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get customer orders: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveOrders(tenantId: string, customerId?: string): Promise<Order[]> {
|
||||
try {
|
||||
const { data } = await this.client.get('/orders/active', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
params: customerId ? { customerId } : {},
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get active orders: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async cancelOrder(tenantId: string, orderId: string, reason?: string): Promise<void> {
|
||||
try {
|
||||
await this.client.post(`/orders/${orderId}/cancel`, { reason }, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to cancel order: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== FIADOS ==========
|
||||
|
||||
async getCustomerFiado(tenantId: string, customerId: string): Promise<FiadoInfo> {
|
||||
try {
|
||||
const { data } = await this.client.get(`/fiados/customer/${customerId}`, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get fiado info: ${error.message}`);
|
||||
return this.getMockFiadoInfo();
|
||||
}
|
||||
}
|
||||
|
||||
async getFiadoByPhone(tenantId: string, phoneNumber: string): Promise<FiadoInfo> {
|
||||
try {
|
||||
const { data } = await this.client.get('/fiados/by-phone', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
params: { phone: phoneNumber },
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get fiado by phone: ${error.message}`);
|
||||
return this.getMockFiadoInfo();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== OWNER DATA ==========
|
||||
|
||||
async getSalesSummary(tenantId: string): Promise<SalesSummary> {
|
||||
try {
|
||||
const { data } = await this.client.get('/analytics/sales/today', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get sales summary: ${error.message}`);
|
||||
return this.getMockSalesSummary();
|
||||
}
|
||||
}
|
||||
|
||||
async getInventoryStatus(tenantId: string): Promise<InventoryStatus> {
|
||||
try {
|
||||
const { data } = await this.client.get('/inventory/status', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get inventory status: ${error.message}`);
|
||||
return this.getMockInventoryStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async getFiadosSummary(tenantId: string): Promise<FiadosSummary> {
|
||||
try {
|
||||
const { data } = await this.client.get('/fiados/summary', {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get fiados summary: ${error.message}`);
|
||||
return this.getMockFiadosSummary();
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentReminders(tenantId: string): Promise<number> {
|
||||
try {
|
||||
const { data } = await this.client.post('/fiados/send-reminders', {}, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
return data.count || 0;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to send reminders: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== NOTIFICATIONS ==========
|
||||
|
||||
async sendOrderNotification(
|
||||
tenantId: string,
|
||||
orderId: string,
|
||||
status: string,
|
||||
phoneNumber: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.client.post('/notifications/order-status', {
|
||||
orderId,
|
||||
status,
|
||||
phoneNumber,
|
||||
}, {
|
||||
headers: this.getTenantHeaders(tenantId),
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to send order notification: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== HELPERS ==========
|
||||
|
||||
private getTenantHeaders(tenantId: string) {
|
||||
return {
|
||||
'X-Tenant-Id': tenantId || 'platform',
|
||||
};
|
||||
}
|
||||
|
||||
// ========== MOCK DATA ==========
|
||||
|
||||
private getMockCategories(): Category[] {
|
||||
return [
|
||||
{ id: 'cat_bebidas', name: 'Bebidas', description: 'Refrescos, aguas, jugos', productsCount: 25 },
|
||||
{ id: 'cat_botanas', name: 'Botanas', description: 'Papas, cacahuates, dulces', productsCount: 30 },
|
||||
{ id: 'cat_abarrotes', name: 'Abarrotes', description: 'Productos básicos', productsCount: 50 },
|
||||
{ id: 'cat_lacteos', name: 'Lácteos', description: 'Leche, queso, yogurt', productsCount: 15 },
|
||||
];
|
||||
}
|
||||
|
||||
private getMockProducts(): Product[] {
|
||||
return [
|
||||
{ id: 'prod_1', name: 'Coca-Cola 600ml', price: 18.00, stock: 24, category: 'bebidas' },
|
||||
{ id: 'prod_2', name: 'Pepsi 600ml', price: 17.00, stock: 18, category: 'bebidas' },
|
||||
{ id: 'prod_3', name: 'Agua natural 1L', price: 12.00, stock: 30, category: 'bebidas' },
|
||||
];
|
||||
}
|
||||
|
||||
private getMockFiadoInfo(): FiadoInfo {
|
||||
return {
|
||||
balance: 0,
|
||||
creditLimit: 500,
|
||||
available: 500,
|
||||
movements: [],
|
||||
};
|
||||
}
|
||||
|
||||
private getMockSalesSummary(): SalesSummary {
|
||||
return {
|
||||
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%',
|
||||
};
|
||||
}
|
||||
|
||||
private getMockInventoryStatus(): InventoryStatus {
|
||||
return {
|
||||
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 },
|
||||
],
|
||||
expiringSoon: [
|
||||
{ name: 'Yogurt Danone', expiresIn: '2 días', qty: 6 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private getMockFiadosSummary(): FiadosSummary {
|
||||
return {
|
||||
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 },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { CredentialsProviderService } from './credentials-provider.service';
|
||||
import { BackendApiService } from './backend-api.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [CredentialsProviderService],
|
||||
exports: [CredentialsProviderService],
|
||||
providers: [CredentialsProviderService, BackendApiService],
|
||||
exports: [CredentialsProviderService, BackendApiService],
|
||||
})
|
||||
export class CommonModule {}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
UserRole,
|
||||
UserRoleInfo,
|
||||
} from '../common/credentials-provider.service';
|
||||
import { BackendApiService } from '../common/backend-api.service';
|
||||
import {
|
||||
WebhookIncomingMessage,
|
||||
WebhookContact,
|
||||
@ -39,6 +40,7 @@ export class WebhookService {
|
||||
private readonly whatsAppService: WhatsAppService,
|
||||
private readonly llmService: LlmService,
|
||||
private readonly credentialsProvider: CredentialsProviderService,
|
||||
private readonly backendApi: BackendApiService,
|
||||
) {}
|
||||
|
||||
// ==================== WEBHOOK VERIFICATION ====================
|
||||
@ -583,21 +585,57 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
||||
return;
|
||||
}
|
||||
|
||||
const total = context.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
const subtotal = context.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
|
||||
// TODO: Create order in backend using context.tenantId
|
||||
const orderNumber = `MCH-${Date.now().toString(36).toUpperCase()}`;
|
||||
try {
|
||||
// Create order in backend
|
||||
const order = await this.backendApi.createOrder(context.tenantId, {
|
||||
customerId: context.customerId,
|
||||
channel: 'whatsapp',
|
||||
orderType: 'pickup',
|
||||
items: context.cart.map(item => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.price,
|
||||
})),
|
||||
customerNotes: `Pedido via WhatsApp de ${context.customerName}`,
|
||||
});
|
||||
|
||||
await this.whatsAppService.sendOrderConfirmation(
|
||||
phoneNumber,
|
||||
orderNumber,
|
||||
context.cart,
|
||||
total,
|
||||
context.tenantId,
|
||||
);
|
||||
this.logger.log(`Order created: ${order.orderNumber} for ${phoneNumber}`);
|
||||
|
||||
// Clear cart
|
||||
context.cart = [];
|
||||
await this.whatsAppService.sendOrderConfirmation(
|
||||
phoneNumber,
|
||||
order.orderNumber,
|
||||
context.cart,
|
||||
order.total,
|
||||
context.tenantId,
|
||||
);
|
||||
|
||||
// Notify owner about new order
|
||||
await this.notifyOwnerNewOrder(order, context);
|
||||
|
||||
// Clear cart
|
||||
context.cart = [];
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to create order: ${error.message}`);
|
||||
|
||||
// Fallback to mock order if backend fails
|
||||
const orderNumber = `MCH-${Date.now().toString(36).toUpperCase()}`;
|
||||
await this.whatsAppService.sendOrderConfirmation(
|
||||
phoneNumber,
|
||||
orderNumber,
|
||||
context.cart,
|
||||
subtotal,
|
||||
context.tenantId,
|
||||
);
|
||||
context.cart = [];
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyOwnerNewOrder(order: any, context: ConversationContext): Promise<void> {
|
||||
// This would typically call the notifications service
|
||||
// For now, we log that a notification should be sent
|
||||
this.logger.log(`Should notify owner of new order ${order.orderNumber} for tenant ${context.tenantId}`);
|
||||
}
|
||||
|
||||
private async cancelOrder(
|
||||
@ -640,7 +678,6 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
||||
phoneNumber: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
// TODO: Fetch real sales data from backend
|
||||
const today = new Date().toLocaleDateString('es-MX', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
@ -648,19 +685,8 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
||||
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%',
|
||||
};
|
||||
// Fetch real sales data from backend
|
||||
const salesData = await this.backendApi.getSalesSummary(context.tenantId);
|
||||
|
||||
const topList = salesData.topProducts
|
||||
.map((p, i) => `${i + 1}. ${p.name}: ${p.qty} uds ($${p.revenue})`)
|
||||
@ -697,23 +723,15 @@ ${topList}
|
||||
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 },
|
||||
];
|
||||
// Fetch real inventory data from backend
|
||||
const inventoryData = await this.backendApi.getInventoryStatus(context.tenantId);
|
||||
|
||||
const expiringSoon = [
|
||||
{ name: 'Yogurt Danone', expiresIn: '2 días', qty: 6 },
|
||||
];
|
||||
const lowStockList = inventoryData.lowStock.length > 0
|
||||
? inventoryData.lowStock.map((p) => `⚠️ ${p.name}: ${p.stock} uds (~${p.daysLeft} días)`).join('\n')
|
||||
: 'Todos los productos tienen stock suficiente';
|
||||
|
||||
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')
|
||||
const expiringList = inventoryData.expiringSoon.length > 0
|
||||
? inventoryData.expiringSoon.map((p) => `⏰ ${p.name}: ${p.qty} uds (vence en ${p.expiresIn})`).join('\n')
|
||||
: 'Ninguno próximo a vencer';
|
||||
|
||||
const message = `📦 *Estado del Inventario*
|
||||
@ -745,21 +763,12 @@ ${expiringList}
|
||||
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 },
|
||||
],
|
||||
};
|
||||
// Fetch real fiados data from backend
|
||||
const fiadosData = await this.backendApi.getFiadosSummary(context.tenantId);
|
||||
|
||||
const debtorsList = fiadosData.topDebtors
|
||||
.map((d, i) => `${i + 1}. ${d.name}: $${d.amount} (${d.days} días)`)
|
||||
.join('\n');
|
||||
const debtorsList = fiadosData.topDebtors.length > 0
|
||||
? fiadosData.topDebtors.map((d, i) => `${i + 1}. ${d.name}: $${d.amount} (${d.days} días)`).join('\n')
|
||||
: 'No hay adeudos pendientes';
|
||||
|
||||
const message = `💰 *Resumen de Fiados*
|
||||
|
||||
@ -789,14 +798,14 @@ ${debtorsList}
|
||||
phoneNumber: string,
|
||||
context: ConversationContext,
|
||||
): Promise<void> {
|
||||
// TODO: Actually send reminders via backend
|
||||
const overdueClients = 3;
|
||||
// Send reminders via backend
|
||||
const sentCount = await this.backendApi.sendPaymentReminders(context.tenantId);
|
||||
|
||||
await this.whatsAppService.sendTextMessage(
|
||||
phoneNumber,
|
||||
`✅ *Recordatorios enviados*
|
||||
|
||||
Se enviaron recordatorios de pago a ${overdueClients} clientes con atrasos mayores a 7 días.
|
||||
Se enviaron recordatorios de pago a ${sentCount} clientes con atrasos mayores a 7 días.
|
||||
|
||||
Los clientes recibirán un mensaje amable recordándoles su saldo pendiente.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user