Lays the data + job foundation for tracking customer interactions, recurrence, and Pix conversion on Contact. Design decisions negotiated with Rodrigo (see docs to come): Rules: - Gap of 30h from last message defines separate interactions - Qualified interaction = >=2 customer msgs + >=2 attendant msgs, both with textual content (>= 2 letters) - One-shot consultation = >=1+1 but below the qualified threshold (tracked as secondary KPI) - Excludes contacts labeled `equipe_interna` - is_recurring = interactions_count >= 2 - pix_generated_count counts all PixCharges; reservations_paid_count only counts those with status = paid Surface area: - Migration adds denormalized stats to contacts + indexes for fast filtering - Captain::ContactStats::InteractionCalculatorService computes the stats for a single contact (pure, no persistence) - Captain::Retention::RecalculateContactStatsJob persists them for one contact (idempotent) - Captain::Retention::RecalculateAllContactStatsJob runs daily at 3am BRT, enqueues per-contact jobs for everyone active in the last 120 days - Event-driven refresh: CaptainListener#conversation_resolved enqueues recalc; Captain::PixCharge after_create/after_update enqueues recalc on status change No UI yet — that's the next layer.
30 lines
1.7 KiB
Ruby
30 lines
1.7 KiB
Ruby
class AddRetentionStatsToContacts < ActiveRecord::Migration[7.1]
|
|
# Desnormaliza estatísticas de retenção/recorrência no próprio contato.
|
|
# Atualizado por Captain::ContactStats::RecalculateJob (diário) + hooks
|
|
# incrementais quando conversa ou PixCharge muda de estado.
|
|
#
|
|
# Colunas:
|
|
# - interactions_count: interações qualificadas (≥2 msg cliente + ≥2 msg Jasmine, gap 30h)
|
|
# - one_shot_consultations_count: consultas ≥1+1 que não atingiram o limiar de qualificada
|
|
# - first_interaction_at / last_interaction_at: range da presença do cliente
|
|
# - pix_generated_count: quantos Pix foram gerados (sinal de intenção)
|
|
# - reservations_paid_count: quantos Pix foram efetivamente pagos (reserva real)
|
|
# - is_recurring: true se interactions_count >= 2
|
|
# - days_since_last_interaction: materializado pra filtros rápidos sem funções em tempo real
|
|
def change
|
|
add_column :contacts, :interactions_count, :integer, default: 0, null: false
|
|
add_column :contacts, :one_shot_consultations_count, :integer, default: 0, null: false
|
|
add_column :contacts, :first_interaction_at, :datetime
|
|
add_column :contacts, :last_interaction_at, :datetime
|
|
add_column :contacts, :pix_generated_count, :integer, default: 0, null: false
|
|
add_column :contacts, :reservations_paid_count, :integer, default: 0, null: false
|
|
add_column :contacts, :is_recurring, :boolean, default: false, null: false
|
|
add_column :contacts, :days_since_last_interaction, :integer
|
|
|
|
add_index :contacts, :last_interaction_at
|
|
add_index :contacts, :is_recurring
|
|
add_index :contacts, :days_since_last_interaction
|
|
add_index :contacts, %i[account_id is_recurring last_interaction_at], name: 'idx_contacts_account_recurring_last'
|
|
end
|
|
end
|