CreaRack-SL

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 billing nueva con endpoint POST /api/billing/webhook/stripe, modelo StripeEvent (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 org
  • customer.subscription.created/updated → status + period_end
  • customer.subscription.deleted → canceled
  • invoice.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_at en Organization (migración core/0036) guarda el created de Stripe del último customer.subscription.* aplicado — detalle de los campos: [[entity—core—model—organization—stripe-fields]].
  • apply_event() descarta (skipped, 200) un evento de suscripción cuyo created sea 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, con SET 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_at desde 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]]