| id |
type |
title |
status |
created_at |
updated_at |
simco_version |
author |
tags |
| SPEC-INTEGRACIONES-EXTERNAS |
Specification |
MiChangarrito - Integraciones Externas |
Published |
2026-01-04 |
2026-01-10 |
3.8.0 |
Equipo MiChangarrito |
| 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
1.3 SDK
npm install stripe
1.4 Configuracion
// 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
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
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
// 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
2.3 Configuracion Inicial
- Crear app en Meta for Developers
- Agregar producto "WhatsApp"
- Configurar numero de telefono
- Verificar negocio
- Configurar webhooks
2.4 Endpoints Principales
// 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
// 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
// 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
3.3 Configuracion Agnostica
// 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
// 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
// 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
// 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
4.3 Proveedor PAC Recomendado
// 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)
npm install facturapi
4.5 Integracion
// 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
# 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
5.3 Configuracion
// 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)
// 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
// 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
# 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
6.3 SDK
npm install mercadopago
6.4 Integracion
// 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
@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
7.3 SDK
npm install firebase-admin
7.4 Configuracion
// 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
// 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
8.3 Integracion (REST API)
// 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
9.3 SDK
npm install openpay
9.4 Integracion
// 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
10.3 Integracion
// 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
11.3 Integracion
// 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
# 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)