michangarrito/backups/docs-backup-2026-01-10/docs/02-especificaciones/INTEGRACIONES-EXTERNAS.md
rckrdmrd 928eb795e6 [SIMCO-V38] feat: Actualizar a SIMCO v3.8.0 + cambios apps
- 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>
2026-01-10 08:53:05 -06:00

25 KiB

MiChangarrito - Integraciones Externas

Indice de Integraciones

Integracion Categoria Prioridad Complejidad
Stripe Pagos/Suscripciones P0 Media
WhatsApp Business API Mensajeria P0 Alta
OpenRouter/LLM Inteligencia Artificial P0 Media
Mercado Pago Point Terminal Pago P1 Media
Firebase FCM Push Notifications P1 Baja
Clip Terminal Pago P2 Media
CoDi (Banxico) Pagos QR P2 Alta
Google Vision OCR P2 Baja
OpenAI Whisper Transcripcion P2 Baja

1. Stripe

1.1 Proposito

  • Suscripciones mensuales (Plan Changarrito, Plan Tiendita)
  • Pagos con tarjeta
  • Referencias de pago OXXO
  • Gestion de clientes y facturacion

1.2 Documentacion

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
GPT-4o-mini $0.15 $0.60
Claude 3 Haiku $0.25 $1.25
Claude 3.5 Sonnet $3.00 $15.00
Llama 3 8B (OpenRouter) $0.07 $0.07

4. Mercado Pago Point

4.1 Proposito

  • Cobros con tarjeta via terminal fisica
  • Meses sin intereses

4.2 Documentacion

4.3 SDK

npm install mercadopago

4.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}`,
        },
      },
    );
  }
}

4.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 };
}

5. Firebase Cloud Messaging

5.1 Proposito

  • Push notifications a app movil
  • Alertas en tiempo real

5.2 Documentacion

5.3 SDK

npm install firebase-admin

5.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();

5.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,
    });
  }
}

6. Clip

6.1 Proposito

  • Cobros con tarjeta via terminal Clip

6.2 Documentacion

6.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();
  }
}

7. CoDi (via Openpay)

7.1 Proposito

  • Cobros con QR sin comisiones

7.2 Documentacion

7.3 SDK

npm install openpay

7.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);
        }
      });
    });
  }
}

8. Google Cloud Vision (OCR)

8.1 Proposito

  • Leer listas de precios desde fotos
  • Procesar notas de compra
  • Extraer texto de imagenes

8.2 Documentacion

8.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);
  }
}

9. OpenAI Whisper (Transcripcion)

9.1 Proposito

  • Transcribir audios de WhatsApp
  • Comandos de voz

9.2 Documentacion

9.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-haiku
LLM_BASE_URL=https://openrouter.ai/api/v1

# Mercado Pago
MERCADOPAGO_ACCESS_TOKEN=...
MERCADOPAGO_DEVICE_ID=...

# Clip
CLIP_API_KEY=...
CLIP_MERCHANT_ID=...

# CoDi (Openpay)
OPENPAY_MERCHANT_ID=...
OPENPAY_PRIVATE_KEY=...

# Firebase
FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-sa.json

# Google Cloud
GOOGLE_APPLICATION_CREDENTIALS=./gcp-sa.json

# OpenAI (Whisper)
OPENAI_API_KEY=...

Version: 1.0.0 Fecha: 2026-01-04