# Auto-facturación de pagos MercadoPago con Facturapi ## Resumen Cuando MercadoPago confirma un pago aprobado (webhook), Horux 360 emite automáticamente un CFDI al **público en general** vía Facturapi, sin intervención manual. Aplica a pagos recurrentes (preapproval) y pagos de prorateo de upgrade (preference). ## Motivación Antes: los pagos se registraban en tabla `payments` pero la emisión de factura era 100% manual. Riesgo de olvido → pagos cobrados sin CFDI, cliente se queja, SAT audita. Ahora: emisión automática como default. El admin solo toca la primera factura de cada cliente nuevo (para capturar/verificar datos fiscales); el resto va solo a público en general. ## Reglas | Condición | ¿Se factura? | |-----------|--------------| | Payment `status === 'approved'` + `amount > 0` + **hay al menos 1 payment aprobado previo** del mismo tenant | **Sí** — auto-emit a público en general | | Payment es el **primer** aprobado de este tenant | **No** — admin emite manual para capturar datos fiscales del cliente | | `amount === 0` (trial) | No | | `status !== 'approved'` (pending, rejected) | No | | `Payment.facturapiInvoiceId` ya existe | No (idempotente — webhook reintentó) | | Tenant emisor (Horux 360) sin `facturapiOrgId` configurado | No — log warning, admin configura en `/configuracion` | | Tenant emisor sin CP en datos fiscales | No — log warning | ## Emisor: Horux 360 (RESICO PM) - **RFC:** `HTS240708LJA` - **Régimen fiscal:** 626 (RESICO PM) - **Retenciones:** ninguna (RESICO PM no retiene IVA ni ISR en sus facturas emitidas) La organización Facturapi del tenant Horux 360 es la que emite — `facturapiService.createInvoice(horux360TenantId, payload)` enruta al API key correcto. ## Receptor: Público en general | Campo | Valor | |-------|-------| | `taxId` | `XAXX010101000` (RFC genérico) | | `legalName` | `PUBLICO EN GENERAL` | | `taxSystem` | `616` (Sin obligaciones fiscales) | | `zip` | CP del emisor (patrón SAT) | **CFDI `use`:** `S01` — Sin efectos fiscales. **Nota:** esto NO es "factura global consolidada" (que requiere `periodicidad`/`mes`/`año` y acumula varios tickets). Es una factura simple de tipo I a público en general por cada pago. ## Concepto | Campo | Valor | |-------|-------| | `description` | `Suscripción ${plan} ${mensual\|anual} a Horux 360` | | `productKey` | `81112502` — Servicios de alojamiento de aplicaciones | | `unitKey` | `E48` — Unidad de servicio | | `unitName` | `Servicio` | | `quantity` | `1` | | `price` | `Payment.amount` (ya incluye IVA) | | `taxIncluded` | `true` — Facturapi desagrega subtotal + IVA 16% | | `taxes` | `[{ type: 'IVA', rate: 0.16, factor: 'Tasa' }]` — sin retenciones | ## Forma de pago Mapeo del `paymentMethodId` que manda MercadoPago al código SAT: | MP paymentMethodId | SAT forma pago | |--------------------|-----------------| | `credit_card` | `04` — Tarjeta de crédito | | `debit_card` | `28` — Tarjeta de débito | | `account_money` | `03` — Transferencia | | `bank_transfer` | `03` | | *otro / desconocido* | `03` (default conservador) | **`paymentMethod`:** `PUE` (pago en una sola exhibición — MP ya cobró cuando el webhook dispara). ## Flujo ``` 1. Usuario paga en MercadoPago ↓ 2. MP POST /webhooks/mercadopago { type: 'payment', data: { id } } ↓ 3. webhook.controller detecta tipo ↓ ↓ proration:* tenantId (UUID) → recordPayment → recordPayment → applyApprovedUpgrade → subscription.status = 'authorized' → emitInvoiceIfApplicable(payment.id) → emitInvoiceIfApplicable(payment.id) ↓ 4. invoicing.service.emitInvoiceIfApplicable: - Gate: ya facturado? approved? amount>0? primer pago? - Gate: emisor Horux 360 bien configurado? - Build payload (público general + concepto) - facturapiService.createInvoice(horux360TenantId, payload) - UPDATE Payment.facturapiInvoiceId ``` ## Fail-soft: el webhook nunca muere por facturación Si Facturapi está caído o devuelve error: - `emitInvoiceIfApplicable` catchea todo, logea `[Invoicing] Error emitiendo factura...` - `Payment.facturapiInvoiceId` queda `null` - Webhook retorna 200 a MP → no hay reintento → no se duplica Payment El admin ve `Payment.facturapiInvoiceId IS NULL AND status = 'approved'` y puede re-emitir manualmente. Mejora futura: cron de reintento. ## Schema Campo nuevo en `Payment` (BD central): ```prisma facturapiInvoiceId String? @map("facturapi_invoice_id") ``` Aplicar con: `pnpm prisma db push` (idempotente) o `prisma migrate deploy` en prod. ## Archivos ### Nuevos - `apps/api/src/services/payment/invoicing.service.ts` — lógica completa del auto-emit ### Modificados - `apps/api/prisma/schema.prisma` — `Payment.facturapiInvoiceId` - `apps/api/src/controllers/webhook.controller.ts` — llama `emitInvoiceIfApplicable(paymentRecord.id)` en ambos caminos (proration + recurring) después de `recordPayment` si `status === 'approved'` ## Constantes ajustables Viven como `const` al inicio de `invoicing.service.ts`: | Constante | Valor actual | Cuándo cambiar | |-----------|--------------|----------------| | `CONCEPT_PRODUCT_KEY` | `81112502` | Si cambias clasificación SAT del servicio | | `CUSTOMER_TAX_ID` | `XAXX010101000` | Nunca (RFC genérico público general) | | `USE_CFDI` | `S01` | Nunca para público en general | | `IVA_RATE` | `0.16` | Solo si cambia la ley fiscal | | `FORMA_PAGO_POR_METHOD` | mapeo | Si MP agrega método no contemplado | Para cambiar: 1. Edita la constante en el archivo 2. `pnpm typecheck` (verifica que nada más se rompa) 3. Restart server ## Pruebas (requiere `MP_ACCESS_TOKEN` + Facturapi configurado) ### Caso 1: Primer pago (manual) 1. Usuario nuevo subscribe plan Business mensual → crea preapproval 2. MP cobra primer pago → webhook → `recordPayment` → `emitInvoiceIfApplicable` 3. En log: `[Invoicing] Payment X es el PRIMER pago aprobado del tenant Y, skip (factura manual)` 4. `Payment.facturapiInvoiceId = null` 5. Admin emite manual desde Facturapi dashboard ### Caso 2: Segundo pago en adelante (auto) 1. MP cobra recurrente del mismo tenant → webhook 2. En log: `[Invoicing] Emitiendo factura para Payment X (tenant Y, $480)` 3. `Payment.facturapiInvoiceId = fac_xxxxx` 4. Ver factura en Facturapi dashboard: PUBLICO EN GENERAL, concepto "Suscripción Business mensual a Horux 360" ### Caso 3: Upgrade con proration 1. Usuario cambia de Business → Business+IA mid-period → MP Preference por diff prorateado 2. Usuario paga → webhook con `external_reference: proration:...` 3. Si NO es primer pago aprobado del tenant: auto-emit con descripción del nuevo plan 4. Si ES primer pago (nuevo tenant que upgradea inmediatamente): skip manual ### Caso 4: Facturapi caído 1. Webhook fires 2. `createInvoice` lanza error 3. Log: `[Invoicing] Error emitiendo factura para Payment X: ...` 4. `Payment.facturapiInvoiceId = null` — admin puede re-emitir después 5. Webhook retorna 200 → MP no reintenta → no hay duplicación ## Pendientes / mejoras posibles 1. **Cron de reintento** para payments con `facturapiInvoiceId IS NULL AND status='approved'`. Hoy requiere acción manual del admin. 2. **Email confirmación al cliente** con el PDF/XML — Facturapi tiene `sendByEmail` que podría llamarse después de la emisión exitosa. Requiere capturar el email del admin del tenant. 3. **UI para admin de pagos sin factura** — vista que liste `Payment.status='approved' AND facturapiInvoiceId IS NULL` con botón "Emitir manualmente". 4. **Cancelación de factura** si el pago se reembolsa — actualmente si MP reembolsa un pago, la factura queda emitida. Necesitaría llamar `cancelInvoice` de Facturapi. 5. **Reporte fiscal mensual** — query que consolide ingresos emitidos en Facturapi del tenant Horux 360 (para declaración ISR).