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_NAMEempackages/shared/src/constants.ts - [x] fullstack-queue-helper — Criar
apps/fullstack/src/lib/free-plan-renewal-queue.ts(singletonQueue+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 porGET /api/subscription - [x] worker-processor — Worker: job
renew-free-plan, avança securrentPeriodEnd <= now,BillingEvent, re-enfileira próximo delay - [x] worker-startup-reconcile — No
start()do worker: reconciliar subs FREE nativas + pagos (PIXpdev_e Stripesub_) comdelay = max(0, end - now)ejobIdpor ciclo, somente se o job ainda não existir - [x] paid-pix-reconcile — Extrair
schedulePixSubscriptionExpirationdepayment.servicee reutilizar no startup do worker para cada sub ativa comstripeSubscriptionIdpdev_ - [x] paid-stripe-boundary-job — Novo job delayed para sync de período Stripe (
sub_) emcurrentPeriodEnd; handler alinhado ao safety net deGET /api/subscription; exporSTRIPE_SECRET_KEYno worker / deploy - [x] enqueue-call-sites — Chamar schedulers após criar/atualizar período FREE (signup/seed, downgrade PIX,
GET /api/subscriptionapó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
Queueno Redis (espelharpix-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 deapps/fullstack/src/app/api/subscription/route.tspara função compartilhada importável pelo worker e pelo fullstack (ex. empackages/database).
4. Worker — apps/worker/src/index.ts
Queue+WorkerparaFREE_PLAN_RENEWAL_QUEUE_NAME(semrepeatcron).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). processPixSubscriptionExpirationquando faz downgrade para FREE — agendar próximo boundary FREE.
5b. Planos pagos
PIX (Pague Dev)
- Função
schedulePixSubscriptionExpiration({ subscriptionId, periodEnd })compartilhada (fullstack + worker), espelhandopayment.service.ts(pix-subscriptions,"expire-pix-subscription",jobId: pix-expire:...). - Startup do worker: para cada sub com
pdev_, plano pago, status ativo,currentPeriodEnddefinido — 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 asubscription/route.ts~111–151); re-enfileirar próximo boundary. - Deploy:
STRIPE_SECRET_KEYno container do worker (ex.ilumin.yml). - Enfileirar também após
stripe/webhooke checkout quandocurrentPeriodEndmudar.
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 |