michangarrito/docs/02-especificaciones/INTEGRACIONES-EXTERNAS.md
rckrdmrd 97f407c661 [MIGRATION-V2] feat: Migrar michangarrito a estructura v2
- Prefijo v2: MCH
- TRACEABILITY-MASTER.yml creado
- Listo para integracion como submodulo

Workspace: v2.0.0 | SIMCO: v4.0.0
2026-01-10 11:28:54 -06:00

37 KiB

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

  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

// 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)