iachat/enterprise/app/models/captain/lifecycle/rule.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

93 lines
2.8 KiB
Ruby

# frozen_string_literal: true
# == Schema Information
#
# Table name: captain_lifecycle_rules
#
# id :bigint not null, primary key
# description :text
# enabled :boolean default(TRUE), not null
# event :string not null
# filters :jsonb not null
# message_body :text not null
# message_payload :jsonb
# message_type :string default("text"), not null
# name :string not null
# offset_minutes :integer default(0), not null
# priority :integer default(50), not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# created_by_user_id :bigint
#
# Indexes
#
# idx_on_account_id_enabled_event_2d8b8a9942 (account_id,enabled,event)
# index_captain_lifecycle_rules_on_account_id (account_id)
# index_captain_lifecycle_rules_on_created_by_user_id (created_by_user_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (created_by_user_id => users.id)
#
class Captain::Lifecycle::Rule < ApplicationRecord
self.table_name = 'captain_lifecycle_rules'
EVENTS = %w[
reservation.confirmed
checkin.scheduled_at
checkin.detected
checkout.scheduled_at
checkout.detected
reservation.cancelled
reservation.no_show
].freeze
MESSAGE_TYPES = %w[text buttons list url_button].freeze
belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true
validates :name, presence: true
validates :event, presence: true, inclusion: { in: EVENTS }
validates :message_body, presence: true
validates :message_type, inclusion: { in: MESSAGE_TYPES }
scope :active, -> { where(enabled: true) }
scope :for_event, ->(event) { where(event: event) }
def matches_reservation?(reservation)
return false unless reservation
filters_hash = filters.presence || {}
matches_unit?(filters_hash, reservation) &&
matches_categoria?(filters_hash, reservation) &&
matches_permanencia?(filters_hash, reservation)
end
private
def matches_unit?(filters_hash, reservation)
unit_ids = Array(filters_hash['unit_ids'])
return true if unit_ids.empty?
unit_ids.include?(reservation.captain_unit_id)
end
def matches_categoria?(filters_hash, reservation)
categorias = Array(filters_hash['categorias'])
return true if categorias.empty?
categorias.include?(reservation.suite_identifier)
end
def matches_permanencia?(filters_hash, reservation)
permanencias = Array(filters_hash['permanencias'])
return true if permanencias.empty?
actual = reservation.metadata.to_h['permanencia']
permanencias.include?(actual)
end
end