<?php namespace CulturaViva; require __DIR__ . "/ImportRegistrationsJob.php"; use CulturaViva\JobTypes\ImportRegistrationsJob; use MapasCulturais\API; use MapasCulturais\ApiQuery; use MapasCulturais\App; use MapasCulturais\Entities\Agent; use MapasCulturais\Entities\Registration; use MapasCulturais\Entities\RegistrationFile; use MapasCulturais\Entities\RegistrationFileConfiguration; use MapasCulturais\Entities\User; use MapasCulturais\Utils; use Monolog\Handler\Curl\Util; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Respect\Validation\Validator as v; /** * São 4 cenários mapeados: * * Cenário 1 - O sistema consegue encontrar no **cadastro** (a inscrição do Ponto certificado). (O usuário preencheu * todo o formulário, marcou que estava concorrendo a um edital municipal ou estadual e enviou ou * não finalizou o cadastro e envio). Neste caso, a inscrição é marcada como Selecionada, o que por sua vez * dispara o fluxo de certificação. Ou seja, o ponto ganha o selo. E o usuário recebe uma notificação por email, * informando a finalização do processo de certificação e convidando para atualizar informações. * * Cenário 2 - O sistema não encontrou no **cadastro** a inscrição do Ponto. Neste caso, o sistema procura pelo CNPJ para * verificar que já é um ponto, no caso de Ponto Entidade ou Pontão. No caso de Ponto Coletivo, o sistema * procura por CPF e nome da organização para encontrar compatibilidades. Encontrando CNPJ, CPF e Nome * o sistema cria uma inscrição no cadastro, com dados incompletos, e seleciona a inscrição. Na sequência, * envia um email notificando para a atualização de dados na plataforma. * * Cenário 3 - O sistema não encontrou no **cadastro** a inscrição do Ponto, nem CNPJ, nem CPF, nem Nome da organização. * Neste caso, o sistema cria um usuário, um agente coletivo e uma inscrição selecionada e notifica * por email para atualização dos dados. * * Cenário 4 - O sistema encontra o CNPJ no **cadastro** da Inscrição do Ponto ou na lista de nome de organizações * já certificadas, mas está com CPF de outra pessoa. O sistema envia uma notificação para o CPF que entrou * pela importação, informando que o Ponto estão cadastrado em outro CPF, para regularização via atualização * cadastral. O ponto/pontão só será selecionado com aplicação de selo após a regularização. * */ class Importer { static $types = [ 'pontao' => [ 'Pontão', 'Pontão com CNPJ', 'Pontão entidade', 'Pontão entidade com CNPJ', 'Pontão de Cultura (com CNPJ)', 'Pontão de Cultura (entidade com CNPJ)', 'Pontão de Cultura (entidade)', ], 'ponto-entidade' => [ 'Ponto com CNPJ', 'Ponto entidade', 'Ponto entidade com CNPJ', 'Ponto de Cultura (com CNPJ)', 'Ponto de Cultura (entidade com CNPJ)', 'Ponto de Cultura (entidade)', ], 'ponto-coletivo' => [ 'Ponto sem CNPJ', 'Ponto Coletivo', 'Ponto Coletivo sem CNPJ', 'Ponto de Cultura (sem CNPJ)', 'Ponto de Cultura (coletivo)', 'Ponto de Cultura (coletivo sem CNPJ)', ], ]; static $column_mapping = [ 'ponto_data' => 'Data da Certificação', 'ponto_tipo' => 'Tipo de ponto', 'ponto_uf' => 'Estado', 'ponto_municipio' => 'Município', 'organizacao_nome' => 'Nome da organização', 'organizacao_cnpj' => 'CNPJ', 'organizacao_email' => 'Email da organização', 'organizacao_telefone' => 'Telefone da organização', 'responsavel_nome' => 'Nome do responsável', 'responsavel_cpf' => 'CPF', 'responsavel_email' => 'Email do responsável', 'responsavel_telefone' => 'Telefone do responsável' ]; private static $theme = null; public static function init($theme) { $app = App::i(); self::$theme = $theme; $app->registerJobType(new ImportRegistrationsJob(ImportRegistrationsJob::SLUG)); // Ao aprovar a inscrição agenda o job de importação das inscrições a partir da planilha enviada na inscrição $app->hook("entity(Registration).status(approved)", function() use ($app) { /** @var \MapasCulturais\Entities\Registration $this */ if ($this->opportunity->id !== (int) $app->config['rcv.pnabOpportunityId']) { return; } if ($file = self::getRegistrationFile($this)) { $params = [ 'registration' => $this ]; $app->enqueueJob("importRegistrations", $params); $app->log->info("Job criado para importar a planilha da inscrição {$this->id}"); } }); } /** * Retorna a planilha de importação de pontos da inscrição * @param Registration $registration * @return Worksheet */ static function getSheet(RegistrationFile $registration_file): Worksheet { $spreadsheet = IOFactory::load($registration_file->path); $sheet = $spreadsheet->getActiveSheet(); return $sheet; } /** * Retorna o arquivo da planilha de importação de pontos da inscrição * @param Registration $registration * @return RegistrationFile */ static function getRegistrationFile(Registration $registration): ?RegistrationFile { $app = App::i(); $field_name = 'rfc_'.$app->config['rcv.pnabOpportunityAttachmentId']; $file = $app->repo('RegistrationFile')->findOneBy([ 'group' => $field_name, 'owner' => $registration ]); return $file; } /** * Parseia a linha da planilha de importação de pontos * @param array $header * @param array $row * @return object */ static function parseRow(array $header, array $row): object { $app = App::i(); $column_mapping = (object) self::$column_mapping; // mapeia os campos da planilha $new_column_mapping = (object) []; foreach ($header as $column => $value) { if (empty($value)) { continue; } $value = $app->slugify($value); foreach($column_mapping as $key => $val) { if(str_starts_with($value, $app->slugify($val))) { $new_column_mapping->$key = $column; } } } $column_mapping = $new_column_mapping; // parseia os dados da linha $data = (object) []; foreach($column_mapping as $key => $column) { if ($key == 'ponto_tipo') { if (!$row[$column_mapping->ponto_tipo]) { $data->$key = ''; } else { $data->$key = self::parseCategory($row[$column_mapping->ponto_tipo]); } continue; } else { $data->$key = $row[$column] ?? null; } } return $data; } /** * Encontra a categoria do ponto * @param string $type * @return string */ static function parseCategory(string $type): string { $app = App::i(); $type = $app->slugify($type); foreach (self::$types as $category => $types) { foreach ($types as $t) { if ($app->slugify($t) == $type) { return $app->config['rcv.categoriesMap'][$category]; } } } return ''; } /** * Encontra a inscrição do ponto na plataforma * @param object $row * @return Registration|null */ static function findRegistrationByRow(object $row): ?Registration { $app = App::i(); $opportunity_id = $app->config['rcv.opportunityId']; if ($row->ponto_tipo == $app->config['rcv.categoriesMap']['ponto-coletivo']) { // para ponto coletivo, busca por responsavel_cpf e itera sobre os resultados buscando pelo nome do ponto $query = new ApiQuery(Registration::class, [ 'opportunity' => API::EQ($opportunity_id), 'category' => API::EQ($row->ponto_tipo), 'status' => API::GTE(0), '@keyword' => "$row->responsavel_cpf" ]); $ids = $query->findIds(); /** @var Registration[] */ $registrations = $app->repo('Registration')->findBy(['id' => $ids]); foreach($registrations as $registration) { $coletivo = $registration->getRelatedAgents('coletivo'); if (!empty($coletivo) && !empty($coletivo[0]->name) && !empty($row->organizacao_nome)) { if ($app->slugify($coletivo[0]->name) == $app->slugify($row->organizacao_nome)) { return $registration; } } } } else { // para ponto entidade e pontão, busca por cnpj e itera sobre os resultados verificando o cpf do responsável $query = new ApiQuery(Registration::class, [ 'opportunity' => API::EQ($opportunity_id), 'category' => API::EQ($row->ponto_tipo), 'status' => API::GTE(0), '@keyword' => "$row->organizacao_cnpj" ]); $ids = $query->findIds(); /** @var Registration[] */ $registrations = $app->repo('Registration')->findBy(['id' => $ids]); foreach($registrations as $registration) { $coletivo = $registration->getRelatedAgents('coletivo'); if ($app->slugify($coletivo[0]->name) == $app->slugify($row->organizacao_nome) && Utils::formatCnpjCpf($registration->owner->cpf) == Utils::formatCnpjCpf($row->responsavel_cpf)) { return $registration; } } } return null; } /** * Encontra a organização do ponto na plataforma * @param object $row * @return Agent|null */ static function findOrganizationByRow(object $row): ?Agent { $app = App::i(); $cpf_query = new ApiQuery(Agent::class, [ 'cpf' => API::EQ($row->responsavel_cpf), 'type' => API::EQ(1) ]); $owner_ids = $cpf_query->findIds(); if($row->ponto_tipo == $app->config['rcv.categoriesMap']['ponto-coletivo']) { $query = new ApiQuery(Agent::class, [ 'name' => API::ILIKE($row->organizacao_nome), 'parent' => API::IN($owner_ids), 'type' => API::EQ(2) ]); } else { $query = new ApiQuery(Agent::class, [ 'cnpj' => API::EQ($row->organizacao_cnpj), 'parent' => API::IN($owner_ids), 'type' => API::EQ(2) ]); } $ids = $query->findIds(); /** @var Agent */ $agent = $app->repo('Agent')->findOneBy(['id' => $ids], ['updateTimestamp' => 'DESC']); return $agent; } /** * Encontra uma organização a partir de outro agente. * * @param object $row * @return Agent|null */ static function findOrganizationFromOtherAgent(object $row): ?Agent { $app = App::i(); if ($row->ponto_tipo == $app->config['rcv.categoriesMap']['ponto-coletivo']) { $query = new ApiQuery(Agent::class, [ 'name' => API::ILIKE($row->organizacao_nome), 'type' => API::EQ(2) ]); } else { $query = new ApiQuery(Agent::class, [ 'cnpj' => API::EQ($row->organizacao_cnpj), 'type' => API::EQ(2) ]); } $ids = $query->findIds(); /** @var Agent */ $organization = $app->repo('Agent')->findOneBy(['id' => $ids], ['updateTimestamp' => 'DESC']); if ( !$organization ) { return null; } if (isset($organization->owner) && $organization->owner->type->id != 1) { return null; } if (!v::cpf()->validate($organization->owner->cpf)) { return null; } if (Utils::formatCnpjCpf($organization->owner->cpf) != Utils::formatCnpjCpf($row->responsavel_cpf)) { return $organization; } return null; } /** * Encontra o responsável do ponto na plataforma * @param object $row * @return Agent|null */ static function findOrganizationOwnerByRow(object $row): ?Agent { $app = App::i(); $query = new ApiQuery(Agent::class, [ 'cpf' => API::EQ($row->responsavel_cpf), 'type' => API::EQ(1) ]); $ids = $query->findIds(); /** @var Agent */ $agent = $app->repo('Agent')->findOneBy(['id' => $ids], ['updateTimestamp' => 'DESC']); return $agent; } /** * Cria a inscrição do ponto na plataforma * @param object $row * @param Agent $organization * @return Registration */ static function createRegistration(object $row, Agent $organization): Registration { $app = App::i(); $opportunity_id = App::i()->config['rcv.opportunityId']; $opportunity = App::i()->repo('Opportunity')->find($opportunity_id); $registration = new Registration(); $registration->opportunity = $opportunity; $registration->owner = $organization->parent; $registration->category = $row->ponto_tipo; $registration->status = Registration::STATUS_DRAFT; if ($row->ponto_tipo == $app->config['rcv.categoriesMap']['ponto-coletivo']) { $registration->proponentType = 'Coletivo'; } else { $registration->proponentType = 'Pessoa Jurídica'; } $registration->save(true); $registration->createAgentRelation($organization, 'coletivo'); return $registration; } /** * Cria a organização do ponto na plataforma * @param object $row * @param Agent $owner * @return Agent */ static function createOrganization(object $row, Agent $owner): Agent { $app = App::i(); $organization = new Agent(); $organization->type = 2; $organization->parent = $owner; $organization->name = $row->organizacao_nome; $organization->cnpj = $row->organizacao_cnpj; $organization->telefonePublico = $row->organizacao_telefone; $organization->En_Estado = $row->ponto_uf; $organization->En_Municipio = $row->ponto_municipio; $organization->rcv_tipo = "ponto"; $organization->save(true); return $organization; } /** * Cria o responsável do ponto na plataforma * @param object $row * @return Agent */ static function createOrganizationOwner(object $row): Agent { // cria o usuário $user = new User(); $user->email = $row->responsavel_email; $user->authProvider = "0"; $user->authUid = $row->responsavel_email; $user->save(true); $owner = new Agent(); $owner->user = $user; $owner->type = 1; // @todo: verificar salvamento correto do type $owner->name = $row->responsavel_nome; $owner->cpf = $row->responsavel_cpf; $owner->emailPrivado = $row->responsavel_email; $owner->telefonePrivado = $row->responsavel_telefone; $owner->save(true); $user->profile = $owner; $user->save(true); return $owner; } /** * Processa uma linha da planilha e realiza a criação/atualização da inscrição, organização e proprietário. * * @param object $row * @return array{ * registration: Registration, * organization: Organization|null, * owner: Owner|null, * scenario: int|float * } */ public static function processRow(object $row) { $app = App::i(); $result = [ 'registration_id' => null, 'organization' => null, 'owner' => null, 'scenario' => null ]; $scenario = null; if ($registration = self::findRegistrationByRow($row)) { $app->log->debug("Cenário 1 - encontrou e aprova a inscrição - {$registration->number}"); $scenario = 1; } else if ($organization = self::findOrganizationByRow($row)) { $app->log->debug("Cenário 2 - encontrou a organização mas não a inscrição, cria e aprova a inscrição"); $registration = self::createRegistration($row, $organization); $result['organization'] = $organization; $scenario = 2; } else if ($owner = self::findOrganizationOwnerByRow($row)) { $app->log->debug("Cenário 3.1 - encontrou somente o proprietário da organização, cria a organização e a inscrição"); $organization = self::createOrganization($row, $owner); $registration = self::createRegistration($row, $organization); $result['organization'] = $organization; $result['owner'] = $owner; $scenario = 3.1; } else if ($organization = self::findOrganizationFromOtherAgent($row)) { $app->log->debug("Cenário 5 - encontrou a organização e compara o CPF do parent com o CPF da planilha"); $result['organization'] = $organization; $registration = $organization->rcv_registration; $scenario = 5; } else { $app->log->debug("Cenário 3.2 - não encontrou nada, cria o proprietário, a organização e a inscrição"); $owner = self::createOrganizationOwner($row); $organization = self::createOrganization($row, $owner); $registration = self::createRegistration($row, $organization); $result['organization'] = $organization; $result['owner'] = $owner; $scenario = 3.2; } $registration->setStatusToApproved(true); $result['registration_id'] = $registration->id; $result['scenario'] = $scenario; return $result; } public static function validateRow(object $row, $line): array { $errors = []; foreach (self::$column_mapping as $key => $label) { $field_value = trim($row->{$key} ?? ''); $cnpj_required = false; if ($key == 'ponto_tipo') { if (empty($field_value)) { $errors[] = "Linha: {$line} - Campo '{$label}' obrigatório."; continue; } $parsed_category = self::parseCategory($field_value); if (empty($parsed_category)) { $errors[] = "Linha: {$line} - Campo '{$label}' inválido."; continue; } $category = ''; foreach (self::$types as $key => $types) { if (in_array($parsed_category, $types)) { $category = $key; break; } } if ($category !== 'ponto-coletivo') { // CNPJ $cnpj_required = true; if (empty($row->organizacao_cnpj)) { $errors[] = "Linha: {$line} - Campo 'CNPJ' obrigatório para a categoria {$category}."; } elseif (!v::cnpj()->validate($row->organizacao_cnpj)) { $errors[] = "Linha: {$line} - CNPJ ({$row->organizacao_cnpj}) inválido."; } // elseif ($check_cnpj = Importer::checkCNPJ($row->organizacao_cnpj)) { // $message_error = "CNPJ ({$row->organizacao_cnpj}) inválido. {$check_cnpj['message']}"; // $errors[] = "Linha: {$line} - {$message_error}"; // } } } elseif ($key == 'responsavel_cpf') { if (empty($row->responsavel_cpf)) { $errors[] = "Linha: {$line} - Campo 'CPF' obrigatório."; } elseif (!v::cpf()->validate($row->responsavel_cpf)) { $errors[] = "Linha: {$line} - Campo 'CPF' inválido."; } } elseif (in_array($key, ['organizacao_cnpj', 'ponto_tipo'])) { continue; // Pula os campos validados anteriormente } elseif ($key == 'responsavel_email') { if (empty($row->responsavel_email)) { $errors[] = "Linha: {$line} - Campo 'Email do responsável' obrigatório."; } elseif (!v::email()->validate($field_value)) { $errors[] = "Linha: {$line} - Campo 'Email do responsável' inválido."; } } elseif ($key == 'organizacao_email') { if ($cnpj_required && empty($row->organizacao_email)) { $errors[] = "Linha: {$line} - Campo 'CNPJ' obrigatório."; } elseif(!empty(!v::email()->validate($field_value))) { $errors[] = "Linha: {$line} - Campo 'Email da organização' inválido."; } } else { // Valida se os demais campos estão preenchidos if (empty($field_value)) { $errors[] = "Linha: {$line} - Campo '{$label}' obrigatório."; } } } return array_filter($errors); } public static function runImportRegistrationsJob(Registration $registration) { $app = App::i(); $app->log->info("Iniciando importação da planilha da inscrição {$registration->id}"); if (!$registration) { $app->log->error("Inscrição {$registration->id} não encontrada."); throw new \Exception("Inscrição {$registration->id} não encontrada."); } $file = Importer::getRegistrationFile($registration); if (!$file) { $app->log->error("Nenhum arquivo encontrado para a inscrição {$registration->id}."); throw new \Exception("Nenhum arquivo encontrado para a inscrição {$registration->id}."); } $app->disableAccessControl(); $sheet = Importer::getSheet($file); $header = $sheet->rangeToArray("A1:" . $sheet->getHighestColumn() . "1", null, true, true, true)[1]; $data_range = $sheet->rangeToArray("A2:" . $sheet->getHighestColumn() . $sheet->getHighestRow(), null, true, true, true); $result = [ 'registration' => $registration, 'total_rows' => 0, 'success_rows' => 0 ]; foreach ($data_range as $index => $row) { if (!array_filter($row)) { continue; } $app->log->debug("========================================================="); $result['total_rows']++; try { $parsed_row = Importer::parseRow($header, $row); $process_row = Importer::processRow($parsed_row); $result['data_rows'][] = $process_row; $result['success_rows']++; $app->log->info("Linha {$index} importada com sucesso."); } catch (\Exception $e) { $app->log->error("Erro ao importar a linha {$index}: " . $e->getMessage()); $app->enableAccessControl(); throw $e; } } $app->enableAccessControl(); $app->log->info("Importação concluída para a inscrição {$registration->id}."); return $result; } /** * Envia e-mails de notificação para os responsáveis pelas inscrições importadas. * * @param array $data * @return void */ public static function sendEmails(array $data) { $app = App::i(); // Envia e-mail para cada caso/linha da planilha foreach ($data['data_rows'] as $row) { if ($row['scenario'] == 1) { $registration = $app->repo('registration')->findOneBy(['id' => $row['registration_id']]); $organization = $app->repo('Agent')->findOneBy(['parent' => $registration->owner->id]); $template_data = [ 'siteName' => $app->siteName, 'userName' => $registration->owner->name, 'registrationNumber' => $registration->number, 'redirectUrl' => $app->createUrl('registration', 'single', [$registration->id]), 'organizationName' => $organization->name, 'organizationCNPJ' => $organization->cnpj, 'type' => $registration->category ]; $message = $app->renderMustacheTemplate('primeiro_caso.html', $template_data); $subject = "Sua inscrição {$registration->number} foi certificada por um Edital de Seleção da Cultura Viva."; } else if ($row['scenario'] == 2) { $registration = $app->repo('registration')->findOneBy(['id' => $row['registration_id']]); $template_data = [ 'siteName' => $app->siteName, 'userName' => $registration->owner->name, 'redirectUrl' => $app->createUrl('registration', 'single', [$registration->id]), 'organizationName' => $row['organization']->name, 'organizationCNPJ' => $row['organization']->cnpj, 'type' => $registration->category ]; $message = $app->renderMustacheTemplate('segundo_caso.html', $template_data); $subject = "Sua organização {$row['organization']->name} foi certificada por um Edital de Seleção da Cultura Viva."; } else if ($row['scenario'] == 3.1) { $registration = $app->repo('registration')->findOneBy(['id' => $row['registration_id']]); $template_data = [ 'siteName' => $app->siteName, 'userName' => $registration->owner->name, 'redirectUrl' => $app->createUrl('registration', 'single', [$registration->id]), 'organizationName' => $row['organization']->name, 'organizationCNPJ' => $row['organization']->cnpj, 'type' => $registration->category ]; $message = $app->renderMustacheTemplate('terceiro_caso.html', $template_data); $subject = "Sua organização {$row['organization']->name} foi certificada por um Edital de Seleção da Cultura Viva."; } else if ($row['scenario'] == 3.2) { $registration = $app->repo('registration')->findOneBy(['id' => $row['registration_id']]); $template_data = [ 'siteName' => $app->siteName, 'userName' => $registration->owner->name, 'redirectUrl' => $app->createUrl('registration', 'single', [$registration->id]), 'organizationName' => $row['organization']->name, 'organizationCNPJ' => $row['organization']->cnpj, 'type' => $registration->category ]; $message = $app->renderMustacheTemplate('quarto_caso.html', $template_data); $subject = "Sua organização {$row['organization']->name} foi certificada por um Edital de Seleção da Cultura Viva."; } else if ($row['scenario'] == 5) { $organization = $row['organization']; $registration = $organization->rcv_registration ?? null; if (!$registration) { continue; } $template_data = [ 'siteName' => $app->siteName, 'userName' => $organization->parent->name, 'redirectUrl' => $app->createUrl('site/atualizacao-cadastral'), 'organizationName' => $organization->name, 'organizationCNPJ' => $organization->cnpj, 'type' => $registration->category ]; $message = $app->renderMustacheTemplate('quinto_caso.html', $template_data); $subject = "Sua organização {$organization->name} foi certificada por um Edital de Seleção da Cultura Viva."; } else { continue; } $to = $registration->owner->emailPrivado; $app->createAndSendMailMessage([ 'to' => $to, 'subject' => $subject, 'body' => $message, ]); } $registration = $data['registration']; // Envia e-mail para o gestor com resumo da importação $template_data = [ 'siteName' => $app->siteName, 'userName' => $registration->owner->name, 'registrationNumber' => $registration->number, 'totalRows' => $data['total_rows'], 'successRows' => $data['success_rows'] ]; $to = $registration->owner->emailPrivado; $subject = "Inscrições importadas com sucesso"; $message = $app->renderMustacheTemplate('import_summary.html', $template_data); $app->createAndSendMailMessage([ 'to' => $to, 'subject' => $subject, 'body' => $message, ]); } /** * Envia um e-mail de erro para o responsável pela inscrição. */ public static function sendEmailError(Registration $registration, $message) { $app = App::i(); $template_data = [ 'siteName' => $app->siteName, 'userName' => $registration->owner->name, 'registrationNumber' => $registration->number, 'redirectUrl' => $app->createUrl('registration', 'single', [$registration->id]) ]; $to = $registration->owner->emailPrivado; $subject = "A importação da planilha falhou"; $message = $app->renderMustacheTemplate('import_error.html', $template_data); $app->createAndSendMailMessage([ 'to' => $to, 'subject' => $subject, 'body' => $message ]); } public static function checkCNPJ($cnpj) { $valid_cnpj = self::$theme->getCNPJ($cnpj, source:'importer'); $result = [ 'error' => false, 'message' => '' ]; if ($valid_cnpj) { if ($valid_cnpj['situacaoCadastral']['codigo'] !== '2') { $result['error'] = true; $result['message'] = 'Situação cadastral do CNPJ é inválida.'; } $legal_nature = $valid_cnpj['naturezaJuridica'] ?? null; if ($legal_nature) { $legal_nature_code = $legal_nature['codigo']; $allowed_legal_natures = ['1', '3', '2143', '3999', '3069', '3131', '3239', '3301', '3220']; } // Se a natureza jurídica não for válida if (!in_array($legal_nature_code, $allowed_legal_natures)) { $result['error'] = true; $result['message'] = 'A natureza jurídica do CNPJ é inválida.'; } } return $result; } }