Documentação / Plano: jobs de boundary de assinatura (FREE + pagos)

Plano: jobs de boundary de assinatura (FREE + pagos)

Entrar

Plano: jobs de boundary de assinatura (FREE + pagos)

Resumo: renovar janelas (currentPeriodStart / currentPeriodEnd) no backend via BullMQ com delay até o fim do ciclo, re-enfileirar o próximo boundary após processar, e reconciliar no startup do worker (FREE nativo, PIX pdev_, Stripe sub_) quando Redis perder jobs ou após deploy — sem cron horário como gatilho principal.


TODOs

Marque conforme for implementando.

  • [x] shared-constant — Adicionar FREE_PLAN_RENEWAL_QUEUE_NAME em packages/shared/src/constants.ts
  • [x] fullstack-queue-helper — Criar apps/fullstack/src/lib/free-plan-renewal-queue.ts (singleton Queue + scheduleFreePlanRenewal)
  • [x] shared-renewal-logic — Extrair lógica de avanço de período FREE para módulo compartilhado (ex. helper em packages/database) usada pelo worker e por GET /api/subscription
  • [x] worker-processor — Worker: job renew-free-plan, avança se currentPeriodEnd <= now, BillingEvent, re-enfileira próximo delay
  • [x] worker-startup-reconcile — No start() do worker: reconciliar subs FREE nativas + pagos (PIX pdev_ e Stripe sub_) com delay = max(0, end - now) e jobId por ciclo, somente se o job ainda não existir
  • [x] paid-pix-reconcile — Extrair schedulePixSubscriptionExpiration de payment.service e reutilizar no startup do worker para cada sub ativa com stripeSubscriptionId pdev_
  • [x] paid-stripe-boundary-job — Novo job delayed para sync de período Stripe (sub_) em currentPeriodEnd; handler alinhado ao safety net de GET /api/subscription; expor STRIPE_SECRET_KEY no worker / deploy
  • [x] enqueue-call-sites — Chamar schedulers após criar/atualizar período FREE (signup/seed, downgrade PIX, GET /api/subscription após renew lazy) e após webhooks/checkout Stripe para boundary pago
  • [x] unit-tests — Unitários: processor FREE, schedulers FREE/PIX, processor Stripe sync (stale no-op, renew + re-queue, delay 0 se expirado)
  • [x] integration-tests — Integração worker + fila; startup reconcile idempotente

Problema atual

O RateLimitService (apps/fullstack/src/services/rate-limit.service.ts) usa currentPeriodStart / currentPeriodEnd do banco. O avanço da janela FREE hoje só persiste de forma confiável quando alguém chama GET /api/subscription (apps/fullstack/src/app/api/subscription/route.ts, ~linhas 156–189). Quem não abre o painel pode ficar com período antigo e bloqueio indevido no envio.

Planos PIX já enfileiram expire-pix-subscription em payment.service.ts, mas se Redis for limpo não há garantia de re-enfileiramento até o próximo pagamento. Planos Stripe dependem de webhook + safety net no GET; não há job no boundary.


Abordagem (alinhada ao PIX)

Seguir o padrão de apps/fullstack/src/services/payment.service.ts: queue.add(..., { delay: Math.max(0, periodEnd - now), jobId: ... }) e o helper apps/fullstack/src/lib/pix-subscriptions-queue.ts.

| Aspecto | Decisão | |--------|---------| | Disparo | Um job atrasado por subscription, com delay até currentPeriodEnd, não cron horário | | jobId | subscriptionId + timestamp do fim do ciclo (ex. free-plan-renew:${id}:${periodEnd.getTime()}), mesma ideia que pix-expire:${id}:${periodEnd.getTime()} | | Após processar | Worker atualiza o banco e enfileira o próximo job até o novo currentPeriodEnd | | Job atrasado / GET já renovou | Processor idempotente: se currentPeriodEnd > now, no-op | | Redis reset / fila vazia | No start() do worker (apps/worker/src/index.ts), reconciliar com getJob(jobId) ou add + tratar duplicata: (1) FREE nativo; (2) PIX pdev_ na fila pix-subscriptions; (3) Stripe sub_ com job dedicado de sync | | Safety net UI | Manter bloco lazy em GET /api/subscription |

sequenceDiagram
    participant FS as Fullstack_or_WorkerStartup
    participant Q as BullMQ_Redis
    participant W as Worker
    participant DB as Postgres

    FS->>Q add delay until periodEnd
    Note over Q: Job dorme até fim do ciclo
    Q->>W job fires
    W->>DB read subscription
    alt periodEnd still in past
        W->>DB advance period plus limits
        W->>Q add next delay until new periodEnd
    else already renewed
        W->>W no-op
    end

Implementação

1. packages/shared/src/constants.ts

  • Constante FREE_PLAN_RENEWAL_QUEUE_NAME = "free-plan-renewal" (e, se optar por fila Stripe dedicada, constante adicional, ex. STRIPE_SUBSCRIPTION_BOUNDARY_QUEUE_NAME).

2. Fullstack — apps/fullstack/src/lib/free-plan-renewal-queue.ts

  • Singleton Queue no Redis (espelhar pix-subscriptions-queue.ts).
  • scheduleFreePlanRenewal({ subscriptionId, periodEnd }):
    • delay = Math.max(0, periodEnd.getTime() - Date.now())
    • jobId = free-plan-renew:${subscriptionId}:${periodEnd.getTime()}
    • Nome do job: ex. "renew-free-plan"
    • Payload: { subscriptionId }
    • Tratar job duplicado (log debug / ignorar).

3. Lógica compartilhada FREE

  • Extrair o loop while (newEnd <= now) + prisma.subscription.update + billing event de apps/fullstack/src/app/api/subscription/route.ts para função compartilhada importável pelo worker e pelo fullstack (ex. em packages/database).

4. Worker — apps/worker/src/index.ts

  • Queue + Worker para FREE_PLAN_RENEWAL_QUEUE_NAME (sem repeat cron).
  • processFreePlanRenewal: guardas (FREE nativo), idempotência, avanço compartilhado, re-enfileirar próximo boundary.
  • start(): reconcileFreePlanRenewalJobs() + reconciliação PIX + Stripe conforme seções abaixo.

5. Pontos de chamada FREE

  • Após lazy renew em GET /api/subscription.
  • Criação da sub FREE no onboarding (subscriptions.create / seed).
  • processPixSubscriptionExpiration quando faz downgrade para FREE — agendar próximo boundary FREE.

5b. Planos pagos

PIX (Pague Dev)

  • Função schedulePixSubscriptionExpiration({ subscriptionId, periodEnd }) compartilhada (fullstack + worker), espelhando payment.service.ts (pix-subscriptions, "expire-pix-subscription", jobId: pix-expire:...).
  • Startup do worker: para cada sub com pdev_, plano pago, status ativo, currentPeriodEnd definido — agendar se job não existir.

Stripe (sub_)

  • Novo job delayed (preferir fila própria ou segundo worker na mesma conexão, para não misturar com handler PIX), ex. "sync-stripe-subscription-period".
  • jobId = stripe-period-sync:${subscriptionId}:${periodEnd.getTime()}.
  • Processor: subscriptions.retrieve, atualizar período no Prisma (alinhado a subscription/route.ts ~111–151); re-enfileirar próximo boundary.
  • Deploy: STRIPE_SECRET_KEY no container do worker (ex. ilumin.yml).
  • Enfileirar também após stripe/webhook e checkout quando currentPeriodEnd mudar.

6. Testes

  • Unitários: FREE processor, schedulers FREE/PIX, Stripe sync processor.
  • Integração: padrão apps/worker/src/test/integration/index.integration.test.ts; reconcile idempotente.

Fora de escopo (por ora)

  • Cron horário como gatilho principal.
  • Sweep diário opcional para drift extremo (cinto extra).

Impacto no RateLimitService

Nenhuma mudança obrigatória: após o job rodar no boundary, o banco reflete a nova janela no tempo do ciclo (módulo clock/fila).


Referências rápidas no repositório

| Arquivo | Papel | |---------|--------| | apps/fullstack/src/services/rate-limit.service.ts | Contagem mensal usa período do banco | | apps/fullstack/src/app/api/subscription/route.ts | Renew lazy FREE + safety net Stripe | | apps/fullstack/src/services/payment.service.ts | Enfileira expire-pix-subscription com delay | | apps/fullstack/src/lib/pix-subscriptions-queue.ts | Fila PIX | | apps/worker/src/index.ts | processPixSubscriptionExpiration, start() | | apps/fullstack/src/app/api/stripe/webhook/route.ts | Atualizações de assinatura Stripe |