Update: nueva version Horux Despachos
This commit is contained in:
174
docs/plans/2026-04-13-auto-invoicing-mp-payments.md
Normal file
174
docs/plans/2026-04-13-auto-invoicing-mp-payments.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user