112 lines
3.8 KiB
Python
112 lines
3.8 KiB
Python
"""
|
|
Modelos de dados usados por todo o pipeline, independente da operadora.
|
|
"""
|
|
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
|
|
# controle interno (não vai para o CSV de saída)
|
|
_linha_original: int = -1
|
|
|
|
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 = ""
|
|
|