# 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 ```