|
|
@@ -5,6 +5,7 @@ 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;
|
|
|
|
|
|
@@ -23,27 +24,63 @@ class ExamesImportService
|
|
|
$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(fn ($value) => trim((string) $value))
|
|
|
+ ->map(function ($value) {
|
|
|
+ return trim((string) $value);
|
|
|
+ })
|
|
|
->values();
|
|
|
|
|
|
/*
|
|
|
* Ignora linha completamente vazia.
|
|
|
*/
|
|
|
- if ($cells->filter(fn ($value) => $value !== '')->isEmpty()) {
|
|
|
+ if (
|
|
|
+ $cells
|
|
|
+ ->filter(fn ($value) => $value !== '')
|
|
|
+ ->isEmpty()
|
|
|
+ ) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Estrutura esperada do Excel:
|
|
|
+ * Estrutura esperada:
|
|
|
*
|
|
|
* Coluna 0 = Código do exame
|
|
|
* Coluna 1 = Nome do exame
|
|
|
- * Coluna 2 = Preço
|
|
|
+ * Coluna 2 = Valor / informação adicional
|
|
|
*/
|
|
|
$codeExams = $cells->get(0, '');
|
|
|
$name = $cells->get(1, '');
|
|
|
@@ -57,22 +94,27 @@ class ExamesImportService
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Trata cabeçalhos do Excel.
|
|
|
+ * 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')
|
|
|
+ str_contains($nameHeader, 'EXAME') &&
|
|
|
+ (
|
|
|
+ str_contains($priceHeader, 'VALOR') ||
|
|
|
+ str_contains($priceHeader, 'PREÇO') ||
|
|
|
+ str_contains($priceHeader, 'PRECO')
|
|
|
+ )
|
|
|
);
|
|
|
|
|
|
if ($isHeader) {
|
|
|
@@ -80,32 +122,129 @@ class ExamesImportService
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Converte o preço para o formato numérico.
|
|
|
+ * 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.
|
|
|
*/
|
|
|
- $associatePrice = $this->parsePrice($price);
|
|
|
+ $isPrice = $associatePrice !== null;
|
|
|
|
|
|
/*
|
|
|
- * Procura um exame já cadastrado para esse parceiro.
|
|
|
+ * REGRA DOS BLOCOS
|
|
|
+ *
|
|
|
+ * Se:
|
|
|
+ *
|
|
|
+ * - não existe código
|
|
|
+ * - e a terceira coluna NÃO possui valor
|
|
|
+ *
|
|
|
+ * então a linha é um bloco.
|
|
|
*
|
|
|
- * Primeiro tenta localizar pelo código quando
|
|
|
- * o código foi informado.
|
|
|
+ * 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)
|
|
|
+ ->where(
|
|
|
+ 'partner_agreement_id',
|
|
|
+ $partnerAgreementId
|
|
|
+ )
|
|
|
+ ->where(
|
|
|
+ 'code_exams',
|
|
|
+ $codeExams
|
|
|
+ )
|
|
|
->first();
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Caso não exista código ou não tenha encontrado pelo código,
|
|
|
+ * Caso não tenha encontrado pelo código,
|
|
|
* procura pelo nome.
|
|
|
*/
|
|
|
if (!$service) {
|
|
|
$service = PartnerAgreementService::withTrashed()
|
|
|
- ->where('partner_agreement_id', $partnerAgreementId)
|
|
|
+ ->where(
|
|
|
+ 'partner_agreement_id',
|
|
|
+ $partnerAgreementId
|
|
|
+ )
|
|
|
->whereRaw(
|
|
|
'LOWER(TRIM(name)) = LOWER(TRIM(?))',
|
|
|
[$name]
|
|
|
@@ -114,7 +253,9 @@ class ExamesImportService
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
+ * ==========================================================
|
|
|
* EXAME JÁ EXISTE
|
|
|
+ * ==========================================================
|
|
|
*/
|
|
|
if ($service) {
|
|
|
/*
|
|
|
@@ -127,50 +268,92 @@ class ExamesImportService
|
|
|
$changed = false;
|
|
|
|
|
|
/*
|
|
|
- * Atualiza o código quando informado.
|
|
|
+ * Atualiza código.
|
|
|
*/
|
|
|
if (
|
|
|
$codeExams !== '' &&
|
|
|
$service->code_exams !== $codeExams
|
|
|
) {
|
|
|
$service->code_exams = $codeExams;
|
|
|
+
|
|
|
$changed = true;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Atualiza o preço somente quando
|
|
|
- * o Excel trouxe um preço válido.
|
|
|
+ * Atualiza preço somente quando
|
|
|
+ * existe valor válido na planilha.
|
|
|
*/
|
|
|
if (
|
|
|
$associatePrice !== null &&
|
|
|
- (float) $service->associate_price !== (float) $associatePrice
|
|
|
+ (
|
|
|
+ (float) $service->associate_price !==
|
|
|
+ (float) $associatePrice
|
|
|
+ )
|
|
|
) {
|
|
|
$service->associate_price = $associatePrice;
|
|
|
+
|
|
|
$changed = true;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Garante que o serviço seja do tipo EXAME.
|
|
|
+ * Garante categoria EXAME.
|
|
|
*/
|
|
|
if (
|
|
|
- $service->type !== PartnerAgreementServiceTypeEnum::EXAME
|
|
|
+ (int) $service->category_id !==
|
|
|
+ (int) $examCategory->id
|
|
|
) {
|
|
|
- $service->type = PartnerAgreementServiceTypeEnum::EXAME;
|
|
|
+ $service->category_id = $examCategory->id;
|
|
|
+
|
|
|
$changed = true;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Garante que o exame fique ativo.
|
|
|
+ * Garante tipo EXAME.
|
|
|
*/
|
|
|
if (
|
|
|
- $service->status !== PartnerAgreementServiceStatusEnum::ACTIVE
|
|
|
+ $service->type !==
|
|
|
+ PartnerAgreementServiceTypeEnum::EXAME
|
|
|
) {
|
|
|
- $service->status = PartnerAgreementServiceStatusEnum::ACTIVE;
|
|
|
+ $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++;
|
|
|
}
|
|
|
|
|
|
@@ -178,71 +361,213 @@ class ExamesImportService
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
+ * ==========================================================
|
|
|
* 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:
|
|
|
*
|
|
|
- * Cria um novo serviço vinculado
|
|
|
- * ao parceiro informado.
|
|
|
+ * PACOTE CONTRASTE
|
|
|
+ *
|
|
|
+ * quando ele possui valor.
|
|
|
*/
|
|
|
PartnerAgreementService::create([
|
|
|
'partner_agreement_id' => $partnerAgreementId,
|
|
|
- 'code_exams' => $codeExams !== '' ? $codeExams : null,
|
|
|
- 'name' => $name,
|
|
|
- 'associate_price' => $associatePrice,
|
|
|
- 'type' => PartnerAgreementServiceTypeEnum::EXAME,
|
|
|
- 'status' => PartnerAgreementServiceStatusEnum::ACTIVE,
|
|
|
+
|
|
|
+ '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(),
|
|
|
+ 'total' => $rows->count(),
|
|
|
'created' => $created,
|
|
|
'updated' => $updated,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Converte diferentes formatos de preço
|
|
|
- * para float.
|
|
|
+ * 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): ?float
|
|
|
+ private function parsePrice(mixed $value): array
|
|
|
{
|
|
|
- if ($value === null || $value === '') {
|
|
|
- return null;
|
|
|
+ /*
|
|
|
+ * Campo vazio.
|
|
|
+ */
|
|
|
+ if ($value === null || trim((string) $value) === '') {
|
|
|
+ return [
|
|
|
+ 'price' => null,
|
|
|
+ 'description' => null,
|
|
|
+ ];
|
|
|
}
|
|
|
|
|
|
$value = trim((string) $value);
|
|
|
|
|
|
/*
|
|
|
- * Remove moeda e espaços.
|
|
|
- *
|
|
|
- * Exemplos:
|
|
|
- * R$ 50,00 -> 50,00
|
|
|
- * R$50,00 -> 50,00
|
|
|
+ * Remove R$.
|
|
|
+ */
|
|
|
+ $value = str_replace(
|
|
|
+ ['R$', 'r$'],
|
|
|
+ '',
|
|
|
+ $value
|
|
|
+ );
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Normaliza espaços.
|
|
|
*/
|
|
|
- $value = str_replace(['R$', ' '], '', $value);
|
|
|
+ $value = trim($value);
|
|
|
|
|
|
/*
|
|
|
- * Formato brasileiro:
|
|
|
+ * 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($value, ',')) {
|
|
|
- $value = str_replace('.', '', $value);
|
|
|
- $value = str_replace(',', '.', $value);
|
|
|
+ if (str_contains($numericValue, ',')) {
|
|
|
+ $numericValue = str_replace(
|
|
|
+ '.',
|
|
|
+ '',
|
|
|
+ $numericValue
|
|
|
+ );
|
|
|
+
|
|
|
+ $numericValue = str_replace(
|
|
|
+ ',',
|
|
|
+ '.',
|
|
|
+ $numericValue
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
- * Formato decimal:
|
|
|
+ * Caso seja algo como:
|
|
|
*
|
|
|
- * 50
|
|
|
* 50.00
|
|
|
- * 1250.50
|
|
|
+ *
|
|
|
+ * mantém como decimal.
|
|
|
*/
|
|
|
- return is_numeric($value)
|
|
|
- ? (float) $value
|
|
|
+ $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,
|
|
|
+ ];
|
|
|
}
|
|
|
}
|