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
│
└──→ OrganizationSubscription.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/webhookPara 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étodo | Descripció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 compartidoadminUnsupportedenapps/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' }