Volver a la wiki

Tarea Huey reconcile_stripe_subscriptions — Sincronización diaria Stripe ↔ local

Resumen

Tarea Huey (cola de tareas) que se ejecuta diariamente a las 06:40 para sincronizar el estado cacheado en Organization con el estado real de cada suscripción viva en Stripe. Cubre el riesgo “webhook perdido” del plan técnico: si un evento no llegó por timeout, este cron lo detecta y corrige.

Ubicación: billing/tasks.py:reconcile_stripe_subscriptions()

Horario: @db_periodic_task(crontab(hour=6, minute=40))

Función

@db_periodic_task(crontab(hour=6, minute=40))
def reconcile_stripe_subscriptions():
    if not settings.STRIPE_SECRET_KEY:
        return  # No-op silencioso en dev/test
    
    for org in Organization.objects.exclude(stripe_customer_id=""):
        subs = stripe.Subscription.list(
            customer=org.stripe_customer_id,
            status="all",
            limit=1
        )
        if subs.data:
            if sync_subscription_to_org(org, subs.data[0]):
                logger.warning(
                    "Stripe reconcile: org %s divergía → corregida a %s",
                    org.id, org.subscription_status
                )

Flujo

  1. Check de configuración: Si STRIPE_SECRET_KEY no existe (dev/test, PROD antes PR-4) → retorna silenciosamente (no-op)
  2. Itera orgs: Para cada org con stripe_customer_id no vacío
  3. Lee Stripe: Obtiene la suscripción más reciente viva del customer (limit=1)
  4. Compara y sincroniza: Llama sync_subscription_to_org() (el mismo helper que apply_event)
    • Si cambio detectado → loguea warning (⚠️ webhook perdido)
    • Guarda en BD
  5. Termina: Sin rollback, todos los cambios se persisten

Detección de webhooks perdidos

Cada divergencia encontrada se loguea como warning con patrón "Stripe reconcile: org {id} divergía → corregida":

logger.warning(
    "Stripe reconcile: la org %s divergía del estado real; corregida a %s (¿webhook perdido?)",
    org.id,
    org.subscription_status,
)

Esto es un indicador de riesgo. En post-análisis, el equipo de ops puede:

Sin-op en dev/test

En entornos sin STRIPE_SECRET_KEY:

En PROD pre-go-live (PR-4): mismo comportamiento, hasta que se configure la clave.

Tolerancia a errores

try:
    subs = stripe.Subscription.list(customer=org.stripe_customer_id, status="all", limit=1)
except stripe.StripeError:
    logger.exception("Stripe reconcile: no se pudo listar... org %s", org.id)
    continue  # Sigue con la siguiente org

Si un customer produce error de Stripe (ej. no existe, token revocado), el error se loguea pero no aborta la tarea. Las otras orgs se procesan normalmente.

Orgs sin stripe_customer_id

Se excluyen con .exclude(stripe_customer_id=""), así solo procesa orgs que alguna vez tuvieron un checkout exitoso.

Integración con webhook

MecanismoCubre
Webhook (stripe_webhook())Cambios en tiempo real (99% de los casos)
Reconciliación diaria (6:40)Webhooks perdidos por timeout/red (1%)

El combo da garantía de consistencia eventual: en el peor caso, la org vuelve al estado real en <24h.

Configuración

Necesita STRIPE_SECRET_KEY (misma que para stripe.Webhook.construct_event en el webhook):

# config/settings/base.py
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY") or ""

Vacío en dev/test. En PROD, solo activo en PR-4.

Comportamiento en producción esperado

Tras go-live de PR-4:

Véase también

Subir