File manager - Edit - /var/www/html/portalHomolog/app/Http/Services/Importacoes/ImportacaoProdataContaAngraService.php
Back
<?php namespace App\Http\Services\Importacoes; use App\Models\Permissao; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; /** * Service de persistencia da importacao de servidores municipais (Prodata) como * usuarios do Conta Angra. * * Responsabilidades: * - Validar e normalizar campos do CSV (CPF, email, matricula, nome); * - Decidir entre criar ou atualizar com base no CPF; * - Aplicar `dirty check` em updates (somente quando ha divergencia real em * `matriculation`, `structure_id` ou `email`); * - Criar permissoes padrao para usuarios novos sem alterar permissoes * pre-existentes (preserva o que ja estiver concedido); * - Tratar conflito de email entre CPFs sem interromper a importacao. */ class ImportacaoProdataContaAngraService { public const RESULTADO_CRIADO = 'criado'; public const RESULTADO_ATUALIZADO = 'atualizado'; public const RESULTADO_SINCRONIZADO = 'sincronizado'; public const RESULTADO_INVALIDO = 'invalido'; public const RESULTADO_ERRO = 'erro'; public const LOG_CHANNEL = 'importacao_prodata_contaangra'; public const PERMISSOES_PADRAO = [ 'GERENCIADOR_DE_CONTA_ANGRA', 'GUEST', ]; /** * Conflitos de e-mail (CSV x usuario ja cadastrado) acumulados na execucao. * * @var array<int,array<string,mixed>> */ protected array $conflitosEmailDetalhados = []; /** * Processa uma linha do CSV (array associativo com as chaves padronizadas * em minusculo: `cpf`, `matricula`, `nome`, `unidade`, `email`). * * Retorna um array com a estrutura: * [ * 'resultado' => RESULTADO_*, * 'mensagem' => string|null, * 'cpf' => string|null, * 'campos' => array<string,array{antes:mixed,depois:mixed}>, * ] * * @param array<string,mixed> $linha * @param string|null $structureId Resolvido externamente via MapeamentoUnidadeResource. * @return array{resultado:string,mensagem:?string,cpf:?string,campos:array<string,array{antes:mixed,depois:mixed}>} */ public function processarLinha(array $linha, ?string $structureId, int $numeroLinha = 0): array { $cpf = $this->normalizarCpf($linha['cpf'] ?? null); $email = $this->normalizarEmail($linha['email'] ?? null); $matricula = $this->normalizarMatricula($linha['matricula'] ?? null); $nomes = $this->normalizarNome($linha['nome'] ?? null); $erroValidacao = $this->validar($cpf, $email, $matricula, $nomes, $structureId); if ($erroValidacao !== null) { $this->logInvalido($numeroLinha, $linha, $erroValidacao); return [ 'resultado' => self::RESULTADO_INVALIDO, 'mensagem' => $erroValidacao, 'cpf' => $cpf, 'campos' => [], ]; } /** @var User|null $usuarioExistente */ $usuarioExistente = User::query()->where('cpf', $cpf)->first(); if ($usuarioExistente === null) { return $this->criarUsuario($linha, $cpf, $email, $matricula, $nomes, $structureId, $numeroLinha); } return $this->atualizarUsuario($usuarioExistente, $email, $matricula, $structureId, $numeroLinha); } /** * @return array<int,array<string,mixed>> */ public function getConflitosEmailDetalhados(): array { return $this->conflitosEmailDetalhados; } /** * Cria um novo usuario com senha aleatoria + permissoes padrao. * * @param array<string,mixed> $linha * @param array{firstName:string,lastName:string} $nomes */ protected function criarUsuario(array $linha, string $cpf, string $email, ?string $matricula, array $nomes, string $structureId, int $numeroLinha): array { $usuarioComEmail = $this->buscarUsuarioPorEmail($email, null); if ($usuarioComEmail !== null) { $mensagem = __('Email ja cadastrado para outro CPF; registro pulado.'); $detalhe = $this->montarDetalheConflitoEmail($numeroLinha, $linha, $cpf, $email, $matricula, $nomes, $usuarioComEmail); $this->conflitosEmailDetalhados[] = $detalhe; $this->logInvalido($numeroLinha, array_merge($linha, ['conflito_email' => $detalhe]), $mensagem); Log::channel(self::LOG_CHANNEL)->warning('email_em_conflito_criacao', $detalhe); return [ 'resultado' => self::RESULTADO_INVALIDO, 'mensagem' => $mensagem, 'cpf' => $cpf, 'campos' => [], ]; } $user = new User(); $user->id = (string) Str::uuid(); $user->structure_id = $structureId; $user->firstName = $nomes['firstName']; $user->lastName = $nomes['lastName']; $user->cpf = $cpf; $user->matriculation = $matricula; $user->phoneNumber = ''; $user->email = $email; $user->password = Hash::make(Str::random(12)); $user->email_verified_at = null; $user->photo = 'default/assets/img/icons/usuario.png'; $user->status = 'ATIVADO'; $user->isPasswordDefault = 1; $user->isServidorPublico = 'Sim'; $user->save(); $permissoesAdicionadas = $this->concederPermissoesPadrao($user); Log::channel(self::LOG_CHANNEL)->info('usuario_criado', [ 'linha' => $numeroLinha, 'cpf' => $cpf, 'email' => $email, 'matricula' => $matricula, 'structure_id' => $structureId, 'permissoes_adicionadas' => $permissoesAdicionadas, ]); return [ 'resultado' => self::RESULTADO_CRIADO, 'mensagem' => null, 'cpf' => $cpf, 'campos' => [ 'firstName' => ['antes' => null, 'depois' => $nomes['firstName']], 'lastName' => ['antes' => null, 'depois' => $nomes['lastName']], 'matriculation' => ['antes' => null, 'depois' => $matricula], 'structure_id' => ['antes' => null, 'depois' => $structureId], 'email' => ['antes' => null, 'depois' => $email], ], ]; } /** * Aplica o dirty check e atualiza apenas os campos divergentes. Conflito de * email com outro CPF e tratado mantendo o email atual. Garante tambem que * as permissoes padrao (`GERENCIADOR_DE_CONTA_ANGRA` e `GUEST`) existam, * sem alterar as demais permissoes ja cadastradas. */ protected function atualizarUsuario(User $user, string $email, ?string $matricula, string $structureId, int $numeroLinha): array { $campos = []; $matriculaAtual = $this->normalizarMatricula($user->matriculation); if ($matriculaAtual !== $matricula) { $campos['matriculation'] = ['antes' => $matriculaAtual, 'depois' => $matricula]; } $structureAtual = (string) $user->structure_id; if ($structureAtual !== $structureId) { $campos['structure_id'] = ['antes' => $structureAtual, 'depois' => $structureId]; } $emailAtual = $this->normalizarEmail($user->getAttributes()['email'] ?? null); if ($emailAtual !== $email) { if ($this->emailEmUso($email, (string) $user->id)) { Log::channel(self::LOG_CHANNEL)->warning('email_em_conflito', [ 'linha' => $numeroLinha, 'cpf' => $user->cpf, 'user_id' => $user->id, 'email_atual' => $emailAtual, 'email_csv' => $email, ]); } else { $campos['email'] = ['antes' => $emailAtual, 'depois' => $email]; } } $permissoesAdicionadas = $this->concederPermissoesPadrao($user); if ($campos === [] && $permissoesAdicionadas === []) { Log::channel(self::LOG_CHANNEL)->debug('usuario_sincronizado', [ 'linha' => $numeroLinha, 'cpf' => $user->cpf, 'user_id' => $user->id, ]); return [ 'resultado' => self::RESULTADO_SINCRONIZADO, 'mensagem' => null, 'cpf' => (string) $user->cpf, 'campos' => [], ]; } if ($campos !== []) { foreach ($campos as $campo => $valores) { $user->{$campo} = $valores['depois']; } $user->save(); } Log::channel(self::LOG_CHANNEL)->info('usuario_atualizado', [ 'linha' => $numeroLinha, 'cpf' => $user->cpf, 'user_id' => $user->id, 'campos' => array_keys($campos), 'permissoes_adicionadas' => $permissoesAdicionadas, ]); return [ 'resultado' => self::RESULTADO_ATUALIZADO, 'mensagem' => null, 'cpf' => (string) $user->cpf, 'campos' => $campos, ]; } /** * Cria, se necessario, as permissoes padrao do Conta Angra (Conta Angra + * Visitante). Permissoes ja existentes nao sao alteradas, preservando o * comportamento atual do usuario. * * @return array<int,string> Lista das permissoes efetivamente criadas. */ public function concederPermissoesPadrao(User $user): array { $criadas = []; foreach (self::PERMISSOES_PADRAO as $permissao) { $existe = Permissao::query() ->where('user_id', $user->id) ->where('permissao', $permissao) ->exists(); if ($existe) { continue; } Permissao::create([ 'user_id' => $user->id, 'permissao' => $permissao, 'visualizar' => 1, 'adicionar' => 1, 'editar' => 1, 'excluir' => 1, 'status' => 'ATIVADO', ]); $criadas[] = $permissao; } return $criadas; } /** * Retorna true caso o email pertenca a outro usuario (CPF diferente). */ protected function emailEmUso(string $email, ?string $ignorarUserId): bool { return $this->buscarUsuarioPorEmail($email, $ignorarUserId) !== null; } protected function buscarUsuarioPorEmail(string $email, ?string $ignorarUserId): ?User { $query = User::query()->where('email', $email); if ($ignorarUserId !== null) { $query->where('id', '!=', $ignorarUserId); } return $query->first(); } /** * @param array<string,mixed> $linha * @param array{firstName:string,lastName:string} $nomes * @return array<string,mixed> */ protected function montarDetalheConflitoEmail( int $numeroLinha, array $linha, string $cpf, string $email, ?string $matricula, array $nomes, User $usuarioBase ): array { $cpfRaw = preg_replace('/\D+/', '', (string) ($linha['cpf'] ?? '')) ?? ''; return [ 'linha_csv' => $numeroLinha, 'registro_csv' => [ 'cpf_planilha' => $cpfRaw !== '' ? $cpfRaw : null, 'cpf_normalizado' => $cpf, 'nome' => trim((string) ($linha['nome'] ?? '')), 'email' => $email, 'matricula' => $matricula, 'unidade' => trim((string) ($linha['unidade'] ?? '')), ], 'usuario_ja_cadastrado' => $this->resumirUsuarioParaRelatorio($usuarioBase), ]; } /** * @return array<string,mixed> */ protected function resumirUsuarioParaRelatorio(User $user): array { $user->loadMissing('pertence:id,name,abbreviation'); return [ 'user_id' => (string) $user->id, 'cpf' => $this->normalizarCpf($user->cpf), 'nome' => trim($user->firstName . ' ' . $user->lastName), 'email' => (string) ($user->getAttributes()['email'] ?? $user->email), 'matricula' => $user->matriculation, 'structure_id'=> (string) $user->structure_id, 'secretaria' => $user->pertence ? trim($user->pertence->name . ($user->pertence->abbreviation ? ' [' . $user->pertence->abbreviation . ']' : '')) : null, 'status' => (string) $user->status, ]; } /** * Sanitiza o CPF: remove nao-digitos e completa com zeros a esquerda ate 11 * caracteres (planilhas que exportam CPF como numero costumam perder o zero inicial). */ public function normalizarCpf(mixed $cpf): string { $digitos = preg_replace('/\D+/', '', (string) $cpf) ?? ''; if ($digitos === '') { return ''; } if (strlen($digitos) < 11) { $digitos = str_pad($digitos, 11, '0', STR_PAD_LEFT); } if (strlen($digitos) > 11) { $digitos = substr($digitos, 0, 11); } return $digitos; } /** * Lowercased + trim. Retorna string vazia se nao houver email. */ public function normalizarEmail(mixed $email): string { return mb_strtolower(trim((string) $email)); } /** * Mantem apenas os digitos da matricula. Se vazio, retorna null. */ public function normalizarMatricula(mixed $matricula): ?string { $digitos = preg_replace('/\D+/', '', (string) $matricula) ?? ''; return $digitos === '' ? null : $digitos; } /** * Quebra o nome em `firstName` (primeira palavra) e `lastName` (restante), * preservando caixa/acentuacao para depois ser formatado por `User::name`. * * @return array{firstName:string,lastName:string}|null Null se invalido. */ public function normalizarNome(mixed $nome): ?array { $nome = trim(preg_replace('/\s+/u', ' ', (string) $nome) ?? ''); if ($nome === '') { return null; } $partes = explode(' ', $nome); $firstName = array_shift($partes) ?: ''; $lastName = implode(' ', $partes); if ($firstName === '') { return null; } return [ 'firstName' => $firstName, 'lastName' => $lastName, ]; } /** * Valida os campos minimos para considerar uma linha importavel. * * @param array{firstName:string,lastName:string}|null $nomes */ public function validar(string $cpf, string $email, ?string $matricula, ?array $nomes, ?string $structureId): ?string { if (!$this->cpfValido($cpf)) { return __('CPF invalido.'); } if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { return __('Email invalido.'); } if ($matricula !== null && strlen($matricula) > 30) { return __('Matricula com tamanho invalido.'); } if ($nomes === null || $nomes['firstName'] === '') { return __('Nome invalido.'); } if (!is_string($structureId) || $structureId === '') { return __('Unidade nao mapeada para uma Structure.'); } return null; } /** * Valida o CPF aplicando o calculo oficial de digitos verificadores. */ public function cpfValido(string $cpf): bool { if (strlen($cpf) !== 11) { return false; } if (preg_match('/^(\d)\1{10}$/', $cpf)) { return false; } for ($t = 9; $t < 11; $t++) { $soma = 0; for ($i = 0; $i < $t; $i++) { $soma += (int) $cpf[$i] * (($t + 1) - $i); } $resto = ($soma * 10) % 11; if ($resto === 10) { $resto = 0; } if ($resto !== (int) $cpf[$t]) { return false; } } return true; } /** * Registra no log o motivo de uma linha invalida. * * @param array<string,mixed> $contexto */ protected function logInvalido(int $numeroLinha, array $contexto, string $motivo): void { Log::channel(self::LOG_CHANNEL)->warning('registro_invalido', [ 'linha' => $numeroLinha, 'motivo' => $motivo, 'dados' => $contexto, ]); } /** * Monta o detalhe de conflito de e-mail para uma linha do CSV (consulta a base). * Retorna null se nao houver outro usuario com o mesmo e-mail. * * @param array<string,mixed> $linha * @return array<string,mixed>|null */ public function detalharConflitoEmailNaLinha(array $linha, int $numeroLinha): ?array { $cpf = $this->normalizarCpf($linha['cpf'] ?? null); $email = $this->normalizarEmail($linha['email'] ?? null); if ($email === '') { return null; } $usuarioComEmail = $this->buscarUsuarioPorEmail($email, null); if ($usuarioComEmail === null) { return null; } if ($cpf !== '' && (string) $usuarioComEmail->cpf === $cpf) { return null; } $nomes = $this->normalizarNome($linha['nome'] ?? null); if ($nomes === null) { $nomes = ['firstName' => '', 'lastName' => '']; } return $this->montarDetalheConflitoEmail( $numeroLinha, $linha, $cpf, $email, $this->normalizarMatricula($linha['matricula'] ?? null), $nomes, $usuarioComEmail ); } /** * Gera relatorio Markdown com todos os conflitos de e-mail coletados. * * @param array<int,array<string,mixed>> $conflitos */ public static function renderizarRelatorioConflitosEmailMarkdown(array $conflitos): string { $linhas = []; $linhas[] = '# Relatorio de conflitos de e-mail — Importacao Prodata Conta Angra'; $linhas[] = ''; $linhas[] = '- Gerado em: ' . now()->toDateTimeString(); $linhas[] = '- Total de conflitos: ' . count($conflitos); $linhas[] = ''; $linhas[] = 'Cada item abaixo representa um servidor da planilha que **nao foi criado** porque o e-mail informado ja pertence a outro CPF na base.'; $linhas[] = ''; if ($conflitos === []) { $linhas[] = '_Nenhum conflito de e-mail registrado._'; return implode(PHP_EOL, $linhas) . PHP_EOL; } $linhas[] = '## Indice por e-mail'; $linhas[] = ''; $linhas[] = '| E-mail | Linha CSV | CPF planilha | CPF na base |'; $linhas[] = '| --- | ---: | --- | --- |'; foreach ($conflitos as $item) { $csv = $item['registro_csv'] ?? []; $base = $item['usuario_ja_cadastrado'] ?? []; $linhas[] = sprintf( '| %s | %d | %s | %s |', (string) ($csv['email'] ?? ''), (int) ($item['linha_csv'] ?? 0), (string) ($csv['cpf_normalizado'] ?? ''), (string) ($base['cpf'] ?? '') ); } $linhas[] = ''; $linhas[] = '## Detalhamento'; $linhas[] = ''; foreach ($conflitos as $indice => $item) { $csv = $item['registro_csv'] ?? []; $base = $item['usuario_ja_cadastrado'] ?? []; $numero = $indice + 1; $linhas[] = '### ' . $numero . '. Linha ' . (int) ($item['linha_csv'] ?? 0) . ' — `' . (string) ($csv['email'] ?? '') . '`'; $linhas[] = ''; $linhas[] = '#### Registro na planilha (nao importado)'; $linhas[] = ''; $linhas[] = '| Campo | Valor |'; $linhas[] = '| --- | --- |'; $linhas[] = '| CPF (como na planilha) | ' . self::celulaMarkdown($csv['cpf_planilha'] ?? null) . ' |'; $linhas[] = '| CPF (normalizado) | ' . self::celulaMarkdown($csv['cpf_normalizado'] ?? null) . ' |'; $linhas[] = '| Nome | ' . self::celulaMarkdown($csv['nome'] ?? null) . ' |'; $linhas[] = '| E-mail | ' . self::celulaMarkdown($csv['email'] ?? null) . ' |'; $linhas[] = '| Matricula | ' . self::celulaMarkdown($csv['matricula'] ?? null) . ' |'; $linhas[] = '| Unidade (CSV) | ' . self::celulaMarkdown($csv['unidade'] ?? null) . ' |'; $linhas[] = ''; $linhas[] = '#### Usuario ja cadastrado na base (detentor do e-mail)'; $linhas[] = ''; $linhas[] = '| Campo | Valor |'; $linhas[] = '| --- | --- |'; $linhas[] = '| ID (users) | ' . self::celulaMarkdown($base['user_id'] ?? null) . ' |'; $linhas[] = '| CPF | ' . self::celulaMarkdown($base['cpf'] ?? null) . ' |'; $linhas[] = '| Nome | ' . self::celulaMarkdown($base['nome'] ?? null) . ' |'; $linhas[] = '| E-mail | ' . self::celulaMarkdown($base['email'] ?? null) . ' |'; $linhas[] = '| Matricula | ' . self::celulaMarkdown($base['matricula'] ?? null) . ' |'; $linhas[] = '| structure_id | ' . self::celulaMarkdown($base['structure_id'] ?? null) . ' |'; $linhas[] = '| Secretaria / unidade | ' . self::celulaMarkdown($base['secretaria'] ?? null) . ' |'; $linhas[] = '| Status | ' . self::celulaMarkdown($base['status'] ?? null) . ' |'; $linhas[] = ''; } return implode(PHP_EOL, $linhas) . PHP_EOL; } protected static function celulaMarkdown(mixed $valor): string { if ($valor === null || $valor === '') { return '—'; } return str_replace('|', '\\|', (string) $valor); } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings