iachat/enterprise/app/jobs/captain/retention/churn_outreach_scheduler_job.rb
Rodribm10 cfffea9c16 feat(captain): semantic memory fixes + roleta + reclamações + analytics
Consolida o trabalho desta branch de abril/2026 em um bloco pronto pra
testar em staging antes do merge pra main.

## Correções de memória semântica
- ExtractionService: Princípio Zero + Regra de Ouro (ação consumada vs intenção).
- Cenário Daniela_Reservas: Passo 0 de classificação (consulta/intenção/fora).

## Roleta da Sorte (end-to-end)
- Schema Supabase + 7 RPCs atômicas (server-side, idempotentes).
- Services: Offer, Redeem, WeeklyReport.
- Jobs: OfferRouletteJob (hook em ConfirmationService após Pix pago),
  NotifyRevealed + Scheduler de fallback.
- Tool manual GenerateRoletaLinkTool + endpoint público /roleta/notify.
- Dashboard /captain/roleta com Resgate + Relatório + anomaly detection.

## Cenário Reclamacoes_Ouvidoria
- Triagem P1-P4, framework LAST, Three-level listening, Self-check.
- Sem compensação material, detecção de cliente frustrado eleva prioridade.

## Analytics
- Funil de conversão /captain/funnel: 5 etapas via regex, zero LLM.
- Detector de churn via ChurnOutreach* (cron dias úteis 10h-17h BRT).

## Trabalho pré-existente incluído
- Captain Executive Reports (ceo_digest, mattermost_delivery).
- get_reserva_preco_tool, Lifecycle ajustes, Reservations UI polimentos.

## Outros
- .gitignore: patterns pra credenciais.
- Migrations de scenarios idempotentes.
- i18n completa pt_BR+en pra roleta/funnel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:36:25 -03:00

78 lines
2.4 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
# Dispara mensagem de re-engajamento pra clientes que sumiram.
# Critérios:
# - 2+ reservas pagas no histórico
# - Última reserva há 60+ dias
# - Nenhum outreach nos últimos 180 dias (custom_attributes.churn_outreach_at)
# Segurança:
# - Só dias úteis 10h18h BRT (cron garante)
# - Máximo 20 mensagens por account por dia
class Captain::Retention::ChurnOutreachSchedulerJob < ApplicationJob
MIN_PAID_RESERVATIONS = 2
DAYS_SILENT = 60
COOLDOWN_DAYS = 180
MAX_PER_ACCOUNT_DAY = 20
queue_as :scheduled_jobs
def perform
return unless within_business_hours?
Account.find_each do |account|
# Skip accounts without any captain assistant
next unless Captain::Assistant.exists?(account_id: account.id)
eligible = find_eligible_contacts(account)
next if eligible.empty?
already_sent_today = sent_today_count(account)
budget = MAX_PER_ACCOUNT_DAY - already_sent_today
next if budget <= 0
eligible.limit(budget).each do |contact|
Captain::Retention::ChurnOutreachJob.perform_later(contact.id)
end
end
rescue StandardError => e
Rails.logger.error("[ChurnOutreachScheduler] #{e.class}: #{e.message}")
end
private
def within_business_hours?
now = Time.current.in_time_zone('America/Sao_Paulo')
return false if now.saturday? || now.sunday?
(10..17).cover?(now.hour)
end
def find_eligible_contacts(account)
cutoff_last_res = DAYS_SILENT.days.ago
cutoff_outreach = COOLDOWN_DAYS.days.ago.iso8601
# Contatos com 2+ reservas PAGAS e última reserva antes do cutoff
contact_ids = Captain::Reservation
.where(account_id: account.id, payment_status: 'paid')
.group(:contact_id)
.having('COUNT(*) >= ? AND MAX(check_in_at) < ?', MIN_PAID_RESERVATIONS, cutoff_last_res)
.pluck(:contact_id)
return Contact.none if contact_ids.empty?
account.contacts
.where(id: contact_ids)
.where(
"custom_attributes ->> 'churn_outreach_at' IS NULL OR custom_attributes ->> 'churn_outreach_at' < ?",
cutoff_outreach
)
end
def sent_today_count(account)
today = Time.zone.today.iso8601
account.contacts
.where("custom_attributes ->> 'churn_outreach_at' >= ?", today)
.count
end
end