Pagamento por checkout + consolidação por veículo (em vez de rota)
Regra de pagamento: - Entrega.pagas agora exige status='completed' E checkout preenchido (antes era checkin). Afeta dashboard, painel do motorista, job 1h e elegibilidade das consolidações de forma consistente. Veículo no lugar de rota na consolidação: - migration troca consolidacoes.route_ids por vehicle_ids (jsonb + gin) - Entrega: da_veiculo/veiculos_unicos; contar_pagas usa vehicle - new.html.erb: filtro "Veículos" (multi-select por vehicle) - validar.html.erb: mostra o veículo em vez do código da rota - filtros/elegibilidade do controller passam a usar vehicle_ids Docs/diagnóstico atualizados. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ Default credentials (change in production): `admin@gade.com` / `Gade@2026!`
|
||||
|
||||
The table `public.db_reem_simplerout_2026` is owned by an external system (SimpleRoute) that updates it every hour. **NEVER** create migrations for it, and never run INSERT/UPDATE/DELETE/DROP/TRUNCATE on it.
|
||||
|
||||
`Entrega` (app/models/entrega.rb) maps to this table and enforces `readonly? = true`. All queries go through its scopes — the most important being `.pagas` (`status='completed' AND checkin IS NOT NULL`), which defines payment eligibility.
|
||||
`Entrega` (app/models/entrega.rb) maps to this table and enforces `readonly? = true`. All queries go through its scopes — the most important being `.pagas` (`status='completed' AND checkout IS NOT NULL`), which defines payment eligibility. Consolidations filter eligible deliveries by **vehicle** (`vehicle` column → `Consolidacao#vehicle_ids`), not by route.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class ConsolidacaoEntregasController < ApplicationController
|
||||
.da_conta_gade
|
||||
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
|
||||
.do_motorista(@motorista)
|
||||
@entregas = @entregas.da_rota(@consolidacao.route_ids) if @consolidacao.route_ids.present?
|
||||
@entregas = @entregas.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
|
||||
@entregas = @entregas.order(:planned_date)
|
||||
|
||||
# Classificações já feitas — indexadas por tracking_id
|
||||
@@ -139,18 +139,18 @@ class ConsolidacaoEntregasController < ApplicationController
|
||||
nome.present? && @consolidacao.consolidacao_motoristas.exists?(motorista_nome: nome)
|
||||
end
|
||||
|
||||
# tracking_ids pagos/elegíveis para o motorista no período (respeita rotas)
|
||||
# tracking_ids pagos/elegíveis para o motorista no período (respeita veículos)
|
||||
def tracking_ids_elegiveis(motorista)
|
||||
return [] unless motorista.present?
|
||||
|
||||
rel = Entrega.pagas.da_conta_gade
|
||||
.no_periodo(@consolidacao.data_inicio, @consolidacao.data_fim)
|
||||
.do_motorista(motorista)
|
||||
rel = rel.da_rota(@consolidacao.route_ids) if @consolidacao.route_ids.present?
|
||||
rel = rel.da_veiculo(@consolidacao.vehicle_ids) if @consolidacao.vehicle_ids.present?
|
||||
rel.pluck(:tracking_id)
|
||||
end
|
||||
|
||||
# Valida que a entrega pertence ao motorista informado e ao período/rota da
|
||||
# Valida que a entrega pertence ao motorista informado e ao período/veículo da
|
||||
# consolidação — impede classificar tracking_ids arbitrários via requisição.
|
||||
def entrega_elegivel?(motorista, tracking_id)
|
||||
motorista_na_consolidacao?(motorista) &&
|
||||
|
||||
@@ -22,8 +22,8 @@ class ConsolidacoesController < ApplicationController
|
||||
@consolidacoes = @consolidacoes.where(id: ids)
|
||||
end
|
||||
|
||||
if params[:route_id].present?
|
||||
@consolidacoes = @consolidacoes.where('route_ids @> ?', [params[:route_id]].to_json)
|
||||
if params[:vehicle].present?
|
||||
@consolidacoes = @consolidacoes.where('vehicle_ids @> ?', [params[:vehicle]].to_json)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ class ConsolidacoesController < ApplicationController
|
||||
data_fim: Date.current
|
||||
)
|
||||
@motoristas = Entrega.motoristas_ativos
|
||||
@rotas = Entrega.rotas_unicas
|
||||
@veiculos = Entrega.veiculos_unicos
|
||||
end
|
||||
|
||||
# POST /consolidacoes
|
||||
@@ -160,10 +160,10 @@ class ConsolidacoesController < ApplicationController
|
||||
|
||||
def preparar_form_new
|
||||
@motoristas = Entrega.motoristas_ativos
|
||||
@rotas = Entrega.rotas_unicas
|
||||
@veiculos = Entrega.veiculos_unicos
|
||||
end
|
||||
|
||||
def consolidacao_params
|
||||
params.require(:consolidacao).permit(:nome, :data_inicio, :data_fim, route_ids: [])
|
||||
params.require(:consolidacao).permit(:nome, :data_inicio, :data_fim, vehicle_ids: [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,7 +61,7 @@ class DashboardController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
# Por operação (route_id → contact_name)
|
||||
# Por local de entrega (contact_name)
|
||||
@por_operacao = entregas.pagas
|
||||
.group(:contact_name)
|
||||
.count
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# app/jobs/atualizar_historico_estimado_job.rb
|
||||
#
|
||||
# Roda a cada 1 hora (via Whenever → rake historico:atualizar).
|
||||
# Lógica: conta entregas do dia com status='completed' E checkin IS NOT NULL,
|
||||
# Lógica: conta entregas do dia com status='completed' E checkout IS NOT NULL,
|
||||
# multiplica pelo preco_entrega configurado e persiste em historico_estimados.
|
||||
#
|
||||
class AtualizarHistoricoEstimadoJob < ApplicationJob
|
||||
|
||||
@@ -73,14 +73,14 @@ class Consolidacao < ApplicationRecord
|
||||
end
|
||||
|
||||
# Entregas pagas elegíveis para um motorista no período, respeitando o filtro
|
||||
# de rotas da consolidação (quando houver). Fonte única usada pelo wizard,
|
||||
# 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,
|
||||
route_id: route_ids.presence
|
||||
vehicle: vehicle_ids.presence
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ class Entrega < ApplicationRecord
|
||||
|
||||
# ── Scopes ──────────────────────────────────────────────────
|
||||
scope :concluidas, -> { where(status: 'completed') }
|
||||
scope :com_checkin, -> { where.not(checkin: nil) }
|
||||
scope :pagas, -> { concluidas.com_checkin }
|
||||
scope :com_checkout, -> { where.not(checkout: nil) }
|
||||
# Pagamento = entrega concluída com checkout registrado
|
||||
scope :pagas, -> { concluidas.com_checkout }
|
||||
scope :pendentes, -> { where.not(status: 'completed') }
|
||||
|
||||
scope :no_periodo, ->(inicio, fim) {
|
||||
@@ -27,8 +28,8 @@ class Entrega < ApplicationRecord
|
||||
where(driver: nome)
|
||||
}
|
||||
|
||||
scope :da_rota, ->(route_id) {
|
||||
where(route_id: route_id)
|
||||
scope :da_veiculo, ->(vehicle) {
|
||||
where(vehicle: vehicle)
|
||||
}
|
||||
|
||||
scope :do_mes, ->(data = Date.current) {
|
||||
@@ -48,18 +49,18 @@ class Entrega < ApplicationRecord
|
||||
base.distinct.order(:driver).pluck(:driver).compact.reject(&:empty?)
|
||||
end
|
||||
|
||||
# Lista rotas únicas (route_id → descrição)
|
||||
def self.rotas_unicas(inicio: nil, fim: nil)
|
||||
# Lista veículos únicos (para selects/filtros de consolidação)
|
||||
def self.veiculos_unicos(inicio: nil, fim: nil)
|
||||
base = da_conta_gade
|
||||
base = base.no_periodo(inicio, fim) if inicio && fim
|
||||
base.distinct.pluck(:route_id).compact
|
||||
base.distinct.order(:vehicle).pluck(:vehicle).compact.reject(&:empty?)
|
||||
end
|
||||
|
||||
# Contagem de entregas pagas para cálculo estimado
|
||||
def self.contar_pagas(inicio:, fim:, motorista: nil, route_id: nil)
|
||||
def self.contar_pagas(inicio:, fim:, motorista: nil, vehicle: nil)
|
||||
base = pagas.da_conta_gade.no_periodo(inicio, fim)
|
||||
base = base.do_motorista(motorista) if motorista.present?
|
||||
base = base.da_rota(route_id) if route_id.present?
|
||||
base = base.da_veiculo(vehicle) if vehicle.present?
|
||||
base.count
|
||||
end
|
||||
|
||||
@@ -77,12 +78,12 @@ class Entrega < ApplicationRecord
|
||||
status == 'completed'
|
||||
end
|
||||
|
||||
def checkin_registrado?
|
||||
checkin.present?
|
||||
def checkout_registrado?
|
||||
checkout.present?
|
||||
end
|
||||
|
||||
def elegivel_pagamento?
|
||||
concluida? && checkin_registrado?
|
||||
concluida? && checkout_registrado?
|
||||
end
|
||||
|
||||
def atraso_minutos
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
NF <%= e.numero_nf %> · <%= e.local %>
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs">
|
||||
📅 <%= e.planned_date %> · 📍 <%= e.route_id.to_s.first(8) %>…
|
||||
📅 <%= e.planned_date %> · 🚚 <%= e.vehicle.presence || 'sem veículo' %>
|
||||
<% if e.atraso_minutos > 0 %> · ⏱️ atraso <%= e.atraso_minutos %>min<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -63,16 +63,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Rotas — multi-select opcional %>
|
||||
<%# Veículos — multi-select opcional %>
|
||||
<div>
|
||||
<label class="block text-white font-semibold mb-2">
|
||||
Rotas <span class="text-gray-500 font-normal text-sm">(opcional — filtra entregas por operação)</span>
|
||||
Veículos <span class="text-gray-500 font-normal text-sm">(opcional — filtra entregas por veículo)</span>
|
||||
</label>
|
||||
<div class="bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg p-3 max-h-48 overflow-y-auto space-y-1">
|
||||
<% @rotas.each do |r| %>
|
||||
<label class="flex items-center gap-2 text-white hover:bg-[#1a1a1a] rounded px-2 py-2 cursor-pointer text-sm font-mono min-h-[44px]">
|
||||
<%= check_box_tag 'consolidacao[route_ids][]', r, false, class: 'rounded text-orange-500 bg-[#1a1a1a] border-[#2a2a2a]', id: nil %>
|
||||
<%= r %>
|
||||
<% if @veiculos.blank? %>
|
||||
<p class="text-gray-500 text-sm px-2 py-2">Nenhum veículo encontrado no período.</p>
|
||||
<% end %>
|
||||
<% @veiculos.each do |v| %>
|
||||
<label class="flex items-center gap-2 text-white hover:bg-[#1a1a1a] rounded px-2 py-2 cursor-pointer text-sm min-h-[44px]">
|
||||
<%= check_box_tag 'consolidacao[vehicle_ids][]', v, false, class: 'rounded text-orange-500 bg-[#1a1a1a] border-[#2a2a2a]', id: nil %>
|
||||
<%= v %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Consolidação passa a filtrar por VEÍCULO em vez de rota (route_id era um
|
||||
# UUID pouco legível). Troca a coluna route_ids por vehicle_ids.
|
||||
class SwapRouteIdsForVehicleIdsOnConsolidacoes < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
unless column_exists?(:consolidacoes, :vehicle_ids)
|
||||
add_column :consolidacoes, :vehicle_ids, :jsonb, default: []
|
||||
add_index :consolidacoes, :vehicle_ids, using: :gin
|
||||
end
|
||||
remove_column :consolidacoes, :route_ids if column_exists?(:consolidacoes, :route_ids)
|
||||
end
|
||||
|
||||
def down
|
||||
unless column_exists?(:consolidacoes, :route_ids)
|
||||
add_column :consolidacoes, :route_ids, :jsonb, default: []
|
||||
add_index :consolidacoes, :route_ids, using: :gin
|
||||
end
|
||||
remove_column :consolidacoes, :vehicle_ids if column_exists?(:consolidacoes, :vehicle_ids)
|
||||
end
|
||||
end
|
||||
@@ -24,14 +24,14 @@ namespace :reem do
|
||||
puts " status='completed': #{no_periodo.where(status: 'completed').count}"
|
||||
puts " checkin preenchido: #{no_periodo.where.not(checkin: nil).count}"
|
||||
puts " checkout preenchido: #{no_periodo.where.not(checkout: nil).count}"
|
||||
puts " .pagas (completed + checkin): #{no_periodo.pagas.count}"
|
||||
puts " .pagas (completed + checkout): #{no_periodo.pagas.count}"
|
||||
puts "-------------------------------------------------"
|
||||
puts "planned_date mais antiga: #{Entrega.minimum(:planned_date)}"
|
||||
puts "planned_date mais recente: #{Entrega.maximum(:planned_date)}"
|
||||
puts "================================================="
|
||||
puts "Leitura:"
|
||||
puts " • 'SEM filtro' > 0 e 'com filtro Gade' = 0 → ajuste DB_EXISTING_ACCOUNT_ID no .env"
|
||||
puts " • total > 0 mas '.pagas' = 0 (checkin vazio) → regra de pagamento deveria usar checkout"
|
||||
puts " • total > 0 mas '.pagas' = 0 (checkout vazio) → entregas sem checkout não contam como pagas"
|
||||
puts " • tudo 0 → não há dados nesse período (veja datas acima)"
|
||||
rescue => e
|
||||
warn "[diagnostico] Falha: #{e.class} - #{e.message}"
|
||||
|
||||
Reference in New Issue
Block a user