rows ?? collect(); $created = 0; $updated = 0; /* * Guarda o último bloco encontrado. * * Exemplo: * * BLOCO: * "EXAMES DE IMAGEM" * * Os próximos exames receberão essa informação * na descrição até que outro bloco seja encontrado. */ $currentBlockDescription = null; /* * Localiza a categoria EXAME. */ $examCategory = Category::whereRaw( 'LOWER(TRIM(name)) = ?', ['exame'] )->first(); /* * A categoria é obrigatória para a importação. */ if (!$examCategory) { throw new \RuntimeException( 'A categoria "exame" não foi encontrada.' ); } foreach ($rows as $row) { /* * Normaliza as células da linha. */ $cells = collect($row) ->map(function ($value) { return trim((string) $value); }) ->values(); /* * Ignora linha completamente vazia. */ if ( $cells ->filter(fn ($value) => $value !== '') ->isEmpty() ) { continue; } /* * Estrutura esperada: * * Coluna 0 = Código do exame * Coluna 1 = Nome do exame * Coluna 2 = Valor / informação adicional */ $codeExams = $cells->get(0, ''); $name = $cells->get(1, ''); $price = $cells->get(2, ''); /* * Ignora linhas sem nome. */ if ($name === '') { continue; } /* * Normalização para identificação de cabeçalhos. */ $codeHeader = mb_strtoupper($codeExams); $nameHeader = mb_strtoupper($name); $priceHeader = mb_strtoupper($price); /* * Identifica cabeçalhos. */ $isHeader = str_contains($codeHeader, 'CÓDIGO') || str_contains($codeHeader, 'CODIGO') || $nameHeader === 'EXAME' || $nameHeader === 'NOME' || ( str_contains($nameHeader, 'EXAME') && ( str_contains($priceHeader, 'VALOR') || str_contains($priceHeader, 'PREÇO') || str_contains($priceHeader, 'PRECO') ) ); if ($isHeader) { continue; } /* * Processa a terceira coluna. * * Retorna: * * [ * 'price' => valor numérico, * 'description' => texto adicional * ] */ $priceData = $this->parsePrice($price); $associatePrice = $priceData['price']; $priceDescription = $priceData['description']; /* * Define se realmente existe um preço. * * Importante: * "0" também é considerado um valor válido. */ $isPrice = $associatePrice !== null; /* * REGRA DOS BLOCOS * * Se: * * - não existe código * - e a terceira coluna NÃO possui valor * * então a linha é um bloco. * * Exemplo: * * Código: vazio * Nome: "EXAMES LABORATORIAIS" * Valor: vazio * * Essa linha NÃO será cadastrada. * * Ela será utilizada como descrição dos exames * seguintes até que outro bloco seja encontrado. */ if ($codeExams === '' && !$isPrice) { $currentBlockDescription = $name; continue; } /* * Monta a descrição final do exame. * * Primeiro adiciona o bloco. * * Depois adiciona qualquer texto encontrado junto * ao valor. * * Exemplo: * * Bloco: * "PACOTE CONTRASTE" * * Terceira coluna: * "150,00 + contraste" * * Resultado: * * descrição: * "PACOTE CONTRASTE + contraste" * * preço: * 150.00 */ $descriptionParts = []; if ( $currentBlockDescription !== null && trim($currentBlockDescription) !== '' ) { $descriptionParts[] = trim($currentBlockDescription); } if ( $priceDescription !== null && trim($priceDescription) !== '' ) { $descriptionParts[] = trim($priceDescription); } $description = !empty($descriptionParts) ? implode(' ', $descriptionParts) : null; /* * Procura exame existente. * * Primeiro pelo código. */ $service = null; if ($codeExams !== '') { $service = PartnerAgreementService::withTrashed() ->where( 'partner_agreement_id', $partnerAgreementId ) ->where( 'code_exams', $codeExams ) ->first(); } /* * Caso não tenha encontrado pelo código, * procura pelo nome. */ if (!$service) { $service = PartnerAgreementService::withTrashed() ->where( 'partner_agreement_id', $partnerAgreementId ) ->whereRaw( 'LOWER(TRIM(name)) = LOWER(TRIM(?))', [$name] ) ->first(); } /* * ========================================================== * EXAME JÁ EXISTE * ========================================================== */ if ($service) { /* * Se estava excluído logicamente, restaura. */ if ($service->trashed()) { $service->restore(); } $changed = false; /* * Atualiza código. */ if ( $codeExams !== '' && $service->code_exams !== $codeExams ) { $service->code_exams = $codeExams; $changed = true; } /* * Atualiza preço somente quando * existe valor válido na planilha. */ if ( $associatePrice !== null && ( (float) $service->associate_price !== (float) $associatePrice ) ) { $service->associate_price = $associatePrice; $changed = true; } /* * Garante categoria EXAME. */ if ( (int) $service->category_id !== (int) $examCategory->id ) { $service->category_id = $examCategory->id; $changed = true; } /* * Garante tipo EXAME. */ if ( $service->type !== PartnerAgreementServiceTypeEnum::EXAME ) { $service->type = PartnerAgreementServiceTypeEnum::EXAME; $changed = true; } /* * Garante status ACTIVE. */ if ( $service->status !== PartnerAgreementServiceStatusEnum::ACTIVE ) { $service->status = PartnerAgreementServiceStatusEnum::ACTIVE; $changed = true; } /* * Atualiza descrição. * * Aqui usamos a descrição já processada, * incluindo: * * - bloco * - texto adicional do preço */ if ($service->description !== $description) { $service->description = $description; $changed = true; } /* * Salva somente se houve alteração. */ if ($changed) { $service->save(); $updated++; } continue; } /* * ========================================================== * EXAME NÃO EXISTE * ========================================================== * * Cria um novo exame. * * Mesmo sem código, se existir preço na terceira coluna, * a linha será cadastrada. * * Isso resolve o caso do: * * PACOTE CONTRASTE * * quando ele possui valor. */ PartnerAgreementService::create([ 'partner_agreement_id' => $partnerAgreementId, 'code_exams' => $codeExams !== '' ? $codeExams : null, 'name' => $name, 'description' => $description, 'category_id' => $examCategory->id, 'associate_price' => $associatePrice, 'type' => PartnerAgreementServiceTypeEnum::EXAME, 'status' => PartnerAgreementServiceStatusEnum::ACTIVE, ]); $created++; } return [ 'total' => $rows->count(), 'created' => $created, 'updated' => $updated, ]; } /** * Processa a terceira coluna. * * Aceita exemplos como: * * 50 * 50,00 * 50.00 * 1.250,50 * R$ 50,00 * 50,00 + contraste * R$ 50,00 + contraste * * Retorna: * * [ * 'price' => ?float, * 'description' => ?string * ] */ private function parsePrice(mixed $value): array { /* * Campo vazio. */ if ($value === null || trim((string) $value) === '') { return [ 'price' => null, 'description' => null, ]; } $value = trim((string) $value); /* * Remove R$. */ $value = str_replace( ['R$', 'r$'], '', $value ); /* * Normaliza espaços. */ $value = trim($value); /* * Procura o primeiro valor numérico. * * Exemplos encontrados: * * 50 * 50,00 * 50.00 * 1.250,50 * * O modificador u permite trabalhar corretamente * com caracteres UTF-8. */ $pattern = '/\d+(?:\.\d{3})*(?:,\d{1,2})?|\d+(?:\.\d{1,2})?/u'; if (!preg_match($pattern, $value, $matches)) { /* * Não existe valor. * * Nesse caso todo o conteúdo é considerado texto. */ return [ 'price' => null, 'description' => $value, ]; } $numericValue = $matches[0]; /* * Converte formato brasileiro. * * 1.250,50 * => * 1250.50 */ if (str_contains($numericValue, ',')) { $numericValue = str_replace( '.', '', $numericValue ); $numericValue = str_replace( ',', '.', $numericValue ); } /* * Caso seja algo como: * * 50.00 * * mantém como decimal. */ $price = is_numeric($numericValue) ? (float) $numericValue : null; /* * Remove o valor encontrado do texto original. * * Exemplo: * * "50,00 + contraste" * * vira: * * "+ contraste" */ $description = preg_replace( '/' . preg_quote($matches[0], '/') . '/u', '', $value, 1 ); /* * Remove espaços extras. */ $description = trim( preg_replace( '/\s+/u', ' ', (string) $description ) ); /* * Remove caracteres separadores sobrando * no começo/fim. * * Exemplo: * * "+ contraste" * * vira: * * "contraste" */ $description = trim( $description, " \t\n\r\0\x0B+-–—" ); return [ 'price' => $price, 'description' => $description !== '' ? $description : null, ]; } }