""" 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 = ""