# app/models/consolidacao.rb class Consolidacao < ApplicationRecord self.table_name = 'consolidacoes' belongs_to :criador, class_name: 'User', foreign_key: :created_by belongs_to :finalizador, class_name: 'User', foreign_key: :finalizado_por, optional: true belongs_to :arquivador, class_name: 'User', foreign_key: :arquivado_por, optional: true has_many :consolidacao_motoristas, dependent: :destroy has_many :consolidacao_entregas, dependent: :destroy # ── Enums ─────────────────────────────────────────────────── enum status: { rascunho: 0, finalizada: 1, arquivada: 2 } # ── Soft delete ───────────────────────────────────────────── scope :ativas, -> { where(deleted_at: nil) } scope :arquivadas, -> { where.not(deleted_at: nil) } # ── Validações ────────────────────────────────────────────── validates :nome, presence: true validates :data_inicio, presence: true validates :data_fim, presence: true validates :created_by, presence: true validate :periodo_valido # ── Callbacks ─────────────────────────────────────────────── before_save :recalcular_valor_total # ── Escopos ───────────────────────────────────────────────── scope :recentes, -> { order(created_at: :desc) } scope :no_periodo, ->(i, f) { where('data_inicio >= ? AND data_fim <= ?', i, f) } # ── Filtro por status de pagamento (derivado dos motoristas) ─ # pago_em NÃO-nulo conta como pago; COUNT(pago_em) ignora os NULL. scope :pagamento_pago, -> { where(id: ConsolidacaoMotorista.group(:consolidacao_id) .having('COUNT(*) > 0 AND COUNT(*) = COUNT(pago_em)') .select(:consolidacao_id)) } scope :pagamento_pendente, -> { where(id: ConsolidacaoMotorista.group(:consolidacao_id) .having('COUNT(pago_em) = 0') .select(:consolidacao_id)) } scope :pagamento_parcial, -> { where(id: ConsolidacaoMotorista.group(:consolidacao_id) .having('COUNT(pago_em) > 0 AND COUNT(pago_em) < COUNT(*)') .select(:consolidacao_id)) } # Consolidações que contêm QUALQUER das operações informadas (jsonb). # SEGURANÇA: os nomes passam por whitelist (Operacao.sanitizar) e por quote. scope :com_operacoes, ->(ops) { validas = Operacao.sanitizar(ops) next all if validas.empty? lista = validas.map { |o| connection.quote(o) }.join(',') where("EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(operacoes, '[]'::jsonb)) e WHERE e IN (#{lista}))") } # ── Métodos ───────────────────────────────────────────────── def arquivar!(user) update!( deleted_at: Time.current, status: :arquivada, arquivado_por: user&.id, arquivado_em: Time.current ) end # Restaura uma consolidação arquivada. Como o status original não é guardado, # inferimos: se já tinha sido finalizada (finalizado_em presente) volta para # :finalizada, senão para :rascunho. Limpa os campos de arquivamento. def reativar!(_user) update!( deleted_at: nil, status: finalizado_em? ? :finalizada : :rascunho, arquivado_por: nil, arquivado_em: nil ) end # ── Pagamento (status derivado dos motoristas da consolidação) ── # :pendente (nenhum pago) · :pago (todos pagos) · :parcial (alguns). def status_pagamento total = consolidacao_motoristas.count return :pendente if total.zero? pagos = consolidacao_motoristas.pagos.count return :pendente if pagos.zero? pagos >= total ? :pago : :parcial end def valor_pago consolidacao_motoristas.pagos.sum(:valor_total) end def valor_pendente consolidacao_motoristas.pendentes.sum(:valor_total) end def finalizar!(user) return false unless todas_entregas_classificadas? update!( status: :finalizada, finalizado_por: user.id, finalizado_em: Time.current ) end # Só está completa quando CADA motorista tem todas as suas entregas elegíveis # classificadas. (Antes era uma soma agregada com >=, que mascarava motoristas # pendentes quando outro tinha entregas a mais.) def todas_entregas_classificadas? consolidacao_motoristas.all? do |cm| total = entregas_elegiveis_count(cm.motorista_nome) total.zero? || classificadas_count(cm.motorista_nome) >= total end end def percentual_classificado return 0 if total_entregas_estimadas.zero? ((consolidacao_entregas.count.to_f / total_entregas_estimadas) * 100).round end def total_entregas_estimadas @total_entregas_estimadas ||= consolidacao_motoristas.sum do |cm| entregas_elegiveis_count(cm.motorista_nome) end end # Entregas pagas elegíveis para um motorista no período, respeitando o filtro # de veículos da consolidação (quando houver). Fonte única usada pelo wizard, # pela revisão e pelo gate de finalização. def entregas_elegiveis_count(motorista) Entrega.contar_pagas( inicio: data_inicio, fim: data_fim, motorista: motorista, vehicle: vehicle_ids.presence, operacoes: operacoes.presence ) end # Rótulos amigáveis das operações vinculadas (para exibição). def operacoes_label Array(operacoes).map { |t| Operacao.label(t) } end # Entrega conta como classificada se tiver ≥1 pilar (distinct por tracking_id, # já que agora pode haver vários pilares por entrega). def classificadas_count(motorista) consolidacao_entregas.where(motorista_nome: motorista).distinct.count(:tracking_id) end private def periodo_valido return unless data_inicio && data_fim errors.add(:data_fim, 'deve ser após a data de início') if data_fim < data_inicio end def recalcular_valor_total self.valor_total = consolidacao_motoristas.sum(:valor_total) end end