142 lines
5.2 KiB
Python
142 lines
5.2 KiB
Python
"""
|
|
Modelos de dados usados por todo o pipeline de importação de plano de
|
|
saúde, independente da operadora. Porta quase 1:1 de
|
|
projects/project/core/modelos.py (ver projects/importacao-planos-saude.skill
|
|
para as regras de negócio por trás destas decisões) — a única diferença é
|
|
`LinhaSistema` não guardar mais `_linha_original` (não é usado fora do CLI
|
|
original) e passar a ter `id` (preenchido pela view a partir da
|
|
ImportacaoPlanoSaudeLinha ao editar uma importação já existente).
|
|
"""
|
|
import unicodedata
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
|
|
def normaliza_cpf(cpf: str) -> str:
|
|
"""Remove pontuação, deixando só os dígitos. Usado como chave de casamento."""
|
|
return "".join(ch for ch in cpf if ch.isdigit())
|
|
|
|
|
|
def normaliza_nome(nome: str) -> str:
|
|
"""
|
|
Normaliza um nome para comparação: maiúsculas, sem acento, sem
|
|
espaços duplicados/nas pontas. Usado como chave de casamento quando
|
|
a operadora não manda CPF (ex: Unimed) — por isso é estrito por
|
|
design: 'SCHAEFER' e 'SCHAFER' são tratados como NOMES DIFERENTES.
|
|
Divergências de grafia devem cair em auditoria, nunca ser resolvidas
|
|
por aproximação automática.
|
|
"""
|
|
sem_acento = unicodedata.normalize("NFKD", nome or "")
|
|
sem_acento = "".join(c for c in sem_acento if not unicodedata.combining(c))
|
|
return " ".join(sem_acento.upper().split())
|
|
|
|
|
|
@dataclass
|
|
class Lancamento:
|
|
"""Uma linha crua extraída do arquivo da operadora (uma rubrica/serviço)."""
|
|
numero_beneficiario: str # identificador do indivíduo no arquivo da operadora
|
|
nome: str
|
|
cpf: str # pode vir vazio quando a operadora não informa CPF (ex: Unimed)
|
|
tipo: str # 'T' = Titular, 'D' = Dependente, 'A' = Agregado
|
|
rubrica: str
|
|
valor: float # já convertido para float, com sinal (negativo = desconto/devolução)
|
|
tipo_lancamento: str = "mensalidade" # 'mensalidade' ou 'coparticipacao'
|
|
dependencia: Optional[str] = None # ex: "Conjuge", "Filho(a)", "Pai/Mãe"
|
|
numero_titular: Optional[str] = None # id do titular da família no arquivo da operadora
|
|
|
|
|
|
@dataclass
|
|
class Individuo:
|
|
"""
|
|
Um beneficiário (titular ou dependente) já com o valor do mês
|
|
agregado PARA UM tipo de lançamento (mensalidade OU coparticipação).
|
|
Uma mesma pessoa pode gerar dois objetos Individuo no mesmo mês —
|
|
um de cada tipo — quando a operadora manda os dois juntos (ex: Unimed).
|
|
"""
|
|
numero_beneficiario: str
|
|
nome: str
|
|
cpf: str
|
|
tipo: str
|
|
tipo_lancamento: str = "mensalidade"
|
|
valor_total: float = 0.0
|
|
rubricas: list = field(default_factory=list) # histórico de rubricas somadas
|
|
numero_titular: Optional[str] = None # para casamento por família (chave_casamento='nome')
|
|
|
|
@property
|
|
def cpf_normalizado(self) -> str:
|
|
return normaliza_cpf(self.cpf)
|
|
|
|
@property
|
|
def nome_normalizado(self) -> str:
|
|
return normaliza_nome(self.nome)
|
|
|
|
|
|
@dataclass
|
|
class LinhaSistema:
|
|
"""Uma linha da planilha padrão do sistema (leiaute de importação)."""
|
|
codigo_empresa: str
|
|
nome_func: str
|
|
cpf_func: str
|
|
codigo_out_emp: str
|
|
data_inicial: str
|
|
nome_dependente: str
|
|
cpf_dependente: str
|
|
valor_empresa: str
|
|
valor: str
|
|
descricao: str
|
|
|
|
def cpf_func_normalizado(self) -> str:
|
|
return normaliza_cpf(self.cpf_func)
|
|
|
|
def cpf_dependente_normalizado(self) -> str:
|
|
return normaliza_cpf(self.cpf_dependente) if self.cpf_dependente else ""
|
|
|
|
def nome_func_normalizado(self) -> str:
|
|
return normaliza_nome(self.nome_func)
|
|
|
|
def nome_dependente_normalizado(self) -> str:
|
|
return normaliza_nome(self.nome_dependente) if self.nome_dependente else ""
|
|
|
|
def eh_linha_titular(self) -> bool:
|
|
"""Linha 'do titular': dependente vazio."""
|
|
return not self.cpf_dependente.strip() and not self.nome_dependente.strip()
|
|
|
|
|
|
@dataclass
|
|
class ItemAuditoria:
|
|
motivo: str # 'VALOR_NEGATIVO' | 'NAO_CADASTRADO' | 'NOME_DIVERGENTE' | 'TIPO_INVALIDO'
|
|
numero_beneficiario: str
|
|
nome: str
|
|
cpf: str
|
|
tipo: str
|
|
valor: float
|
|
tipo_lancamento: str = ""
|
|
detalhe: str = ""
|
|
|
|
|
|
@dataclass
|
|
class VinculoNome:
|
|
"""Representação "pura" (sem ORM) de um VinculoNomeOperadora salvo
|
|
(portal_api.models) — só os campos que matcher.py precisa pra resolver
|
|
automaticamente um nome divergente já confirmado antes, sem esse pacote
|
|
(deliberadamente sem ORM) depender do Django. Montado por
|
|
ImportacaoPlanoSaudeViewSet.create() (views.py) a partir de uma consulta
|
|
real ao banco, e passado adiante até `_casa_por_nome`."""
|
|
id: int
|
|
nome_func_destino: str
|
|
nome_dependente_destino: str
|
|
|
|
|
|
@dataclass
|
|
class VinculoAplicado:
|
|
"""Registra que um Individuo foi casado com uma LinhaSistema através de
|
|
um VinculoNome já salvo (nome divergente, resolvido automaticamente),
|
|
não por nome exato — usado por views.py pra gerar um
|
|
ImportacaoPlanoSaudeAlteracao (tipo "vinculo_automatico") depois que as
|
|
linhas forem persistidas (ainda não têm `id` no momento em que
|
|
matcher.py roda)."""
|
|
indice_linha: int # índice de `linha` dentro de linhas_sistema, pra esse tipo_lancamento
|
|
tipo_lancamento: str
|
|
vinculo_id: int
|
|
nome_arquivo_operadora: str
|