feat: Aprimora cálculo de pontuação e extração de dados

- Adiciona campos situacao, anos_completos, anos_consecutivos e retornos
  na entidade Consultoria para suportar regras documentadas
- Implementa mesclagem de períodos sobrepostos para evitar contagem dupla
- Melhora componente A com cálculo por área e detecção de retornos
- Ajusta componente B com bônus por nota PPG
- Refatora componente C com bônus de continuidade e retorno
- Implementa componente D com classificação de nível de prêmio
  (Grande Prêmio, PCT, Interfarma, outros) e pontuação diferenciada
- Trata datas inconsistentes (fim < início) como períodos em aberto
- Extrai situacaoConsultoria do campo dadosConsultoria.situacaoConsultoria
This commit is contained in:
Frederico Castro
2025-12-09 22:57:46 -03:00
parent 9a8332b740
commit ff4d838f34
7 changed files with 348 additions and 83 deletions

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime
from typing import List
from ..entities.consultor import (
@@ -9,9 +9,43 @@ from ..entities.consultor import (
Premiacao,
)
from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta
from ..value_objects.periodo import Periodo
class CalculadorPontuacao:
@staticmethod
def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]:
"""
Mescla períodos sobrepostos/contíguos para evitar contagem dupla.
"""
if not periodos:
return []
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio)
mesclados: List[Periodo] = []
for p in periodos_ordenados:
if not mesclados:
mesclados.append(p)
continue
ultimo = mesclados[-1]
ultimo_fim = ultimo.fim or datetime.now()
atual_fim = p.fim or datetime.now()
if p.inicio <= ultimo_fim:
novo_fim = max(ultimo_fim, atual_fim)
mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None)
else:
mesclados.append(p)
return mesclados
@staticmethod
def _anos_completos_periodos(periodos: List[Periodo]) -> int:
ref = datetime.now()
return sum(p.anos_completos(ref) for p in periodos)
@staticmethod
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao:
if not coordenacoes:
@@ -38,19 +72,32 @@ class CalculadorPontuacao:
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
coord_do_tipo = coord_por_tipo.get(coord_escolhida_tipo, [])
anos_total = sum(c.periodo.anos_decorridos for c in coord_do_tipo)
ativo = any(c.periodo.ativo for c in coord_do_tipo)
coord_por_area = {}
for c in coord_do_tipo:
area = c.area_avaliacao or "N/A"
coord_por_area.setdefault(area, []).append(c.periodo)
anos_total = 0
ativo = False
retornos_encontrados = 0
for _, periodos_area in coord_por_area.items():
mesclados = CalculadorPontuacao._mesclar_periodos(periodos_area)
anos_total += CalculadorPontuacao._anos_completos_periodos(mesclados)
ativo = ativo or any(p.ativo for p in periodos_area)
if len(mesclados) > 1:
retornos_encontrados += 1
base = base_map.get(coord_escolhida_tipo, 0)
tempo = min(int(anos_total * mult_tempo_map.get(coord_escolhida_tipo, 0)), tempo_max_map.get(coord_escolhida_tipo, 0))
extras = 0
areas_adicionais = [a for c in coord_do_tipo for a in c.areas_adicionais]
if areas_adicionais:
extras = min(len(set(areas_adicionais)) * 20, 100)
areas_distintas = list(coord_por_area.keys())
if len(areas_distintas) > 1:
extras = min((len(areas_distintas) - 1) * 20, 100)
bonus = bonus_atual_map.get(coord_escolhida_tipo, 0) if ativo else 0
retorno = 20 if len(coord_do_tipo) > 1 else 0
retorno = 20 if retornos_encontrados > 0 else 0
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno)
@@ -60,33 +107,51 @@ class CalculadorPontuacao:
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
base = 70
anos_totais = sum(c.periodo.anos_decorridos for c in coordenacoes)
anos_totais = sum(c.periodo.anos_completos(datetime.now()) for c in coordenacoes)
tempo = min(int(anos_totais * 5), 50)
programas_distintos = len({c.id_programa for c in coordenacoes})
extras = min((programas_distintos - 1) * 20, 40)
coord_ativa = any(c.periodo.ativo for c in coordenacoes)
bonus = 20 if coord_ativa else 0
nota_bonus = 0
for c in coordenacoes:
try:
nota_num = float(c.nota_ppg)
if nota_num >= 0:
nota_bonus = 20
break
except Exception:
continue
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=nota_bonus)
@staticmethod
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao:
if not consultoria:
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
base = 150 if consultoria.eventos_recentes > 0 else 100
situacao = (consultoria.situacao or "").lower()
base = 150 if "atividade" in situacao else 100
anos = (datetime.now() - consultoria.primeiro_evento).days / 365.25
anos = consultoria.anos_completos if consultoria.anos_completos else int(
((datetime.now() - consultoria.primeiro_evento).days) // 365
)
tempo = min(int(anos * 5), 50)
extras_eventos = min(consultoria.total_eventos * 2, 20)
extras_responsavel = min(consultoria.vezes_responsavel * 5, 25)
extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0
extras = extras_eventos + extras_responsavel + extras_areas
continuidade = consultoria.anos_consecutivos
if continuidade >= 8:
bonus_continuidade = 15
elif continuidade >= 5:
bonus_continuidade = 10
elif continuidade >= 3:
bonus_continuidade = 5
else:
bonus_continuidade = 0
bonus = 0
retorno_bonus = 15 if consultoria.retornos > 0 else 0
extras = 0
bonus = bonus_continuidade + retorno_bonus
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
@@ -95,7 +160,11 @@ class CalculadorPontuacao:
if not premiacoes:
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
total_pontos = sum(p.pontos for p in premiacoes)
avaliador = [p for p in premiacoes if "avaliador" in (p.tipo or "").lower()]
outros = [p for p in premiacoes if p not in avaliador]
pontos_avaliador = min(sum(p.pontos for p in avaliador), 20)
total_pontos = pontos_avaliador + sum(p.pontos for p in outros)
total_pontos = min(total_pontos, 180)
return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0)