Feature · Stripe PR-2: Webhooks + suscripciones en Organization (v1.67.0)
Resumen
Primera pieza de la plataforma de cobro SaaS. App billing nueva que recibe eventos de Stripe (webhooks firmados), los valida idempotentemente y sincroniza estado local en Organization. PR-2 es tuberías puras: sin UI (checkout/portal = PR-3), sin decisiones de producto (suspender, cambiar plan = PR-3+), agnóstica al modelo de precios (por rack/equipo/sitio aún por definir).
Entrega: v1.67.0 · 2026-08-15
Cambios
- App
billingnueva con endpointPOST /api/billing/webhook/stripe, modeloStripeEvent(ledger global), servicios de mapeo (apply_event,sync_subscription_to_org) - Campos en Organization:
stripe_customer_id,subscription_status(none/trialing/active/past_due/canceled),current_period_end - Migración core 0032: Añade los 3 campos a Organization
- Tarea Huey diaria (06:40): reconciliación de divergencias (webhooks perdidos)
- Dependencia nueva:
stripe==15.5.0(MIT, registrada en licenses) - 12 tests con eventos sintéticos firmados HMAC
Eventos manejados
checkout.session.completed→ liga customer a orgcustomer.subscription.created/updated→ status + period_endcustomer.subscription.deleted→ canceledinvoice.payment_failed→ past_due
Configuración
STRIPE_SECRET_KEY (vacío en dev/test, PROD hasta PR-4)
STRIPE_PUBLISHABLE_KEY (vacío en dev/test, PROD hasta PR-4)
STRIPE_WEBHOOK_SECRET (vacío en dev/test, PROD hasta PR-4)
Fail-closed: sin secrets, webhook responde 503.
Endurecido en v1.163.0 — orden de eventos y reintentos (B-62)
- Campo nuevo
last_subscription_event_atenOrganization(migración core/0036) guarda elcreatedde Stripe del últimocustomer.subscription.*aplicado — detalle de los campos: [[entity—core—model—organization—stripe-fields]]. apply_event()descarta (skipped, 200) un evento de suscripción cuyocreatedsea anterior al ya aplicado: evita que un reintento viejo de Stripe pise un estado más reciente.- La comparación y la escritura ocurren bajo
select_for_update()de la fila, conSET LOCAL lock_timeout = '3s'; si el bloqueo tarda más de eso, el webhook responde 500 para que Stripe reintente en vez de aceptar el evento con un 200 y perderlo. - Tests:
tests/billing/test_mega25_r8ws_stripe_orden.py(TestStripeBloqueoConTope).
Decisiones de diseño
- Caché mínima: verdad en Stripe, solo guardamos
stripe_customer_id,subscription_status,current_period_end(+last_subscription_event_atdesde v1.163.0) - StripeEvent sin FK a Organization: tabla GLOBAL fuera de RLS (webhooks llegan sin tenant)
- Idempotencia por event_id único: reentregas no se re-procesan
- Agnóstico a eje de licencia: código vale igual con any modelo de precios (abierto hasta conv. MSP sept.)
- No toca is_active ni plan: decisiones de producto llegan con PR-3+
Véase también
- [[entity—billing—endpoint—stripe-webhook]]
- [[entity—billing—model—stripe-event]]
- [[entity—billing—service—event-applier]]
- [[entity—billing—task—stripe-reconciliation]]
- [[entity—core—model—organization]]