- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Cambios en backend y frontend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1540 lines
37 KiB
Markdown
1540 lines
37 KiB
Markdown
---
|
|
id: SPEC-INTEGRACIONES-EXTERNAS
|
|
type: Specification
|
|
title: "MiChangarrito - Integraciones Externas"
|
|
status: Published
|
|
created_at: 2026-01-04
|
|
updated_at: 2026-01-10
|
|
simco_version: "3.8.0"
|
|
author: "Equipo MiChangarrito"
|
|
tags:
|
|
- integraciones
|
|
- stripe
|
|
- whatsapp
|
|
- llm
|
|
- pagos
|
|
- facturacion
|
|
- spei
|
|
---
|
|
|
|
# MiChangarrito - Integraciones Externas
|
|
|
|
## Indice de Integraciones
|
|
|
|
| Integracion | Categoria | Prioridad | Complejidad | Estado | Notas |
|
|
|-------------|-----------|-----------|-------------|--------|-------|
|
|
| Stripe | Pagos/Suscripciones | P0 | Media | Implementado (100%) | Listo para produccion |
|
|
| WhatsApp Business API | Mensajeria | P0 | Alta | Implementado (95%) | Requiere cuenta Meta Business |
|
|
| OpenRouter/LLM | Inteligencia Artificial | P0 | Media | Implementado (90%) | Multi-tenant con fallback |
|
|
| SAT CFDI 4.0 | Facturacion Electronica | P0 | Alta | Modelo Base (5%) | Requiere PAC (Facturapi) |
|
|
| SPEI/STP | Transferencias Bancarias | P1 | Alta | Mock (40%) | Requiere integracion STP.mx |
|
|
| Mercado Pago Point | Terminal Pago | P1 | Media | Solo Docs (0%) | Pendiente implementacion |
|
|
| Firebase FCM | Push Notifications | P1 | Baja | Solo Docs (0%) | Pendiente implementacion |
|
|
| Clip | Terminal Pago | P2 | Media | Solo Docs (0%) | Pendiente implementacion |
|
|
| CoDi (Banxico) | Pagos QR | P1 | Alta | Mock (40%) | Requiere Banxico/PAC |
|
|
| Google Vision | OCR | P2 | Baja | Solo Docs (0%) | Opcional |
|
|
| OpenAI Whisper | Transcripcion | P2 | Baja | Solo Docs (0%) | Opcional |
|
|
|
|
---
|
|
|
|
## 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 | Uso Recomendado |
|
|
|--------|-----------------|------------------|-----------------|
|
|
| GPT-4o-mini | $0.15 | $0.60 | Atencion a clientes |
|
|
| GPT-4o | $2.50 | $10.00 | Consultas complejas |
|
|
| Claude 3.5 Haiku | $0.80 | $4.00 | Atencion a clientes |
|
|
| Claude 3.5 Sonnet | $3.00 | $15.00 | Analisis de datos |
|
|
| Claude Opus 4.5 | $15.00 | $75.00 | Tareas criticas |
|
|
| Llama 3.1 70B (OpenRouter) | $0.52 | $0.75 | Costo-efectivo |
|
|
| DeepSeek V3 (OpenRouter) | $0.27 | $1.10 | Alternativa economica |
|
|
|
|
### 3.7 Configuracion de Modelos por Caso de Uso
|
|
|
|
```typescript
|
|
// config/llm-models.config.ts
|
|
export const LLM_MODEL_CONFIG = {
|
|
// Atencion rapida a clientes (bajo costo)
|
|
customer_service: {
|
|
model: 'gpt-4o-mini',
|
|
maxTokens: 1024,
|
|
temperature: 0.7,
|
|
},
|
|
|
|
// Consultas de duenos (balance costo/calidad)
|
|
owner_queries: {
|
|
model: 'anthropic/claude-3-5-haiku-latest',
|
|
maxTokens: 2048,
|
|
temperature: 0.5,
|
|
},
|
|
|
|
// Analisis de datos y reportes
|
|
data_analysis: {
|
|
model: 'anthropic/claude-3-5-sonnet-latest',
|
|
maxTokens: 4096,
|
|
temperature: 0.3,
|
|
},
|
|
|
|
// Procesamiento de documentos/facturas
|
|
document_processing: {
|
|
model: 'gpt-4o',
|
|
maxTokens: 4096,
|
|
temperature: 0.2,
|
|
},
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 4. SAT CFDI 4.0 (Facturacion Electronica)
|
|
|
|
### 4.1 Proposito
|
|
- Emision de facturas electronicas (CFDI 4.0)
|
|
- Timbrado fiscal digital
|
|
- Cancelacion de facturas
|
|
- Complementos de pago
|
|
- Cumplimiento con regulaciones SAT Mexico
|
|
|
|
### 4.2 Documentacion
|
|
- SAT: https://www.sat.gob.mx/consultas/35025/formato-de-factura-electronica-(anexo-20)
|
|
- Portal de certificacion: https://www.sat.gob.mx/aplicacion/operacion/92438/servicio-de-facturacion-cfdi
|
|
- Catalogo SAT: https://www.sat.gob.mx/consultas/35025/catalogo-de-datos
|
|
|
|
### 4.3 Proveedor PAC Recomendado
|
|
|
|
```typescript
|
|
// config/cfdi.config.ts
|
|
export type PACProvider = 'facturapi' | 'sw_sapien' | 'finkok' | 'diverza';
|
|
|
|
export const CFDI_CONFIG = {
|
|
provider: process.env.PAC_PROVIDER as PACProvider,
|
|
apiKey: process.env.PAC_API_KEY,
|
|
environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
|
|
|
|
// Datos fiscales del emisor (por tenant)
|
|
defaults: {
|
|
regimen_fiscal: '612', // Personas Fisicas con Actividad Empresarial
|
|
uso_cfdi_default: 'G03', // Gastos en general
|
|
forma_pago_default: '01', // Efectivo
|
|
metodo_pago_default: 'PUE', // Pago en una sola exhibicion
|
|
},
|
|
};
|
|
```
|
|
|
|
### 4.4 SDK (Facturapi)
|
|
|
|
```bash
|
|
npm install facturapi
|
|
```
|
|
|
|
### 4.5 Integracion
|
|
|
|
```typescript
|
|
// services/cfdi.service.ts
|
|
import Facturapi from 'facturapi';
|
|
|
|
export class CFDIService {
|
|
private facturapi: Facturapi;
|
|
|
|
constructor() {
|
|
this.facturapi = new Facturapi(process.env.FACTURAPI_KEY);
|
|
}
|
|
|
|
// Crear cliente SAT
|
|
async createCustomer(data: {
|
|
legal_name: string;
|
|
tax_id: string; // RFC
|
|
email: string;
|
|
tax_system: string; // Regimen fiscal
|
|
address: {
|
|
zip: string;
|
|
country?: string;
|
|
};
|
|
}): Promise<any> {
|
|
return this.facturapi.customers.create({
|
|
legal_name: data.legal_name,
|
|
tax_id: data.tax_id,
|
|
email: data.email,
|
|
tax_system: data.tax_system,
|
|
address: {
|
|
zip: data.zip,
|
|
country: data.country || 'MEX',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Crear factura CFDI 4.0
|
|
async createInvoice(data: {
|
|
customer_id: string;
|
|
items: Array<{
|
|
product_key: string; // Clave SAT del producto
|
|
description: string;
|
|
quantity: number;
|
|
price: number;
|
|
tax_included?: boolean;
|
|
}>;
|
|
payment_form: string; // 01=Efectivo, 03=Transferencia, etc.
|
|
payment_method?: string; // PUE o PPD
|
|
use?: string; // Uso CFDI (G03, etc.)
|
|
folio_number?: number;
|
|
}): Promise<any> {
|
|
return this.facturapi.invoices.create({
|
|
customer: data.customer_id,
|
|
items: data.items.map(item => ({
|
|
product: {
|
|
product_key: item.product_key,
|
|
description: item.description,
|
|
price: item.price,
|
|
tax_included: item.tax_included ?? true,
|
|
taxes: [
|
|
{
|
|
type: 'IVA',
|
|
rate: 0.16,
|
|
},
|
|
],
|
|
},
|
|
quantity: item.quantity,
|
|
})),
|
|
payment_form: data.payment_form,
|
|
payment_method: data.payment_method || 'PUE',
|
|
use: data.use || 'G03',
|
|
folio_number: data.folio_number,
|
|
});
|
|
}
|
|
|
|
// Cancelar factura
|
|
async cancelInvoice(invoiceId: string, motive: string): Promise<any> {
|
|
return this.facturapi.invoices.cancel(invoiceId, {
|
|
motive,
|
|
});
|
|
}
|
|
|
|
// Descargar PDF
|
|
async downloadPDF(invoiceId: string): Promise<Buffer> {
|
|
return this.facturapi.invoices.downloadPdf(invoiceId);
|
|
}
|
|
|
|
// Descargar XML
|
|
async downloadXML(invoiceId: string): Promise<Buffer> {
|
|
return this.facturapi.invoices.downloadXml(invoiceId);
|
|
}
|
|
|
|
// Enviar por email
|
|
async sendByEmail(invoiceId: string, email: string): Promise<void> {
|
|
await this.facturapi.invoices.sendByEmail(invoiceId, { email });
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.6 Catalogos SAT Principales
|
|
|
|
| Catalogo | Descripcion | Ejemplo |
|
|
|----------|-------------|---------|
|
|
| c_ClaveProdServ | Productos/Servicios | 01010101 (No existe en el catalogo) |
|
|
| c_ClaveUnidad | Unidades de medida | H87 (Pieza), KGM (Kilogramo) |
|
|
| c_FormaPago | Formas de pago | 01 (Efectivo), 03 (Transferencia) |
|
|
| c_MetodoPago | Metodo de pago | PUE (Una exhibicion), PPD (Parcialidades) |
|
|
| c_UsoCFDI | Uso del CFDI | G03 (Gastos en general) |
|
|
| c_RegimenFiscal | Regimen fiscal | 612 (Personas Fisicas Act. Empresarial) |
|
|
|
|
### 4.7 Variables de Entorno
|
|
|
|
```env
|
|
# PAC Provider
|
|
PAC_PROVIDER=facturapi
|
|
FACTURAPI_KEY=sk_test_...
|
|
|
|
# O para otros proveedores
|
|
SW_USER=...
|
|
SW_PASSWORD=...
|
|
FINKOK_USER=...
|
|
FINKOK_PASSWORD=...
|
|
```
|
|
|
|
---
|
|
|
|
## 5. SPEI / STP (Transferencias Bancarias)
|
|
|
|
### 5.1 Proposito
|
|
- Recepcion de pagos por transferencia SPEI
|
|
- Generacion de CLABEs virtuales por tenant
|
|
- Conciliacion automatica de pagos
|
|
- Notificaciones en tiempo real
|
|
|
|
### 5.2 Proveedor Recomendado
|
|
- STP (Sistema de Transferencias y Pagos): https://www.stp.mx/
|
|
- Arcus: https://www.arcus.fi/
|
|
- Conekta SPEI: https://www.conekta.com/
|
|
|
|
### 5.3 Configuracion
|
|
|
|
```typescript
|
|
// config/spei.config.ts
|
|
export const SPEI_CONFIG = {
|
|
provider: process.env.SPEI_PROVIDER || 'stp',
|
|
stpConfig: {
|
|
empresa: process.env.STP_EMPRESA,
|
|
clave_privada: process.env.STP_PRIVATE_KEY,
|
|
passphrase: process.env.STP_PASSPHRASE,
|
|
url: process.env.NODE_ENV === 'production'
|
|
? 'https://prod.stpmex.com'
|
|
: 'https://demo.stpmex.com',
|
|
},
|
|
webhookSecret: process.env.SPEI_WEBHOOK_SECRET,
|
|
};
|
|
```
|
|
|
|
### 5.4 Integracion (STP)
|
|
|
|
```typescript
|
|
// services/spei.service.ts
|
|
import crypto from 'crypto';
|
|
|
|
export class SPEIService {
|
|
private readonly config = SPEI_CONFIG.stpConfig;
|
|
|
|
// Generar CLABE virtual para un tenant
|
|
async generateVirtualAccount(
|
|
tenantId: string,
|
|
tenantName: string,
|
|
): Promise<{ clabe: string; reference: string }> {
|
|
const reference = this.generateReference(tenantId);
|
|
|
|
const payload = {
|
|
empresa: this.config.empresa,
|
|
cuenta: reference,
|
|
nombre: tenantName.substring(0, 40),
|
|
rfcCurp: 'XAXX010101000', // RFC generico para cuentas virtuales
|
|
};
|
|
|
|
const signature = this.signPayload(payload);
|
|
|
|
const response = await fetch(`${this.config.url}/api/v1/cuentas`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Firma': signature,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
return {
|
|
clabe: data.clabe,
|
|
reference: reference,
|
|
};
|
|
}
|
|
|
|
// Consultar estado de transferencia
|
|
async getTransferStatus(claveRastreo: string): Promise<any> {
|
|
const response = await fetch(
|
|
`${this.config.url}/api/v1/ordenes/${claveRastreo}`,
|
|
{
|
|
headers: {
|
|
'Firma': this.signPayload({ claveRastreo }),
|
|
},
|
|
},
|
|
);
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// Procesar webhook de pago recibido
|
|
async processIncomingPayment(payload: {
|
|
clabe: string;
|
|
monto: number;
|
|
claveRastreo: string;
|
|
nombreOrdenante: string;
|
|
cuentaOrdenante: string;
|
|
rfcOrdenante: string;
|
|
concepto: string;
|
|
}): Promise<void> {
|
|
// Buscar tenant por CLABE
|
|
const virtualAccount = await this.findVirtualAccount(payload.clabe);
|
|
|
|
if (!virtualAccount) {
|
|
throw new Error(`CLABE no encontrada: ${payload.clabe}`);
|
|
}
|
|
|
|
// Registrar transaccion
|
|
await this.recordTransaction({
|
|
tenantId: virtualAccount.tenant_id,
|
|
amount: payload.monto,
|
|
trackingKey: payload.claveRastreo,
|
|
senderName: payload.nombreOrdenante,
|
|
senderAccount: payload.cuentaOrdenante,
|
|
senderRfc: payload.rfcOrdenante,
|
|
concept: payload.concepto,
|
|
status: 'completed',
|
|
});
|
|
|
|
// Notificar al tenant
|
|
await this.notifyTenant(virtualAccount.tenant_id, payload);
|
|
}
|
|
|
|
private generateReference(tenantId: string): string {
|
|
const hash = crypto
|
|
.createHash('sha256')
|
|
.update(tenantId)
|
|
.digest('hex')
|
|
.substring(0, 10);
|
|
return `MCH${hash.toUpperCase()}`;
|
|
}
|
|
|
|
private signPayload(payload: any): string {
|
|
const sign = crypto.createSign('RSA-SHA256');
|
|
sign.update(JSON.stringify(payload));
|
|
return sign.sign({
|
|
key: this.config.clave_privada,
|
|
passphrase: this.config.passphrase,
|
|
}, 'base64');
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.5 Webhook Handler
|
|
|
|
```typescript
|
|
// webhooks/spei.webhook.ts
|
|
@Controller('webhook/spei')
|
|
export class SPEIWebhookController {
|
|
@Post()
|
|
async handleSPEIWebhook(
|
|
@Body() body: any,
|
|
@Headers('x-stp-signature') signature: string,
|
|
): Promise<{ status: string }> {
|
|
// Verificar firma
|
|
if (!this.verifySignature(body, signature)) {
|
|
throw new ForbiddenException('Invalid signature');
|
|
}
|
|
|
|
const { evento, datos } = body;
|
|
|
|
switch (evento) {
|
|
case 'ORDEN_RECIBIDA':
|
|
await this.speiService.processIncomingPayment(datos);
|
|
break;
|
|
|
|
case 'ORDEN_DEVUELTA':
|
|
await this.handleReturnedPayment(datos);
|
|
break;
|
|
|
|
case 'ORDEN_LIQUIDADA':
|
|
await this.handleSettledPayment(datos);
|
|
break;
|
|
}
|
|
|
|
return { status: 'ok' };
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.6 Flujo de Pago SPEI
|
|
|
|
```
|
|
1. Tenant se registra -> Se genera CLABE virtual
|
|
2. Cliente transfiere a CLABE del tenant
|
|
3. STP recibe transferencia -> Webhook a MiChangarrito
|
|
4. Sistema identifica tenant por CLABE
|
|
5. Registra transaccion en sales.spei_transactions
|
|
6. Notifica al tenant via WhatsApp/Push
|
|
7. Actualiza saldo disponible
|
|
```
|
|
|
|
### 5.7 Variables de Entorno
|
|
|
|
```env
|
|
# SPEI/STP
|
|
SPEI_PROVIDER=stp
|
|
STP_EMPRESA=MiChangarrito
|
|
STP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
|
STP_PASSPHRASE=...
|
|
SPEI_WEBHOOK_SECRET=...
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Mercado Pago Point
|
|
|
|
### 6.1 Proposito
|
|
- Cobros con tarjeta via terminal fisica
|
|
- Meses sin intereses
|
|
|
|
### 6.2 Documentacion
|
|
- https://www.mercadopago.com.mx/developers/es/docs/mp-point/overview
|
|
|
|
### 6.3 SDK
|
|
|
|
```bash
|
|
npm install mercadopago
|
|
```
|
|
|
|
### 6.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}`,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6.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 };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Firebase Cloud Messaging
|
|
|
|
### 7.1 Proposito
|
|
- Push notifications a app movil
|
|
- Alertas en tiempo real
|
|
|
|
### 7.2 Documentacion
|
|
- https://firebase.google.com/docs/cloud-messaging
|
|
|
|
### 7.3 SDK
|
|
|
|
```bash
|
|
npm install firebase-admin
|
|
```
|
|
|
|
### 7.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();
|
|
```
|
|
|
|
### 7.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,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Clip
|
|
|
|
### 8.1 Proposito
|
|
- Cobros con tarjeta via terminal Clip
|
|
|
|
### 8.2 Documentacion
|
|
- https://developer.clip.mx/
|
|
|
|
### 8.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();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. CoDi (via Openpay)
|
|
|
|
### 9.1 Proposito
|
|
- Cobros con QR sin comisiones
|
|
|
|
### 9.2 Documentacion
|
|
- https://site.openpay.mx/docs/codi.html
|
|
|
|
### 9.3 SDK
|
|
|
|
```bash
|
|
npm install openpay
|
|
```
|
|
|
|
### 9.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);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Google Cloud Vision (OCR)
|
|
|
|
### 10.1 Proposito
|
|
- Leer listas de precios desde fotos
|
|
- Procesar notas de compra
|
|
- Extraer texto de imagenes
|
|
|
|
### 10.2 Documentacion
|
|
- https://cloud.google.com/vision/docs
|
|
|
|
### 10.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);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. OpenAI Whisper (Transcripcion)
|
|
|
|
### 11.1 Proposito
|
|
- Transcribir audios de WhatsApp
|
|
- Comandos de voz
|
|
|
|
### 11.2 Documentacion
|
|
- https://platform.openai.com/docs/guides/speech-to-text
|
|
|
|
### 11.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-5-haiku-latest
|
|
LLM_BASE_URL=https://openrouter.ai/api/v1
|
|
LLM_MAX_TOKENS=4096
|
|
LLM_TEMPERATURE=0.7
|
|
|
|
# SAT CFDI 4.0
|
|
PAC_PROVIDER=facturapi
|
|
FACTURAPI_KEY=sk_test_...
|
|
# Alternativas:
|
|
# SW_USER=...
|
|
# SW_PASSWORD=...
|
|
# FINKOK_USER=...
|
|
# FINKOK_PASSWORD=...
|
|
|
|
# SPEI/STP
|
|
SPEI_PROVIDER=stp
|
|
STP_EMPRESA=MiChangarrito
|
|
STP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
|
|
STP_PASSPHRASE=...
|
|
SPEI_WEBHOOK_SECRET=...
|
|
|
|
# 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:** 2.0.0
|
|
**Fecha:** 2026-01-10
|
|
**Actualizado:** Agregado SAT CFDI 4.0, SPEI/STP, modelos LLM actualizados (Claude 4.5, GPT-4o, DeepSeek V3)
|