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 { Module, Global } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { CredentialsProviderService } from './credentials-provider.service';
|
import { CredentialsProviderService } from './credentials-provider.service';
|
||||||
|
import { BackendApiService } from './backend-api.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
providers: [CredentialsProviderService],
|
providers: [CredentialsProviderService, BackendApiService],
|
||||||
exports: [CredentialsProviderService],
|
exports: [CredentialsProviderService, BackendApiService],
|
||||||
})
|
})
|
||||||
export class CommonModule {}
|
export class CommonModule {}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
UserRole,
|
UserRole,
|
||||||
UserRoleInfo,
|
UserRoleInfo,
|
||||||
} from '../common/credentials-provider.service';
|
} from '../common/credentials-provider.service';
|
||||||
|
import { BackendApiService } from '../common/backend-api.service';
|
||||||
import {
|
import {
|
||||||
WebhookIncomingMessage,
|
WebhookIncomingMessage,
|
||||||
WebhookContact,
|
WebhookContact,
|
||||||
@ -39,6 +40,7 @@ export class WebhookService {
|
|||||||
private readonly whatsAppService: WhatsAppService,
|
private readonly whatsAppService: WhatsAppService,
|
||||||
private readonly llmService: LlmService,
|
private readonly llmService: LlmService,
|
||||||
private readonly credentialsProvider: CredentialsProviderService,
|
private readonly credentialsProvider: CredentialsProviderService,
|
||||||
|
private readonly backendApi: BackendApiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ==================== WEBHOOK VERIFICATION ====================
|
// ==================== WEBHOOK VERIFICATION ====================
|
||||||
@ -583,22 +585,58 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
|||||||
return;
|
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
|
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}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Order created: ${order.orderNumber} for ${phoneNumber}`);
|
||||||
|
|
||||||
|
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()}`;
|
const orderNumber = `MCH-${Date.now().toString(36).toUpperCase()}`;
|
||||||
|
|
||||||
await this.whatsAppService.sendOrderConfirmation(
|
await this.whatsAppService.sendOrderConfirmation(
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
orderNumber,
|
orderNumber,
|
||||||
context.cart,
|
context.cart,
|
||||||
total,
|
subtotal,
|
||||||
context.tenantId,
|
context.tenantId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear cart
|
|
||||||
context.cart = [];
|
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(
|
private async cancelOrder(
|
||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
@ -640,7 +678,6 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
|||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: Fetch real sales data from backend
|
|
||||||
const today = new Date().toLocaleDateString('es-MX', {
|
const today = new Date().toLocaleDateString('es-MX', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -648,19 +685,8 @@ Tambien puedes escribirme de forma natural y tratare de entenderte!`;
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock data - in production this would call the backend
|
// Fetch real sales data from backend
|
||||||
const salesData = {
|
const salesData = await this.backendApi.getSalesSummary(context.tenantId);
|
||||||
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
|
const topList = salesData.topProducts
|
||||||
.map((p, i) => `${i + 1}. ${p.name}: ${p.qty} uds ($${p.revenue})`)
|
.map((p, i) => `${i + 1}. ${p.name}: ${p.qty} uds ($${p.revenue})`)
|
||||||
@ -697,23 +723,15 @@ ${topList}
|
|||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: Fetch real inventory data from backend
|
// Fetch real inventory data from backend
|
||||||
const lowStock = [
|
const inventoryData = await this.backendApi.getInventoryStatus(context.tenantId);
|
||||||
{ 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 = [
|
const lowStockList = inventoryData.lowStock.length > 0
|
||||||
{ name: 'Yogurt Danone', expiresIn: '2 días', qty: 6 },
|
? inventoryData.lowStock.map((p) => `⚠️ ${p.name}: ${p.stock} uds (~${p.daysLeft} días)`).join('\n')
|
||||||
];
|
: 'Todos los productos tienen stock suficiente';
|
||||||
|
|
||||||
const lowStockList = lowStock
|
const expiringList = inventoryData.expiringSoon.length > 0
|
||||||
.map((p) => `⚠️ ${p.name}: ${p.stock} uds (~${p.daysLeft} días)`)
|
? inventoryData.expiringSoon.map((p) => `⏰ ${p.name}: ${p.qty} uds (vence en ${p.expiresIn})`).join('\n')
|
||||||
.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';
|
: 'Ninguno próximo a vencer';
|
||||||
|
|
||||||
const message = `📦 *Estado del Inventario*
|
const message = `📦 *Estado del Inventario*
|
||||||
@ -745,21 +763,12 @@ ${expiringList}
|
|||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: Fetch real fiados data from backend
|
// Fetch real fiados data from backend
|
||||||
const fiadosData = {
|
const fiadosData = await this.backendApi.getFiadosSummary(context.tenantId);
|
||||||
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
|
const debtorsList = fiadosData.topDebtors.length > 0
|
||||||
.map((d, i) => `${i + 1}. ${d.name}: $${d.amount} (${d.days} días)`)
|
? fiadosData.topDebtors.map((d, i) => `${i + 1}. ${d.name}: $${d.amount} (${d.days} días)`).join('\n')
|
||||||
.join('\n');
|
: 'No hay adeudos pendientes';
|
||||||
|
|
||||||
const message = `💰 *Resumen de Fiados*
|
const message = `💰 *Resumen de Fiados*
|
||||||
|
|
||||||
@ -789,14 +798,14 @@ ${debtorsList}
|
|||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: Actually send reminders via backend
|
// Send reminders via backend
|
||||||
const overdueClients = 3;
|
const sentCount = await this.backendApi.sendPaymentReminders(context.tenantId);
|
||||||
|
|
||||||
await this.whatsAppService.sendTextMessage(
|
await this.whatsAppService.sendTextMessage(
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
`✅ *Recordatorios enviados*
|
`✅ *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.
|
Los clientes recibirán un mensaje amable recordándoles su saldo pendiente.
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user