Notificar o trg-club no vencimento da filiação CITRG
TLDR: Quando uma filiação CITRG vence, o
citrg-apiempurra um webhook para otrg-club-apipara que o perfil PRO seja rebaixado mesmo que a pessoa nunca mais faça login.
Contexto
O rebaixamento de PRO no trg-club-api já existia, mas só rodava no login: sign-in do terapeuta → sign_in_user → auto_subscribe_pro → Subscriptions::Pro::AutoSubscribeJob.perform_now → Subscriptions::Pro::Creation::CreateFlow.call(user:, status: :active), que executa ValidateEligibility (chama o MembershipByEmail do CITRG e checa valid_until_timestamp) e depois ExpireWhenIneligible (marca a assinatura PRO como inactive e reativa regular-terapeuta).
Consequência: um terapeuta cuja filiação CITRG vence mas que nunca mais faz login mantém a assinatura PRO ativa e o perfil publicado/visível na busca indefinidamente, porque nada reavalia elegibilidade fora do caminho de login.
No citrg-api, o vencimento é calculado na leitura a partir de Membership#valid_until (um date); nenhum job vira status. Já existia um cron diário no GoodJob (Notifications::Memberships::Expiration::ExpiredYesterdayJob, 0 8 * * *) que calcula exatamente o conjunto de filiações vencidas ontem e, na época, só enviava e-mail de lembrete. O citrg-api não tinha nenhuma integração de saída com o trg-club-api (só com o Apolo, via HTTParty no ApoloService).
Abordagem escolhida (Opção A — citrg empurra no vencimento, autoritativo)
O citrg-api, a partir do cron diário de vencimento, notifica o trg-club-api de que a filiação de um membro venceu. O webhook é tratado como autoritativo: o citrg só o envia depois da própria guarda de renovação, e o trg-club rebaixa direto pelo caso de uso existente Subscriptions::Pro::ExpireWhenIneligible (pro_eligible: false), sem reconsultar o CITRG. Reconsultar ao vivo foi considerado e descartado: perde o evento quando o CITRG está indisponível no momento do processamento, e um rebaixamento indevido se auto-corrige no próximo login (auto_subscribe_pro).
Alternativas consideradas e rejeitadas:
| Opção | Por que foi rejeitada |
|---|---|
B — varredura noturna do trg-club rodando CreateFlow para todo usuário PRO ativo |
não exige mudança no citrg, mas gera N chamadas/dia e não tem semântica de push |
C — endpoint expired_since em massa no citrg + pull do trg-club |
mais superfície nova nos dois lados |
Objetivos
- Rebaixar o perfil PRO no
trg-club-apiquando a filiação CITRG vence, sem depender de login. - Manter o
citrg-apicomo iniciador/fonte da verdade do evento de vencimento. - Reusar o fluxo de rebaixamento existente (
CreateFlow/ExpireWhenIneligible) — sem lógica nova de rebaixamento e sem confiar no payload do webhook para a decisão. - Ser idempotente e seguro: reentrega, usuário já rebaixado e renovações não podem causar estado errado. Indisponibilidade do trg-club/CITRG não pode rebaixar indevidamente.
- Não degradar o comportamento de e-mail de lembrete do cron diário.
Fora de escopo
- Nenhuma mudança na lógica de elegibilidade do lado do Apolo.
- Nenhum backfill de usuários já vencidos antes desta entrega.
Mudanças
Parte 1 — citrg-api (saída: notificar o trg-club no vencimento)
app/services/trg_club_service.rb(novo) — cliente HTTP de saída espelhando as convenções doApoloService(HTTParty, URL base viaENV.fetchcom default).BASE_URL = ENV.fetch("TRG_CLUB_API_URL", "https://api.trgclub.com/")(staging éhttps://staging-api.trg.club/).- Auth: reusa o token de integração existente (
APOLO_ACCESS_TOKEN, o mesmo valor validado na entrada comoapolo-access-token), enviado emX-Security-Signature-Token. notify_membership_expired(email:, register_number:)→HTTParty.postpara o path novo do trg-club, corpo JSON{ email:, register_number: }.- Levanta
TrgClubService::Errorem qualquer falha (erro de conexão ou não-2xx) para o job enfileirador poder fazer retry.
-
app/jobs/notifications/memberships/expiration/base_expiration_job.rb— hooknotify_external(membership)(no-op por default) chamado logo depois desend_email, dentro da guarda de renovação existente. -
app/jobs/notifications/memberships/expiration/expired_yesterday_job.rb— sobrescrevenotify_externalpara enfileirar umNotifyTrgClubJob.perform_later(membership.id)por filiação vencida. E-mail e notificação falham de forma independente, e o loop do cron nunca faz HTTP. -
app/jobs/notifications/memberships/expiration/notify_trg_club_job.rb(novo) —retry_on TrgClubService::Error, wait: :polynomially_longer, attempts: 10(GoodJob persiste os retries no Postgres; janela de ~4h; visível em/good_job) ediscard_on ActiveRecord::RecordNotFound. Enviamembership.user.emailem minúsculas como chave de match eregister_numberpara rastreabilidade. -
Env — documentar
TRG_CLUB_API_URLno.env.example. Sem token novo. - Testes (Minitest, Triple-A, com mock do HTTP):
TrgClubServicemonta URL, headers e corpo corretos e levantaTrgClubService::Errorem falha de conexão e em não-2xx;ExpiredYesterdayJobenfileiraNotifyTrgClubJobpara filiação vencida e não enfileira quando existe renovação paga comvalid_untilmaior;NotifyTrgClubJobchama o service com e-mail/register_number e descarta quando a filiação não existe mais.
Parte 2 — trg-club-api (entrada: webhook que dispara o rebaixamento existente)
-
config/routes.rb— rota de webhook de entrada espelhando o padrão do zeuspay:ruby namespace :citrg do post "memberships/expired", to: "membership_expirations#create", as: :membership_expirations end -
app/controllers/citrg/membership_expirations_controller.rb(novo) — espelhaZeuspay::PaymentStatusesController:skip_before_action :verify_authenticity_token;before_action :check_citrg_token!, :save_request;head :unauthorizedquandoX-Security-Signature-Tokennão bate comCITRG_WEBHOOK_TOKEN. Nocreate, loga o request, busca o usuário e, se não achar, loga e devolve:ok(para o citrg não ficar em retry por um membro que simplesmente não tem conta no trg-club). Se achar, rebaixa inline comSubscriptions::Pro::ExpireWhenIneligible.call(user:, pro_eligible: false)e devolve:ok. Idempotente: se já rebaixado,pro_subscriptionestá em branco e é no-op. -
Lookup do usuário — match por
email:User.find_by("lower(email) = ?", params[:email].to_s.downcase.strip). -
config/initializers/citrg.rb—CITRG_WEBHOOK_TOKEN = ENV.fetch("CITRG_MEMBERSHIP_TOKEN", Rails.application.credentials.dig(:citrg, :api_membership_token))— o token de integração trg-club↔citrg já existente, mesmo valor doAPOLO_ACCESS_TOKENda Parte 1. -
Testes — request spec: 401 com token ausente/inválido (e PRO intocado); 200 + assinatura PRO
inactiveeregular-terapeutareativada para e-mail conhecido (inclusive match case-insensitive); 200 no-op em reentrega; 200 para e-mail desconhecido.
Contrato entre os dois repos
- Transporte:
POSTJSON. - Auth: segredo compartilhado no header
X-Security-Signature-Token(mesmo valor nos dois lados). - Corpo:
{ "email": "<e-mail do membro>", "register_number": <bigint> }. - Chave de match:
email. - Resposta: trg-club devolve
200para aceito/conhecido/desconhecido-mas-processado e401para token inválido. O citrg faz retry de falha de request viaNotifyTrgClubJob; o loop do cron nunca faz HTTP; o citrg não depende do corpo da resposta. - Semântica: o webhook é um evento autoritativo “este membro venceu” — o citrg só o envia depois da guarda de renovação.
Como verificar
- citrg-api (unitário):
expired_yesterday_job_test.rb,notify_trg_club_job_test.rb,trg_club_service_test.rb— o job de notificação é enfileirado para filiação vencida, pulado em caso de renovação, e falhas levantam para retry. - trg-club-api (unitário): request spec — 401 com token inválido, 200 + PRO expirado para e-mail conhecido válido, no-op para e-mail desconhecido.
- Ponta a ponta (staging): com um terapeuta cujo
valid_untilno CITRG é ontem e que está PRO+publicado no trg-club: dispararExpiredYesterdayJob(ou esperar o cron das 8h) → confirmar o POST do citrg → confirmar que o trg-club rodaExpireWhenIneligible→ confirmar assinatura PROinactive,regular-terapeutareativada e perfil fora da busca — sem o usuário logar. - Idempotência/segurança: reentregar o mesmo webhook (sem efeito duplo); renovar o membro no CITRG e então entregar (sem rebaixamento); apontar o trg-club para um CITRG inalcançável (sem rebaixamento — fail-safe).
Documentação
- R-005 — Vencimento de filiação notifica o trg-club
- trg-club-api: atualizar o doc da regra de rebaixamento de PRO para registrar o gatilho não-login (webhook de vencimento do CITRG) ao lado do gatilho de login; anotar que o token reusa
CITRG_MEMBERSHIP_TOKEN.