| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- <?php
- namespace App\Services;
- use App\Enums\PartnerAgreementServiceStatusEnum;
- use App\Enums\PartnerAgreementServiceTypeEnum;
- use App\Imports\ExamesImport;
- use App\Models\Category;
- use App\Models\PartnerAgreementService;
- use Maatwebsite\Excel\Facades\Excel;
- class ExamesImportService
- {
- public function syncFromExcel(
- string $filePath,
- int $partnerAgreementId
- ): array {
- $import = new ExamesImport();
- Excel::import($import, $filePath);
- $rows = $import->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,
- ];
- }
- }
|