- Add Stripe and Mercado Pago providers for handling subscriptions. - Create billing repository and access services for managing billing records. - Implement webhook handlers for processing events from Stripe and Mercado Pago. - Add routes for checkout, canceling subscriptions, and retrieving billing status. - Introduce billing status management with appropriate state transitions. - Create tests for billing functionalities including webhook idempotency and provider resolution. - Document the billing module architecture, environment variables, and API endpoints.
284 lines
9.3 KiB
Markdown
284 lines
9.3 KiB
Markdown
# Billing / Subscriptions Module
|
|
|
|
## Architecture
|
|
|
|
```
|
|
modules/billing/
|
|
├── billing.routes.ts # Rutas Hono
|
|
├── features/
|
|
│ ├── create-checkout/ # POST /api/billing/checkout
|
|
│ │ ├── create-checkout.handler.ts
|
|
│ │ └── create-checkout.business.ts
|
|
│ ├── cancel-subscription/ # POST /api/billing/cancel
|
|
│ │ ├── cancel-subscription.handler.ts
|
|
│ │ └── cancel-subscription.business.ts
|
|
│ ├── get-billing-status/ # GET /api/billing/status
|
|
│ │ ├── get-billing-status.handler.ts
|
|
│ │ └── get-billing-status.business.ts
|
|
│ ├── stripe-webhook/ # POST /api/billing/webhooks/stripe
|
|
│ │ ├── stripe-webhook.handler.ts
|
|
│ │ └── stripe-webhook.business.ts
|
|
│ └── mercadopago-webhook/ # POST /api/billing/webhooks/mercadopago
|
|
│ ├── mercadopago-webhook.handler.ts
|
|
│ └── mercadopago-webhook.business.ts
|
|
├── providers/
|
|
│ ├── billing-provider.interface.ts # BillingProvider abstraction
|
|
│ ├── stripe.provider.ts # Stripe implementation
|
|
│ └── mercadopago.provider.ts # Mercado Pago implementation
|
|
└── services/
|
|
├── provider-resolver.service.ts # Resuelve Stripe/MP por país
|
|
├── billing-repository.service.ts # DB access (Prisma)
|
|
└── billing-access.service.ts # canUseApp() helper
|
|
```
|
|
|
|
### Resolución de provider
|
|
|
|
| Country | Provider |
|
|
|---------|----------|
|
|
| `AR` | Mercado Pago |
|
|
| Otro / null | Stripe |
|
|
|
|
La resolución usa el campo `country` del `Complex` (ISO 3166-1 alpha-2).
|
|
|
|
---
|
|
|
|
## Variables de entorno
|
|
|
|
```
|
|
# Billing
|
|
BILLING_ENV=sandbox|production
|
|
FRONTEND_BASE_URL=http://localhost:5173
|
|
|
|
# Stripe
|
|
STRIPE_SECRET_KEY=sk_test_xxx
|
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
STRIPE_PRICE_ID_USD_BASIC=price_basic_usd
|
|
STRIPE_PRICE_ID_USD_ADVANCED=price_advanced_usd
|
|
STRIPE_PRICE_ID_USD_ENTERPRISE=price_enterprise_usd
|
|
|
|
# Mercado Pago (Argentina)
|
|
MERCADOPAGO_ACCESS_TOKEN=TEST-xxx
|
|
MERCADOPAGO_WEBHOOK_SECRET=xxx
|
|
MERCADOPAGO_PLAN_ID_ARS_BASIC=2c938084...
|
|
MERCADOPAGO_PLAN_ID_ARS_ADVANCED=2c938084...
|
|
MERCADOPAGO_PLAN_ID_ARS_ENTERPRISE=2c938084...
|
|
```
|
|
|
|
### Naming de plans
|
|
|
|
Los IDs de prices/plans en Stripe y Mercado Pago se nombran con el código del Plan interno (`BASIC`, `ADVANCED`, `ENTERPRISE`) y la moneda (`USD`, `ARS`):
|
|
|
|
```
|
|
STRIPE_PRICE_ID_{CURRENCY}_{PLAN_CODE}
|
|
MERCADOPAGO_PLAN_ID_{CURRENCY}_{PLAN_CODE}
|
|
```
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
| Método | Ruta | Auth | Descripción |
|
|
|--------|------|------|-------------|
|
|
| POST | `/api/billing/checkout` | `requireAuth` + cookie | Inicia checkout de suscripción |
|
|
| POST | `/api/billing/cancel` | `requireAuth` + cookie | Cancela suscripción activa |
|
|
| GET | `/api/billing/status` | `requireAuth` + cookie | Estado actual del billing |
|
|
| POST | `/api/billing/webhooks/stripe` | none (firma) | Webhook de Stripe |
|
|
| POST | `/api/billing/webhooks/mercadopago` | none (firma) | Webhook de Mercado Pago |
|
|
|
|
El `complexId` se obtiene de la cookie `selected-complex-id`.
|
|
|
|
---
|
|
|
|
## Estados internos
|
|
|
|
```
|
|
TRIAL ──(checkout completed)──► ACTIVE
|
|
TRIAL ──(user cancels)────────► CANCELED
|
|
ACTIVE ──(payment fails)──────► PAST_DUE
|
|
PAST_DUE ──(retry ok)─────────► ACTIVE
|
|
PAST_DUE ──(user cancels)─────► CANCELED
|
|
ACTIVE ──(user cancels)───────► CANCELED
|
|
CANCELED ──(grace ends)───────► SUSPENDED
|
|
```
|
|
|
|
| Estado | Significado |
|
|
|--------|-------------|
|
|
| `TRIAL` | Período de prueba, puede usar la app |
|
|
| `ACTIVE` | Suscripción activa, puede usar la app |
|
|
| `PAST_DUE` | Pago fallido, acceso restringido |
|
|
| `CANCELED` | Cancelado por usuario, acceso restringido |
|
|
| `SUSPENDED` | Suspendido, acceso restringido |
|
|
|
|
### Helper: `canUseApp(complexId)`
|
|
|
|
```typescript
|
|
import { canUseApp } from '@/modules/billing/services/billing-access.service';
|
|
|
|
const result = await canUseApp(complexId);
|
|
if (!result.ok) {
|
|
// Rechazar acceso con mensaje según el estado
|
|
}
|
|
```
|
|
|
|
Retorna `ok(true)` si el estado es `TRIAL` o `ACTIVE`. Caso contrario retorna un `AppError` tipo `forbidden` con un mensaje específico según el estado.
|
|
|
|
---
|
|
|
|
## Flujo de Checkout
|
|
|
|
1. **Frontend** → `POST /api/billing/checkout` con `{ planCode: "BASIC" }`
|
|
2. **Backend** verifica:
|
|
- Usuario tiene acceso al complex (cookie)
|
|
- No existe suscripción activa
|
|
3. **Backend** resuelve provider según `Complex.country`
|
|
4. **Provider** genera URL de checkout (Stripe Checkout Session / Mercado Pago Preapproval)
|
|
5. **Backend** guarda/actualiza `ComplexBilling` con status `TRIAL`
|
|
6. **Frontend** redirige al usuario a `checkoutUrl`
|
|
7. **Usuario** completa pago en el provider
|
|
8. **Provider** envía webhook → backend procesa y pasa a `ACTIVE`
|
|
|
|
---
|
|
|
|
## Flujo de Webhooks
|
|
|
|
### Idempotencia
|
|
|
|
Cada webhook tiene un `providerEventId` único (Stripe: `event.id`, MP: `mp-{data.id}-{action}`). El campo `providerEventId` en `BillingEvent` tiene `@unique`.
|
|
|
|
**Antes de procesar:**
|
|
1. Buscar `BillingEvent` por `providerEventId`
|
|
2. Si existe → responder 200 (ya procesado)
|
|
3. Si no existe → procesar y crear `BillingEvent` dentro de una transacción
|
|
|
|
### Stripe
|
|
|
|
```sh
|
|
stripe listen --forward-to localhost:3000/api/billing/webhooks/stripe
|
|
```
|
|
|
|
Stripe envía los eventos al endpoint con la firma en el header `stripe-signature`.
|
|
|
|
**Eventos manejados:**
|
|
|
|
| Evento | Acción |
|
|
|--------|--------|
|
|
| `checkout.session.completed` | Activar suscripción (`ACTIVE`) |
|
|
| `customer.subscription.updated` (active) | Actualizar período |
|
|
| `customer.subscription.updated` (past_due) | Marcar como `PAST_DUE` |
|
|
| `customer.subscription.deleted` | Cancelar (`CANCELED`) |
|
|
| `invoice.payment_failed` | Marcar como `PAST_DUE` |
|
|
|
|
### Mercado Pago
|
|
|
|
Usar ngrok para exponer el servidor local:
|
|
|
|
```sh
|
|
ngrok http 3000
|
|
# Configurar la URL del webhook en el dashboard de MP:
|
|
# https://www.mercadopago.com.ar/developers/panel/webhooks
|
|
# Apuntar a: https://{ngrok-id}.ngrok.app/api/billing/webhooks/mercadopago
|
|
```
|
|
|
|
**Eventos manejados:**
|
|
|
|
| Acción | Tipo |
|
|
|--------|------|
|
|
| `subscription_authorized_payment` | `activated` → `ACTIVE` |
|
|
| `subscription_cancelled` | `cancelled` → `CANCELED` |
|
|
| `subscription_updated` | `updated` → `ACTIVE` |
|
|
| `subscription_charge_payment` | `payment_failed` → `PAST_DUE` |
|
|
|
|
> **Nota:** Los nombres exactos de eventos de Mercado Pago pueden variar. Verificar en el dashboard de desarrolladores de MP durante integración.
|
|
|
|
---
|
|
|
|
## Modelo de datos (Prisma)
|
|
|
|
### `ComplexBilling`
|
|
|
|
| Columna | Tipo | Descripción |
|
|
|---------|------|-------------|
|
|
| `complexId` | `String @id` | FK a Complex |
|
|
| `status` | `BillingStatus` | `TRIAL`, `ACTIVE`, `PAST_DUE`, `CANCELED`, `SUSPENDED` |
|
|
| `planCode` | `String` | Código del Plan (BASIC, ADVANCED, ENTERPRISE) |
|
|
| `currency` | `String` | `ARS` o `USD` |
|
|
| `provider` | `BillingProvider?` | `STRIPE`, `MERCADOPAGO`, `PAYPAL` |
|
|
| `providerCustomerId` | `String?` | ID del customer en el provider |
|
|
| `providerSubscriptionId` | `String?` | ID de la suscripción en Stripe |
|
|
| `providerPreapprovalId` | `String?` | ID del preapproval en MP |
|
|
| `currentPeriodStart` | `DateTime?` | Inicio del período actual |
|
|
| `currentPeriodEnd` | `DateTime?` | Fin del período actual |
|
|
| `trialEndsAt` | `DateTime?` | Fin del trial |
|
|
| `canceledAt` | `DateTime?` | Fecha de cancelación |
|
|
| `suspendedAt` | `DateTime?` | Fecha de suspensión |
|
|
|
|
### `BillingEvent`
|
|
|
|
| Columna | Tipo | Descripción |
|
|
|---------|------|-------------|
|
|
| `id` | `String @id` | UUID |
|
|
| `complexId` | `String` | FK a ComplexBilling |
|
|
| `eventType` | `String` | Tipo de evento del provider |
|
|
| `provider` | `BillingProvider` | Provider que originó el evento |
|
|
| `providerEventId` | `String? @unique` | ID único del evento en el provider (idempotencia) |
|
|
| `providerData` | `Json?` | Payload completo del webhook |
|
|
| `previousStatus` | `BillingStatus?` | Estado anterior |
|
|
| `newStatus` | `BillingStatus?` | Nuevo estado |
|
|
|
|
---
|
|
|
|
## Agregar un nuevo provider (ej: PayPal)
|
|
|
|
1. Crear `providers/paypal.provider.ts` implementando `BillingProvider`
|
|
2. Agregar `PAYPAL` al enum `BillingProvider` en Prisma
|
|
3. Agregar case en `provider-resolver.service.ts` si aplica
|
|
4. Agregar webhook handler en `features/paypal-webhook/`
|
|
5. Agregar ruta en `billing.routes.ts`
|
|
|
|
---
|
|
|
|
## Pruebas en local
|
|
|
|
### Sin cobrar dinero
|
|
|
|
- Stripe: Usar claves `sk_test_...`. Stripe automáticamente usa el entorno de prueba con tarjetas de prueba (`4242 4242 4242 4242`).
|
|
- Mercado Pago: Usar `BILLING_ENV=sandbox` con `MERCADOPAGO_ACCESS_TOKEN=TEST-...`.
|
|
|
|
### Stripe CLI (webhooks locales)
|
|
|
|
```sh
|
|
# Instalar: https://stripe.com/docs/stripe-cli
|
|
stripe login
|
|
stripe listen --forward-to localhost:3000/api/billing/webhooks/stripe
|
|
# Copiar el webhook secret que muestra y ponerlo en STRIPE_WEBHOOK_SECRET
|
|
```
|
|
|
|
### Mercado Pago + ngrok (webhooks locales)
|
|
|
|
```sh
|
|
ngrok http 3000
|
|
# Configurar webhook en: https://www.mercadopago.com.ar/developers/panel/webhooks
|
|
# URL: https://{ngrok-id}.ngrok.app/api/billing/webhooks/mercadopago
|
|
```
|
|
|
|
---
|
|
|
|
## Comandos
|
|
|
|
```sh
|
|
# Instalar dependencias (ya instalado: stripe, mercadopago)
|
|
bun add stripe mercadopago
|
|
|
|
# Generar cliente Prisma
|
|
bun --filter backend prisma:generate
|
|
|
|
# Crear migración
|
|
bun --filter backend prisma:migrate
|
|
|
|
# Tests
|
|
bun run test
|
|
|
|
# Dev server
|
|
bun run dev
|
|
```
|