SaaS Starter

Billing

Stripe, Mercado Pago o Polar — agnóstico al proveedor.

Estrategia

PAYMENT_PROVIDER en env decide. La interface PaymentProvider abstrae checkout, webhooks, subscriptions. Cambiar de provider = un commit.

interface PaymentProvider {
  createCheckout(input): Promise<Result<CheckoutSession, BillingError>>;
  handleWebhook(req): Promise<Result<DomainEvent, BillingError>>;
  cancelSubscription(id): Promise<Result<void, BillingError>>;
}

Providers soportados

Stripe

PAYMENT_PROVIDER=stripe. Vars: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Webhook: POST /webhooks/stripe.

Mercado Pago

PAYMENT_PROVIDER=mercado-pago. Vars: MERCADO_PAGO_ACCESS_TOKEN, MERCADO_PAGO_WEBHOOK_SECRET. Pensado para LATAM — soporta Pix, wallet de Mercado Pago, cuotas.

Polar

PAYMENT_PROVIDER=polar. Vars: POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET. Ideal para developer tools — integra con GitHub.

Modelo

Subscription ─→ Plan ─→ Feature

     └──→ Organization

Subscription.status es la SSOT (active, past_due, canceled, trialing). Los webhooks actualizan; el frontend nunca asume.

Fake (solo E2E / testing)

PAYMENT_PROVIDER=fake. Var: FAKE_WEBHOOK_SECRET (cualquier string). Devuelve planes estáticos, URLs falsas de checkout/portal y acepta webhooks firmados con HMAC-SHA256 (x-fake-signature: sha256=<hex>). Usado por la suite E2E — nunca desplegar en producción.

Test de webhooks local

stripe listen --forward-to localhost:3005/api/v1/billing/webhook

Para Mercado Pago / Polar usá ngrok. Para tests E2E: PAYMENT_PROVIDER=fake FAKE_WEBHOOK_SECRET=<secret>.

Operaciones de admin de plataforma

El Plan 05 agrega cinco métodos cross-tenant de suscripciones a IPaymentProvider. Soportan la página /admin/billing/[orgId] y los endpoints /api/v1/platform/organizations/:id/subscription/*.

MétodoDescripción
getSubscriptionSummary(customerInternalId)Devuelve la suscripción actual (null si no existe).
changePlan(customerInternalId, newPriceId)Cambia la suscripción a otro precio.
extendTrial(customerInternalId, days)Extiende trialEndsAt por [1, 90] días.
cancelSubscription(customerInternalId, { immediate })Cancela ahora o al final del período.
listInvoices(customerInternalId, { page, pageSize })Historial paginado de facturas.

[!NOTE] Solo el adapter Fake los implementa de fábrica (estado in-memory, usado por E2E y smoke tests del admin). Stripe / Polar / MercadoPago devuelven InfrastructureError("X is not implemented by the Y adapter; ...") vía el helper compartido adminUnsupported en apps/server/src/modules/billing/infrastructure/providers/admin-unsupported.ts. Para producción, reemplazá cada stub con la llamada al SDK correspondiente (ej. stripe.subscriptions.update, stripe.invoices.list).

Resolución org → customer

El modelo de billing del boilerplate es per-user: Subscription.customerInternalId === userId. Las operaciones cross-tenant de admin resuelven el target vía org → userId del owner-member (ver application/use-cases/admin/resolve-org-customer.ts). Cuando migres a billing per-org, ese archivo es el único que cambia.

Eventos de auditoría

Cada mutación emite una entrada de audit con el userId real del actor (impersonation-aware):

  • platform.org.plan_changed{ previousPriceId, newPriceId }
  • platform.org.trial_extended{ days, newTrialEndsAt }
  • platform.org.subscription_cancelled{ mode: 'immediate' | 'at_period_end' }

En esta página