7.8 KiB
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:
emitInvoiceIfApplicablecatchea todo, logea[Invoicing] Error emitiendo factura...Payment.facturapiInvoiceIdquedanull- 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):
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.facturapiInvoiceIdapps/api/src/controllers/webhook.controller.ts— llamaemitInvoiceIfApplicable(paymentRecord.id)en ambos caminos (proration + recurring) después derecordPaymentsistatus === '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:
- Edita la constante en el archivo
pnpm typecheck(verifica que nada más se rompa)- Restart server
Pruebas (requiere MP_ACCESS_TOKEN + Facturapi configurado)
Caso 1: Primer pago (manual)
- Usuario nuevo subscribe plan Business mensual → crea preapproval
- MP cobra primer pago → webhook →
recordPayment→emitInvoiceIfApplicable - En log:
[Invoicing] Payment X es el PRIMER pago aprobado del tenant Y, skip (factura manual) Payment.facturapiInvoiceId = null- Admin emite manual desde Facturapi dashboard
Caso 2: Segundo pago en adelante (auto)
- MP cobra recurrente del mismo tenant → webhook
- En log:
[Invoicing] Emitiendo factura para Payment X (tenant Y, $480) Payment.facturapiInvoiceId = fac_xxxxx- Ver factura en Facturapi dashboard: PUBLICO EN GENERAL, concepto "Suscripción Business mensual a Horux 360"
Caso 3: Upgrade con proration
- Usuario cambia de Business → Business+IA mid-period → MP Preference por diff prorateado
- Usuario paga → webhook con
external_reference: proration:... - Si NO es primer pago aprobado del tenant: auto-emit con descripción del nuevo plan
- Si ES primer pago (nuevo tenant que upgradea inmediatamente): skip manual
Caso 4: Facturapi caído
- Webhook fires
createInvoicelanza error- Log:
[Invoicing] Error emitiendo factura para Payment X: ... Payment.facturapiInvoiceId = null— admin puede re-emitir después- Webhook retorna 200 → MP no reintenta → no hay duplicación
Pendientes / mejoras posibles
- Cron de reintento para payments con
facturapiInvoiceId IS NULL AND status='approved'. Hoy requiere acción manual del admin. - Email confirmación al cliente con el PDF/XML — Facturapi tiene
sendByEmailque podría llamarse después de la emisión exitosa. Requiere capturar el email del admin del tenant. - UI para admin de pagos sin factura — vista que liste
Payment.status='approved' AND facturapiInvoiceId IS NULLcon botón "Emitir manualmente". - Cancelación de factura si el pago se reembolsa — actualmente si MP reembolsa un pago, la factura queda emitida. Necesitaría llamar
cancelInvoicede Facturapi. - Reporte fiscal mensual — query que consolide ingresos emitidos en Facturapi del tenant Horux 360 (para declaración ISR).