Files
playzer/docs/billing.md
Jose Selesan e06bc12097 feat(billing): implement billing module with Stripe and Mercado Pago integration
- 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.
2026-06-29 15:55:20 -03:00

9.3 KiB

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)

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. FrontendPOST /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

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:

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 activatedACTIVE
subscription_cancelled cancelledCANCELED
subscription_updated updatedACTIVE
subscription_charge_payment payment_failedPAST_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)

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

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

# 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