Some checks are pending
CI/CD Pipeline / Backend CI (push) Waiting to run
CI/CD Pipeline / Frontend CI (push) Waiting to run
CI/CD Pipeline / WhatsApp Service CI (push) Waiting to run
CI/CD Pipeline / Mobile CI (push) Waiting to run
CI/CD Pipeline / Docker Build (./apps/backend, ./apps/backend/Dockerfile, backend) (push) Blocked by required conditions
CI/CD Pipeline / Docker Build (./apps/frontend, ./apps/frontend/Dockerfile, frontend) (push) Blocked by required conditions
CI/CD Pipeline / Docker Build (./apps/whatsapp-service, ./apps/whatsapp-service/Dockerfile, whatsapp-service) (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
- Move 7 non-standard folders to _archive/ - Archive 3 extra root files - Update _MAP.md with standardized structure Standard: SIMCO-ESTANDAR-ORCHESTRATION v1.0.0 Level: CONSUMER (L2) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1093 lines
25 KiB
Markdown
1093 lines
25 KiB
Markdown
# MiChangarrito - Integraciones Externas
|
|
|
|
## Indice de Integraciones
|
|
|
|
| Integracion | Categoria | Prioridad | Complejidad |
|
|
|-------------|-----------|-----------|-------------|
|
|
| Stripe | Pagos/Suscripciones | P0 | Media |
|
|
| WhatsApp Business API | Mensajeria | P0 | Alta |
|
|
| OpenRouter/LLM | Inteligencia Artificial | P0 | Media |
|
|
| Mercado Pago Point | Terminal Pago | P1 | Media |
|
|
| Firebase FCM | Push Notifications | P1 | Baja |
|
|
| Clip | Terminal Pago | P2 | Media |
|
|
| CoDi (Banxico) | Pagos QR | P2 | Alta |
|
|
| Google Vision | OCR | P2 | Baja |
|
|
| OpenAI Whisper | Transcripcion | P2 | Baja |
|
|
|
|
---
|
|
|
|
## 1. Stripe
|
|
|
|
### 1.1 Proposito
|
|
- Suscripciones mensuales (Plan Changarrito, Plan Tiendita)
|
|
- Pagos con tarjeta
|
|
- Referencias de pago OXXO
|
|
- Gestion de clientes y facturacion
|
|
|
|
### 1.2 Documentacion
|
|
- Dashboard: https://dashboard.stripe.com/
|
|
- API Docs: https://docs.stripe.com/api
|
|
- Subscriptions: https://docs.stripe.com/billing/subscriptions
|
|
- OXXO: https://docs.stripe.com/payments/oxxo
|
|
|
|
### 1.3 SDK
|
|
|
|
```bash
|
|
npm install stripe
|
|
```
|
|
|
|
### 1.4 Configuracion
|
|
|
|
```typescript
|
|
// config/stripe.config.ts
|
|
import Stripe from 'stripe';
|
|
|
|
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2024-12-18.acacia',
|
|
});
|
|
|
|
export const STRIPE_CONFIG = {
|
|
prices: {
|
|
changarrito_monthly: process.env.STRIPE_PRICE_CHANGARRITO,
|
|
tiendita_monthly: process.env.STRIPE_PRICE_TIENDITA,
|
|
tokens_1000: process.env.STRIPE_PRICE_TOKENS_1000,
|
|
tokens_3000: process.env.STRIPE_PRICE_TOKENS_3000,
|
|
tokens_8000: process.env.STRIPE_PRICE_TOKENS_8000,
|
|
tokens_20000: process.env.STRIPE_PRICE_TOKENS_20000,
|
|
},
|
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
};
|
|
```
|
|
|
|
### 1.5 Flujos Principales
|
|
|
|
#### Crear Suscripcion
|
|
|
|
```typescript
|
|
async function createSubscription(
|
|
customerId: string,
|
|
priceId: string,
|
|
): Promise<Stripe.Subscription> {
|
|
return stripe.subscriptions.create({
|
|
customer: customerId,
|
|
items: [{ price: priceId }],
|
|
payment_behavior: 'default_incomplete',
|
|
payment_settings: {
|
|
save_default_payment_method: 'on_subscription',
|
|
},
|
|
expand: ['latest_invoice.payment_intent'],
|
|
});
|
|
}
|
|
```
|
|
|
|
#### Crear Pago OXXO
|
|
|
|
```typescript
|
|
async function createOxxoPayment(
|
|
amount: number, // en centavos
|
|
customerEmail: string,
|
|
customerName: string,
|
|
): Promise<Stripe.PaymentIntent> {
|
|
return stripe.paymentIntents.create({
|
|
amount,
|
|
currency: 'mxn',
|
|
payment_method_types: ['oxxo'],
|
|
payment_method_data: {
|
|
type: 'oxxo',
|
|
billing_details: {
|
|
email: customerEmail,
|
|
name: customerName,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
#### Webhook Handler
|
|
|
|
```typescript
|
|
// webhooks/stripe.webhook.ts
|
|
@Post('webhook/stripe')
|
|
async handleStripeWebhook(
|
|
@Req() req: RawBodyRequest<Request>,
|
|
@Headers('stripe-signature') signature: string,
|
|
) {
|
|
const event = stripe.webhooks.constructEvent(
|
|
req.rawBody,
|
|
signature,
|
|
STRIPE_CONFIG.webhookSecret,
|
|
);
|
|
|
|
switch (event.type) {
|
|
case 'customer.subscription.created':
|
|
case 'customer.subscription.updated':
|
|
await this.handleSubscriptionUpdate(event.data.object);
|
|
break;
|
|
|
|
case 'customer.subscription.deleted':
|
|
await this.handleSubscriptionCancelled(event.data.object);
|
|
break;
|
|
|
|
case 'invoice.paid':
|
|
await this.handleInvoicePaid(event.data.object);
|
|
break;
|
|
|
|
case 'invoice.payment_failed':
|
|
await this.handlePaymentFailed(event.data.object);
|
|
break;
|
|
|
|
case 'payment_intent.succeeded':
|
|
await this.handlePaymentSucceeded(event.data.object);
|
|
break;
|
|
}
|
|
|
|
return { received: true };
|
|
}
|
|
```
|
|
|
|
### 1.6 Webhooks a Configurar
|
|
|
|
| Evento | Accion |
|
|
|--------|--------|
|
|
| customer.subscription.created | Activar suscripcion, asignar tokens |
|
|
| customer.subscription.updated | Actualizar plan |
|
|
| customer.subscription.deleted | Cancelar acceso |
|
|
| invoice.paid | Renovar periodo, resetear tokens |
|
|
| invoice.payment_failed | Notificar, marcar past_due |
|
|
| payment_intent.succeeded | Confirmar pago OXXO/tokens |
|
|
|
|
### 1.7 Comisiones
|
|
|
|
| Metodo | Comision |
|
|
|--------|----------|
|
|
| Tarjeta nacional | 3.6% + $3 MXN + IVA |
|
|
| OXXO | 3.6% + $3 MXN + IVA |
|
|
| Tarjeta internacional | +0.5% |
|
|
|
|
---
|
|
|
|
## 2. WhatsApp Business API
|
|
|
|
### 2.1 Proposito
|
|
- Canal principal de comunicacion con duenos
|
|
- Gestion del negocio via chat con LLM
|
|
- Recepcion de pedidos de clientes
|
|
- Envio de notificaciones y recordatorios
|
|
|
|
### 2.2 Documentacion
|
|
- Meta for Developers: https://developers.facebook.com/
|
|
- Cloud API: https://developers.facebook.com/docs/whatsapp/cloud-api
|
|
- Webhooks: https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks
|
|
|
|
### 2.3 Configuracion Inicial
|
|
|
|
1. Crear app en Meta for Developers
|
|
2. Agregar producto "WhatsApp"
|
|
3. Configurar numero de telefono
|
|
4. Verificar negocio
|
|
5. Configurar webhooks
|
|
|
|
### 2.4 Endpoints Principales
|
|
|
|
```typescript
|
|
// config/whatsapp.config.ts
|
|
export const WHATSAPP_CONFIG = {
|
|
apiVersion: 'v18.0',
|
|
baseUrl: 'https://graph.facebook.com',
|
|
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID,
|
|
accessToken: process.env.WHATSAPP_ACCESS_TOKEN,
|
|
verifyToken: process.env.WHATSAPP_VERIFY_TOKEN,
|
|
};
|
|
|
|
export const getWhatsAppUrl = (endpoint: string) =>
|
|
`${WHATSAPP_CONFIG.baseUrl}/${WHATSAPP_CONFIG.apiVersion}/${WHATSAPP_CONFIG.phoneNumberId}/${endpoint}`;
|
|
```
|
|
|
|
### 2.5 Enviar Mensaje
|
|
|
|
```typescript
|
|
// services/whatsapp.service.ts
|
|
import axios from 'axios';
|
|
|
|
export class WhatsAppService {
|
|
private readonly headers = {
|
|
Authorization: `Bearer ${WHATSAPP_CONFIG.accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
// Mensaje de texto
|
|
async sendTextMessage(to: string, text: string): Promise<void> {
|
|
await axios.post(
|
|
getWhatsAppUrl('messages'),
|
|
{
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual',
|
|
to,
|
|
type: 'text',
|
|
text: { body: text },
|
|
},
|
|
{ headers: this.headers },
|
|
);
|
|
}
|
|
|
|
// Mensaje con template (para iniciar conversacion)
|
|
async sendTemplateMessage(
|
|
to: string,
|
|
templateName: string,
|
|
components?: any[],
|
|
): Promise<void> {
|
|
await axios.post(
|
|
getWhatsAppUrl('messages'),
|
|
{
|
|
messaging_product: 'whatsapp',
|
|
to,
|
|
type: 'template',
|
|
template: {
|
|
name: templateName,
|
|
language: { code: 'es_MX' },
|
|
components,
|
|
},
|
|
},
|
|
{ headers: this.headers },
|
|
);
|
|
}
|
|
|
|
// Mensaje con botones interactivos
|
|
async sendButtonMessage(
|
|
to: string,
|
|
body: string,
|
|
buttons: { id: string; title: string }[],
|
|
): Promise<void> {
|
|
await axios.post(
|
|
getWhatsAppUrl('messages'),
|
|
{
|
|
messaging_product: 'whatsapp',
|
|
to,
|
|
type: 'interactive',
|
|
interactive: {
|
|
type: 'button',
|
|
body: { text: body },
|
|
action: {
|
|
buttons: buttons.map((b) => ({
|
|
type: 'reply',
|
|
reply: { id: b.id, title: b.title },
|
|
})),
|
|
},
|
|
},
|
|
},
|
|
{ headers: this.headers },
|
|
);
|
|
}
|
|
|
|
// Enviar imagen
|
|
async sendImageMessage(
|
|
to: string,
|
|
imageUrl: string,
|
|
caption?: string,
|
|
): Promise<void> {
|
|
await axios.post(
|
|
getWhatsAppUrl('messages'),
|
|
{
|
|
messaging_product: 'whatsapp',
|
|
to,
|
|
type: 'image',
|
|
image: { link: imageUrl, caption },
|
|
},
|
|
{ headers: this.headers },
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.6 Webhook Handler
|
|
|
|
```typescript
|
|
// webhooks/whatsapp.webhook.ts
|
|
@Controller('webhook/whatsapp')
|
|
export class WhatsAppWebhookController {
|
|
// Verificacion inicial (GET)
|
|
@Get()
|
|
verify(@Query() query: any): string {
|
|
const mode = query['hub.mode'];
|
|
const token = query['hub.verify_token'];
|
|
const challenge = query['hub.challenge'];
|
|
|
|
if (mode === 'subscribe' && token === WHATSAPP_CONFIG.verifyToken) {
|
|
return challenge;
|
|
}
|
|
throw new ForbiddenException('Verification failed');
|
|
}
|
|
|
|
// Recibir mensajes (POST)
|
|
@Post()
|
|
async handleWebhook(@Body() body: any): Promise<{ status: string }> {
|
|
const entry = body.entry?.[0];
|
|
const changes = entry?.changes?.[0];
|
|
const value = changes?.value;
|
|
|
|
if (value?.messages) {
|
|
for (const message of value.messages) {
|
|
await this.processMessage(message, value.contacts?.[0]);
|
|
}
|
|
}
|
|
|
|
if (value?.statuses) {
|
|
for (const status of value.statuses) {
|
|
await this.processStatus(status);
|
|
}
|
|
}
|
|
|
|
return { status: 'ok' };
|
|
}
|
|
|
|
private async processMessage(message: any, contact: any): Promise<void> {
|
|
const from = message.from;
|
|
const messageType = message.type;
|
|
const timestamp = message.timestamp;
|
|
|
|
// Determinar si es dueno o cliente
|
|
const tenant = await this.findTenantByPhone(from);
|
|
const isOwner = !!tenant;
|
|
|
|
// Procesar segun tipo
|
|
switch (messageType) {
|
|
case 'text':
|
|
await this.handleTextMessage(from, message.text.body, isOwner, tenant);
|
|
break;
|
|
case 'image':
|
|
await this.handleImageMessage(from, message.image, isOwner, tenant);
|
|
break;
|
|
case 'audio':
|
|
await this.handleAudioMessage(from, message.audio, isOwner, tenant);
|
|
break;
|
|
case 'interactive':
|
|
await this.handleInteractiveMessage(from, message.interactive, tenant);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.7 Templates Requeridos
|
|
|
|
| Template | Proposito | Variables |
|
|
|----------|-----------|-----------|
|
|
| welcome | Bienvenida a nuevo negocio | {{1}} nombre |
|
|
| daily_summary | Resumen diario de ventas | {{1}} fecha, {{2}} total, {{3}} transacciones |
|
|
| low_stock_alert | Alerta de stock bajo | {{1}} productos |
|
|
| fiado_reminder | Recordatorio de fiado | {{1}} cliente, {{2}} monto, {{3}} dias |
|
|
| order_confirmation | Confirmacion de pedido | {{1}} numero, {{2}} total |
|
|
| payment_received | Confirmacion de pago | {{1}} monto |
|
|
|
|
### 2.8 Rate Limits
|
|
|
|
| Tier | Mensajes/dia |
|
|
|------|--------------|
|
|
| Inicial | 250 |
|
|
| Verificado | 1,000 |
|
|
| Escalado | 10,000+ |
|
|
|
|
---
|
|
|
|
## 3. OpenRouter / LLM Providers
|
|
|
|
### 3.1 Proposito
|
|
- Asistente IA para duenos (consultas, gestion)
|
|
- Atencion a clientes (pedidos, precios)
|
|
- Procesamiento de texto natural
|
|
- Soporte tecnico automatizado
|
|
|
|
### 3.2 Documentacion
|
|
- OpenRouter: https://openrouter.ai/docs
|
|
- OpenAI: https://platform.openai.com/docs
|
|
- Anthropic: https://docs.anthropic.com/
|
|
|
|
### 3.3 Configuracion Agnostica
|
|
|
|
```typescript
|
|
// config/llm.config.ts
|
|
export type LLMProvider = 'openrouter' | 'openai' | 'anthropic' | 'ollama';
|
|
|
|
export interface LLMConfig {
|
|
provider: LLMProvider;
|
|
apiKey: string;
|
|
baseUrl: string;
|
|
model: string;
|
|
maxTokens: number;
|
|
temperature: number;
|
|
}
|
|
|
|
export const getLLMConfig = (): LLMConfig => {
|
|
const provider = process.env.LLM_PROVIDER as LLMProvider;
|
|
|
|
const configs: Record<LLMProvider, Partial<LLMConfig>> = {
|
|
openrouter: {
|
|
baseUrl: 'https://openrouter.ai/api/v1',
|
|
model: 'anthropic/claude-3-haiku',
|
|
},
|
|
openai: {
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4o-mini',
|
|
},
|
|
anthropic: {
|
|
baseUrl: 'https://api.anthropic.com/v1',
|
|
model: 'claude-3-haiku-20240307',
|
|
},
|
|
ollama: {
|
|
baseUrl: 'http://localhost:11434/api',
|
|
model: 'llama3',
|
|
},
|
|
};
|
|
|
|
return {
|
|
provider,
|
|
apiKey: process.env.LLM_API_KEY,
|
|
baseUrl: process.env.LLM_BASE_URL || configs[provider].baseUrl,
|
|
model: process.env.LLM_MODEL || configs[provider].model,
|
|
maxTokens: parseInt(process.env.LLM_MAX_TOKENS || '4096'),
|
|
temperature: parseFloat(process.env.LLM_TEMPERATURE || '0.7'),
|
|
};
|
|
};
|
|
```
|
|
|
|
### 3.4 LLM Service
|
|
|
|
```typescript
|
|
// services/llm.service.ts
|
|
import OpenAI from 'openai';
|
|
|
|
export class LLMService {
|
|
private client: OpenAI;
|
|
private config: LLMConfig;
|
|
|
|
constructor() {
|
|
this.config = getLLMConfig();
|
|
this.client = new OpenAI({
|
|
apiKey: this.config.apiKey,
|
|
baseURL: this.config.baseUrl,
|
|
});
|
|
}
|
|
|
|
async chat(
|
|
messages: { role: 'system' | 'user' | 'assistant'; content: string }[],
|
|
tools?: any[],
|
|
): Promise<{ content: string; toolCalls?: any[]; usage: any }> {
|
|
const response = await this.client.chat.completions.create({
|
|
model: this.config.model,
|
|
messages,
|
|
tools,
|
|
max_tokens: this.config.maxTokens,
|
|
temperature: this.config.temperature,
|
|
});
|
|
|
|
const choice = response.choices[0];
|
|
|
|
return {
|
|
content: choice.message.content || '',
|
|
toolCalls: choice.message.tool_calls,
|
|
usage: response.usage,
|
|
};
|
|
}
|
|
|
|
async chatWithTools(
|
|
userMessage: string,
|
|
systemPrompt: string,
|
|
tools: Tool[],
|
|
context: any,
|
|
): Promise<string> {
|
|
const messages = [
|
|
{ role: 'system' as const, content: systemPrompt },
|
|
{ role: 'user' as const, content: userMessage },
|
|
];
|
|
|
|
const toolDefinitions = tools.map((t) => ({
|
|
type: 'function' as const,
|
|
function: {
|
|
name: t.name,
|
|
description: t.description,
|
|
parameters: t.parameters,
|
|
},
|
|
}));
|
|
|
|
let response = await this.chat(messages, toolDefinitions);
|
|
|
|
// Procesar tool calls
|
|
while (response.toolCalls && response.toolCalls.length > 0) {
|
|
for (const toolCall of response.toolCalls) {
|
|
const tool = tools.find((t) => t.name === toolCall.function.name);
|
|
if (tool) {
|
|
const args = JSON.parse(toolCall.function.arguments);
|
|
const result = await tool.execute(args, context);
|
|
|
|
messages.push({
|
|
role: 'assistant',
|
|
content: null,
|
|
tool_calls: [toolCall],
|
|
} as any);
|
|
|
|
messages.push({
|
|
role: 'tool' as any,
|
|
content: JSON.stringify(result),
|
|
tool_call_id: toolCall.id,
|
|
} as any);
|
|
}
|
|
}
|
|
|
|
response = await this.chat(messages, toolDefinitions);
|
|
}
|
|
|
|
return response.content;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.5 System Prompts
|
|
|
|
```typescript
|
|
// prompts/owner.prompt.ts
|
|
export const OWNER_SYSTEM_PROMPT = `
|
|
Eres el asistente virtual de MiChangarrito, ayudando al dueno de un negocio.
|
|
|
|
CAPACIDADES:
|
|
- Consultar ventas del dia, semana o mes
|
|
- Consultar ganancias y utilidades
|
|
- Ver inventario y stock
|
|
- Alertar sobre productos por agotarse
|
|
- Registrar ventas por chat
|
|
- Agregar o modificar productos
|
|
- Generar reportes
|
|
- Recordar fiados pendientes
|
|
|
|
PERSONALIDAD:
|
|
- Amigable y servicial
|
|
- Respuestas cortas y directas
|
|
- Usa lenguaje coloquial mexicano
|
|
- Ofrece sugerencias proactivas
|
|
|
|
CONTEXTO DEL NEGOCIO:
|
|
- Nombre: {{business_name}}
|
|
- Giro: {{business_type}}
|
|
- Plan: {{plan_name}}
|
|
|
|
Responde siempre en espanol mexicano.
|
|
`;
|
|
|
|
// prompts/customer.prompt.ts
|
|
export const CUSTOMER_SYSTEM_PROMPT = `
|
|
Eres el asistente de {{business_name}}, atendiendo a un cliente.
|
|
|
|
CAPACIDADES:
|
|
- Informar sobre productos disponibles
|
|
- Dar precios
|
|
- Tomar pedidos
|
|
- Informar horarios
|
|
- Informar sobre estado de pedidos
|
|
|
|
RESTRICCIONES:
|
|
- NO puedes dar descuentos sin autorizacion
|
|
- NO tienes acceso a informacion financiera
|
|
- Redirige consultas complejas al dueno
|
|
|
|
Responde amablemente y de forma concisa.
|
|
`;
|
|
```
|
|
|
|
### 3.6 Costos por Modelo
|
|
|
|
| Modelo | Input/1M tokens | Output/1M tokens |
|
|
|--------|-----------------|------------------|
|
|
| GPT-4o-mini | $0.15 | $0.60 |
|
|
| Claude 3 Haiku | $0.25 | $1.25 |
|
|
| Claude 3.5 Sonnet | $3.00 | $15.00 |
|
|
| Llama 3 8B (OpenRouter) | $0.07 | $0.07 |
|
|
|
|
---
|
|
|
|
## 4. Mercado Pago Point
|
|
|
|
### 4.1 Proposito
|
|
- Cobros con tarjeta via terminal fisica
|
|
- Meses sin intereses
|
|
|
|
### 4.2 Documentacion
|
|
- https://www.mercadopago.com.mx/developers/es/docs/mp-point/overview
|
|
|
|
### 4.3 SDK
|
|
|
|
```bash
|
|
npm install mercadopago
|
|
```
|
|
|
|
### 4.4 Integracion
|
|
|
|
```typescript
|
|
// services/mercadopago.service.ts
|
|
import { MercadoPagoConfig, Payment, PaymentMethod } from 'mercadopago';
|
|
|
|
export class MercadoPagoService {
|
|
private client: MercadoPagoConfig;
|
|
|
|
constructor() {
|
|
this.client = new MercadoPagoConfig({
|
|
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN,
|
|
});
|
|
}
|
|
|
|
// Crear payment intent para terminal
|
|
async createPointPayment(
|
|
amount: number,
|
|
description: string,
|
|
externalReference: string,
|
|
): Promise<any> {
|
|
const response = await fetch(
|
|
`https://api.mercadopago.com/point/integration-api/devices/${process.env.MERCADOPAGO_DEVICE_ID}/payment-intents`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.MERCADOPAGO_ACCESS_TOKEN}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
amount,
|
|
description,
|
|
external_reference: externalReference,
|
|
print_on_terminal: true,
|
|
}),
|
|
},
|
|
);
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Consultar estado de pago
|
|
async getPaymentIntent(paymentIntentId: string): Promise<any> {
|
|
const response = await fetch(
|
|
`https://api.mercadopago.com/point/integration-api/payment-intents/${paymentIntentId}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.MERCADOPAGO_ACCESS_TOKEN}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Cancelar payment intent
|
|
async cancelPaymentIntent(paymentIntentId: string): Promise<void> {
|
|
await fetch(
|
|
`https://api.mercadopago.com/point/integration-api/payment-intents/${paymentIntentId}`,
|
|
{
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.MERCADOPAGO_ACCESS_TOKEN}`,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.5 Webhooks
|
|
|
|
```typescript
|
|
@Post('webhook/mercadopago')
|
|
async handleMercadoPagoWebhook(@Body() body: any) {
|
|
const { type, data } = body;
|
|
|
|
if (type === 'point_integration_wh') {
|
|
const paymentIntent = await this.mpService.getPaymentIntent(data.id);
|
|
|
|
if (paymentIntent.state === 'FINISHED') {
|
|
await this.salesService.confirmPayment(
|
|
paymentIntent.external_reference,
|
|
'card_mercadopago',
|
|
paymentIntent.id,
|
|
);
|
|
}
|
|
}
|
|
|
|
return { received: true };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Firebase Cloud Messaging
|
|
|
|
### 5.1 Proposito
|
|
- Push notifications a app movil
|
|
- Alertas en tiempo real
|
|
|
|
### 5.2 Documentacion
|
|
- https://firebase.google.com/docs/cloud-messaging
|
|
|
|
### 5.3 SDK
|
|
|
|
```bash
|
|
npm install firebase-admin
|
|
```
|
|
|
|
### 5.4 Configuracion
|
|
|
|
```typescript
|
|
// config/firebase.config.ts
|
|
import * as admin from 'firebase-admin';
|
|
|
|
const serviceAccount = JSON.parse(
|
|
process.env.FIREBASE_SERVICE_ACCOUNT_JSON ||
|
|
fs.readFileSync(process.env.FIREBASE_SERVICE_ACCOUNT_PATH, 'utf8'),
|
|
);
|
|
|
|
admin.initializeApp({
|
|
credential: admin.credential.cert(serviceAccount),
|
|
});
|
|
|
|
export const messaging = admin.messaging();
|
|
```
|
|
|
|
### 5.5 Enviar Notificacion
|
|
|
|
```typescript
|
|
// services/push.service.ts
|
|
export class PushNotificationService {
|
|
async sendToDevice(
|
|
deviceToken: string,
|
|
title: string,
|
|
body: string,
|
|
data?: Record<string, string>,
|
|
): Promise<void> {
|
|
await messaging.send({
|
|
token: deviceToken,
|
|
notification: { title, body },
|
|
data,
|
|
android: {
|
|
priority: 'high',
|
|
notification: {
|
|
sound: 'default',
|
|
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
|
|
},
|
|
},
|
|
apns: {
|
|
payload: {
|
|
aps: {
|
|
sound: 'default',
|
|
badge: 1,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async sendToTopic(
|
|
topic: string,
|
|
title: string,
|
|
body: string,
|
|
data?: Record<string, string>,
|
|
): Promise<void> {
|
|
await messaging.send({
|
|
topic,
|
|
notification: { title, body },
|
|
data,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Clip
|
|
|
|
### 6.1 Proposito
|
|
- Cobros con tarjeta via terminal Clip
|
|
|
|
### 6.2 Documentacion
|
|
- https://developer.clip.mx/
|
|
|
|
### 6.3 Integracion (REST API)
|
|
|
|
```typescript
|
|
// services/clip.service.ts
|
|
export class ClipService {
|
|
private readonly baseUrl = 'https://api-gw.payclip.com';
|
|
private readonly apiKey = process.env.CLIP_API_KEY;
|
|
|
|
async createPaymentRequest(
|
|
amount: number,
|
|
reference: string,
|
|
): Promise<any> {
|
|
const response = await fetch(`${this.baseUrl}/paymentrequest/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'x-api-key': this.apiKey,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
amount,
|
|
currency: 'MXN',
|
|
reference,
|
|
message: `Cobro ${reference}`,
|
|
}),
|
|
});
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async cancelPaymentRequest(code: string): Promise<void> {
|
|
await fetch(`${this.baseUrl}/paymentrequest/code/${code}`, {
|
|
method: 'DELETE',
|
|
headers: { 'x-api-key': this.apiKey },
|
|
});
|
|
}
|
|
|
|
async getTransaction(receiptNo: string): Promise<any> {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/payments/receipt-no/${receiptNo}`,
|
|
{
|
|
headers: { 'x-api-key': this.apiKey },
|
|
},
|
|
);
|
|
|
|
return response.json();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. CoDi (via Openpay)
|
|
|
|
### 7.1 Proposito
|
|
- Cobros con QR sin comisiones
|
|
|
|
### 7.2 Documentacion
|
|
- https://site.openpay.mx/docs/codi.html
|
|
|
|
### 7.3 SDK
|
|
|
|
```bash
|
|
npm install openpay
|
|
```
|
|
|
|
### 7.4 Integracion
|
|
|
|
```typescript
|
|
// services/codi.service.ts
|
|
const Openpay = require('openpay');
|
|
|
|
export class CodiService {
|
|
private openpay: any;
|
|
|
|
constructor() {
|
|
this.openpay = new Openpay(
|
|
process.env.OPENPAY_MERCHANT_ID,
|
|
process.env.OPENPAY_PRIVATE_KEY,
|
|
false, // production = false para sandbox
|
|
);
|
|
}
|
|
|
|
async createQRCharge(
|
|
amount: number,
|
|
description: string,
|
|
orderId: string,
|
|
): Promise<{ qrUrl: string; chargeId: string }> {
|
|
return new Promise((resolve, reject) => {
|
|
this.openpay.charges.create(
|
|
{
|
|
method: 'codi',
|
|
amount,
|
|
description,
|
|
order_id: orderId,
|
|
codi_options: {
|
|
mode: 'QR_CODE',
|
|
},
|
|
},
|
|
(error: any, charge: any) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve({
|
|
qrUrl: charge.payment_method.barcode_url,
|
|
chargeId: charge.id,
|
|
});
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
async getChargeStatus(chargeId: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
this.openpay.charges.get(chargeId, (error: any, charge: any) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(charge.status);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Google Cloud Vision (OCR)
|
|
|
|
### 8.1 Proposito
|
|
- Leer listas de precios desde fotos
|
|
- Procesar notas de compra
|
|
- Extraer texto de imagenes
|
|
|
|
### 8.2 Documentacion
|
|
- https://cloud.google.com/vision/docs
|
|
|
|
### 8.3 Integracion
|
|
|
|
```typescript
|
|
// services/ocr.service.ts
|
|
import vision from '@google-cloud/vision';
|
|
|
|
export class OCRService {
|
|
private client: vision.ImageAnnotatorClient;
|
|
|
|
constructor() {
|
|
this.client = new vision.ImageAnnotatorClient({
|
|
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
});
|
|
}
|
|
|
|
async extractText(imageUrl: string): Promise<string> {
|
|
const [result] = await this.client.textDetection(imageUrl);
|
|
const detections = result.textAnnotations;
|
|
|
|
if (detections && detections.length > 0) {
|
|
return detections[0].description || '';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
async extractProducts(
|
|
imageUrl: string,
|
|
): Promise<{ name: string; price: number }[]> {
|
|
const text = await this.extractText(imageUrl);
|
|
|
|
// Usar LLM para estructurar el texto
|
|
const structured = await this.llmService.chat([
|
|
{
|
|
role: 'system',
|
|
content:
|
|
'Extrae productos y precios del siguiente texto. Responde en JSON: [{"name": "...", "price": 0.00}]',
|
|
},
|
|
{ role: 'user', content: text },
|
|
]);
|
|
|
|
return JSON.parse(structured.content);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. OpenAI Whisper (Transcripcion)
|
|
|
|
### 9.1 Proposito
|
|
- Transcribir audios de WhatsApp
|
|
- Comandos de voz
|
|
|
|
### 9.2 Documentacion
|
|
- https://platform.openai.com/docs/guides/speech-to-text
|
|
|
|
### 9.3 Integracion
|
|
|
|
```typescript
|
|
// services/transcription.service.ts
|
|
import OpenAI from 'openai';
|
|
import fs from 'fs';
|
|
|
|
export class TranscriptionService {
|
|
private openai: OpenAI;
|
|
|
|
constructor() {
|
|
this.openai = new OpenAI({
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
});
|
|
}
|
|
|
|
async transcribeAudio(audioPath: string): Promise<string> {
|
|
const transcription = await this.openai.audio.transcriptions.create({
|
|
file: fs.createReadStream(audioPath),
|
|
model: 'whisper-1',
|
|
language: 'es',
|
|
});
|
|
|
|
return transcription.text;
|
|
}
|
|
|
|
async transcribeFromUrl(audioUrl: string): Promise<string> {
|
|
// Descargar audio
|
|
const response = await fetch(audioUrl);
|
|
const buffer = await response.arrayBuffer();
|
|
const tempPath = `/tmp/audio_${Date.now()}.ogg`;
|
|
fs.writeFileSync(tempPath, Buffer.from(buffer));
|
|
|
|
try {
|
|
const text = await this.transcribeAudio(tempPath);
|
|
return text;
|
|
} finally {
|
|
fs.unlinkSync(tempPath);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Resumen de Variables de Entorno
|
|
|
|
```env
|
|
# Stripe
|
|
STRIPE_SECRET_KEY=sk_test_...
|
|
STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
|
|
# WhatsApp
|
|
WHATSAPP_VERIFY_TOKEN=...
|
|
WHATSAPP_ACCESS_TOKEN=...
|
|
WHATSAPP_PHONE_NUMBER_ID=...
|
|
WHATSAPP_BUSINESS_ACCOUNT_ID=...
|
|
|
|
# LLM
|
|
LLM_PROVIDER=openrouter
|
|
LLM_API_KEY=...
|
|
LLM_MODEL=anthropic/claude-3-haiku
|
|
LLM_BASE_URL=https://openrouter.ai/api/v1
|
|
|
|
# Mercado Pago
|
|
MERCADOPAGO_ACCESS_TOKEN=...
|
|
MERCADOPAGO_DEVICE_ID=...
|
|
|
|
# Clip
|
|
CLIP_API_KEY=...
|
|
CLIP_MERCHANT_ID=...
|
|
|
|
# CoDi (Openpay)
|
|
OPENPAY_MERCHANT_ID=...
|
|
OPENPAY_PRIVATE_KEY=...
|
|
|
|
# Firebase
|
|
FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-sa.json
|
|
|
|
# Google Cloud
|
|
GOOGLE_APPLICATION_CREDENTIALS=./gcp-sa.json
|
|
|
|
# OpenAI (Whisper)
|
|
OPENAI_API_KEY=...
|
|
```
|
|
|
|
---
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2026-01-04
|