---
id: SVC-EXCELIMPORTER
tipo: servico
onda: 2
status: concluido
unidades: [SVC-EXCELIMPORTER]
fontes:
  - V2/Services/ExcelImporter/
depende_de: [CORE-BOOT, CORE-AUTH]
consumidores: [SVC-MEDIA]
criticidade: alta
dono: SvcExcel
---

# Serviço ExcelImporter (importação de planilhas por schema declarativo)

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Recebe um upload XLSX/XLS/CSV, converte-o em um relatório de linhas/células validadas (`preview`) e, depois, persiste cada linha via callback do consumidor emitindo progresso NDJSON; separadamente, transpila planilhas para CSV/HTML e gera template XLSX. |
| Entrada | `ImportExcelableSchema` (schema declarado pelo consumidor), `UploadedFile` (`import_file`), JSON `{rows, log_id, group_id, clear_current}` no processamento, `$context` (`model_class`, `group_id`, `file_name`). |
| Saída | Array de relatório (`summary`, `rows[]`, `file_url`); `StreamedResponse` `application/x-ndjson` com uma linha `{"type":"progress",...}` por lote de 25; string CSV (`;`, CRLF); string HTML de tabela; download `.xlsx` do template. |
| Efeitos colaterais | Grava o upload em `imports/AAAA/MM/<time>_<slug>.<ext>`; grava dois registros em `activity_log` ("Importação - Preview", "Importação - Concluída"); logs `Log::info`/`Log::warning`/`Log::error`; muta o arquivo temporário do upload ao converter encoding CSV; eleva `memory_limit` para 512M e `set_time_limit(300)`. |
| Particularidade central | O contrato é **schema declarativo + validação de dados + persistência por callback**: o serviço nunca conhece o model de destino, nunca abre transação de lote e nunca aplica autorização; a "inteligência" mora em `ImportExcelSchema`/`ImportExcelSchemaColumn` e o consumidor grava com seu próprio `$persister`. |
| Como se integra | `ExcelImporterServiceProvider` registra views no namespace `excel-importer` e singletons `ImportExcelService`/`ExcelTranspilerService` (`V2/Services/ExcelImporter/Providers/ExcelImporterServiceProvider.php:16-35`), carregado por `config/app.php:186`; Blade `@include('excel-importer::components.excel-transpiler', [...])`. |
| Se removido, quebra | `SpreadsheetOptimizer` (variantes `.csv`/`.html` de planilhas em `SVC-MEDIA`, `V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php:48-59`) e qualquer módulo que inclua o Blade (nenhum módulo em `V2/Modules` o faz hoje — ver §9). |
| Superfície pública | `Contracts\ImportExcelableSchema`, `Services\ImportExcelSchema`, `Services\ImportExcelSchemaColumn`, `Services\ImportExcelDataValidator`, `Services\ImportExcelService`, `Services\ImportExcelTemplateGenerator`, `Services\ExcelTranspilerService`, views `excel-transpiler`/`modals`/`scripts`/`styles`. |

## 1. Efeito

### 1.1 Transformação que o serviço realiza

1. **Preview** — `previewImportExcelData(UploadedFile $file, ImportExcelableSchema $importer, array $context = [])`: converte o CSV para UTF-8 se necessário, grava o arquivo, audita, lê a **primeira aba** com `Excel::toCollection($importer, $file)`, valida cabeçalhos/células/duplicatas e devolve um relatório. Não grava nenhuma entidade de negócio.
2. **Processamento** — `processImportExcelData(array $input, callable $persister, array $context = [])`: percorre `$input['rows']`, monta `$data` com `normalized_value` de cada célula, chama `$persister($data, $row)` **uma vez por linha dentro de `DB::beginTransaction()`**, e emite progresso a cada 25 linhas. Linhas com exceção são revertidas e contabilizadas, sem abortar o lote.
3. **Template** — `ImportExcelTemplateGenerator::download()` gera XLSX com os `label` do schema como cabeçalho e **uma** linha de exemplo preenchida com `exemplo_descricao`.
4. **Transpilação** — `ExcelTranspilerService::convertToCsv()` produz CSV delimitado por `;`, com aspas, CRLF e apenas a aba 0; `convertToHtmlTable()` produz `<div class="table-responsive"><table role="grid">` com a primeira linha como `<th scope="col">` e o resto em `<td>` escapados.

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Converte CSV não-UTF-8 para UTF-8 **in place** no arquivo temporário do upload | `$file->getRealPath()` | `mb_convert_encoding` + `file_put_contents` | `V2/Services/ExcelImporter/Services/ImportExcelService.php:38-49` |
| Grava o original em `imports/AAAA/MM/time_slug.ext` no disco default | filesystem (`$file->storeAs`) | antes de qualquer parsing | `V2/Services/ExcelImporter/Services/ImportExcelService.php:52-55` |
| Registra auditoria de preview com IP, user-agent, arquivo, módulo, group id | tabela `activity_log` (spatie) | `activity()->causedBy(Sentinel::getUser())->...->log('Importação - Preview')` | `V2/Services/ExcelImporter/Services/ImportExcelService.php:60-72` |
| Registra auditoria de conclusão com contadores e até 50 erros | `activity_log` | `logImportFinished` | `V2/Services/ExcelImporter/Services/ImportExcelService.php:174-193` |
| Persiste negócio linha a linha | tabelas do consumidor | `$persister($data, $row)` + `DB::commit()` | `V2/Services/ExcelImporter/Services/ImportExcelService.php:131-148` |
| Ajusta limites de execução da requisição | PHP | `ini_set('memory_limit','512M')`, `set_time_limit(300)` | `V2/Services/ExcelImporter/Services/ImportExcelService.php:28-29` |
| Grava variantes `.csv`/`.html` de planilhas (consumidor Media) | storage público | `SpreadsheetOptimizer::generateVariants` | `V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php:48-59` |

Não há escrita em cache, índice de busca, e-mail, fila ou evento de domínio.

### 1.3 Contrato observável

```text
ImportExcelService::previewImportExcelData(UploadedFile, ImportExcelableSchema, array $context = []): array
  → { summary: { total_rows, valid_rows, invalid_rows, duplicate_rows, has_errors, missing_columns[], encoding_warning? },
      rows: [ { line, is_valid, cells: { <key>: { column_key, column_label, column_type,
               original_value, normalized_value, is_valid, is_required, errors[], hint } },
               unique_key, is_duplicate, duplicate_type?, duplicate_info? } ],
      file_url: string }
  → exceções: "O arquivo enviado está vazio ou não possui abas de dados válidas."
              "A primeira aba da planilha não contém dados processáveis."
              qualquer exceção do leitor/validador é re-lançada após Log::error

ImportExcelService::processImportExcelData(array $input, callable $persister, array $context = []): StreamedResponse
  → HTTP 200, Content-Type application/x-ndjson
  → N linhas JSON {"type":"progress","processed":int,"total":int,"success":int,"errors_count":int,"is_final":bool}
  → não retorna corpo final agregado; erros só nos contadores e em activity_log

ImportExcelDataValidator::validateImportExcelData(iterable $rows, ImportExcelSchema $schema, array $context = []): array
ImportExcelDataValidator::validateCell($value, ImportExcelSchemaColumn $column): array   // {valid, errors[], normalized}
ExcelTranspilerService::convertToCsv(UploadedFile $file): string
ExcelTranspilerService::convertToHtmlTable(UploadedFile $file, array $options = []): string
```

Estados inválidos: `$data` vazio (arquivo sem abas) e primeira aba vazia são exceções (`ImportExcelService.php:79-87`); planilha com coluna obrigatória ausente devolve relatório com `summary.has_errors = true` e **`rows: []`** (`ImportExcelDataValidator.php:59-70`); formato irreconhecível no transpilador lança `"Formato de planilha não reconhecido: ..."` (`ExcelTranspilerService.php:183`).

### 1.4 O que o serviço deliberadamente NÃO faz

- **Não declara rotas, controller, permissão nem middleware** — o diretório não tem `Routes/`; as rotas `v2.modulo.import.preview|process|template` do docblock do Blade (`V2/Services/ExcelImporter/Views/components/excel-transpiler.blade.php:4-10`) são responsabilidade do consumidor.
- **Não persiste entidade** — `processImportExcelData` exige um `$persister`; sem ele não há escrita de negócio.
- **Não valida cabeçalho contra o banco nem consulta duplicatas existentes** — só compara linhas do próprio arquivo (`duplicate_type = 'file'`).
- **Não ignora `log_id`, `group_id` nem `clear_current`** no processamento: o payload JS é aceito, mas o serviço lê apenas `$input['rows']` (`ImportExcelService.php:115`). `clear_current` não tem implementação server-side.
- **Não apaga** o arquivo gravado, nem em caso de erro de validação.
- **Não aplica `WithHeadingRow`/`WithMapping`** ao importador; isso é pré-requisito do consumidor (§3.2 RN-EXCELIMPORTER-01).
- **Não usa transação de lote**: commit/rollback é por linha; uma falha no meio não desfaz as anteriores (`ImportExcelService.php:131-148`).

## 2. Abordagem

### 2.1 Estratégia e por quê

O serviço separa três camadas explícitas:

1. **Schema declarativo** (`ImportExcelSchema` + `ImportExcelSchemaColumn`): descreve colunas, tipos, obrigatoriedade, exemplos/aliases, chave de unicidade e validadores cross-column. Um módulo novo só declara dados, não escreve parser.
2. **Validação de dados** (`ImportExcelDataValidator`): mapeia cabeçalhos → chaves internas, normaliza tipos, detecta duplicatas e devolve um relatório "Poka-Yoke" (a UI permite editar/ignorar linhas antes de persistir).
3. **Persistência** (`ImportExcelService::processImportExcelData`): streaming NDJSON + callback, para evitar timeout e dar feedback de progresso em volumes grandes.

Essa separação é o motivo de `preview` e `process` serem chamadas HTTP distintas: o consumidor valida/edita no navegador e depois envia de volta **apenas os valores normalizados** das linhas ativas.

### 2.2 Mecânica passo a passo

```mermaid
flowchart TD
  A[POST preview: import_file] --> B{ext == csv?}
  B -- sim --> C[detecta encoding e converte para UTF-8 in place]
  B -- não --> D
  C --> D[storeAs imports/AAAA/MM]
  D --> E[activity 'Importação - Preview']
  E --> F[schema = importer.getImportExcelSchema()]
  F --> G[Excel::toCollection importer, file -> primeira aba]
  G --> H[validateImportExcelData]
  H --> I{coluna obrigatória ausente?}
  I -- sim --> J[retorna summary.has_errors + rows vazio]
  I -- não --> K[mapeia headers, valida células, rowValidators]
  K --> L[ordena por uniqueColumns, marca duplicatas, reordena por line]
  L --> M[retorna report + file_url]
  M --> N[JS renderiza tabela Poka-Yoke]
  N --> O[POST process: rows normalizadas]
  O --> P[stream NDJSON: chunk 25]
  P --> Q[beginTransaction -> persister -> commit]
  Q --> R{exceção?}
  R -- sim --> S[rollBack + errors++]
  R -- não --> T[success++]
  S --> U[echo progress + flush]
  T --> U
  U --> V[logImportFinished]
```

Passos reais do preview (`ImportExcelService.php:26-104`): encoding (36-50) → `storeAs` (52-55) → `activity()` (60-72) → `getImportExcelSchema()` (74) → `Excel::toCollection` (77) → guarda de vazio (79-87) → `validateImportExcelData` (90) → injeta `encoding_warning` (93-95) → `file_url` (97) → `return` (99).

Passos do validador (`ImportExcelDataValidator.php:19-183`): monta `summary`/`rows` (20-29) → mapeia cabeçalhos (40-66) → aborta se faltar obrigatória (69-70) → por linha, por coluna, cria `cellReport` e chama `validateCell` (86-118) → roda `rowValidators` (121-123) → calcula `unique_key` (125) → ordena por chave única, descendente por linha (129-144) → segunda passagem marca `is_duplicate`/contadores (146-176) → reordena por `line` (179-182).

### 2.3 Trechos de código pivô

```php
// V2/Services/ExcelImporter/Contracts/ImportExcelableSchema.php:7-12 — contrato mínimo:
// o consumidor entrega o schema; nada mais é exigido pelo tipo.
interface ImportExcelableSchema
{
    /**
     * Retorna o schema de importação (colunas, tipos, regras).
     */
    public function getImportExcelSchema(): ImportExcelSchema;
}
```

O tipo não impõe `WithHeadingRow`; a exigência existe por causa de `Excel::toCollection($importer, $file)` (§3.2).

```php
// tests/Feature/V2/Services/ExcelImporter/ExcelImporterTest.php:20-24 — única declaração real
// de schema no repositório (não existe módulo de produção que implemente o contrato).
$schema = (new ImportExcelSchema('Planilha de Teste'))
    ->addColumn(ImportExcelSchemaColumn::make('titulo', 'Título')->tipo('string')->obrigatorio())
    ->addColumn(ImportExcelSchemaColumn::make('cnpj', 'CNPJ')->tipo('cnpj'))
    ->addColumn(ImportExcelSchemaColumn::make('ativo', 'Ativo')->tipo('boolean'))
    ->setUniqueColumns(['titulo']);
```

Mapeamento resultante: cabeçalho `Título`/`titulo` → chave `titulo` (tipo `string`, obrigatório); `CNPJ` → `cnpj` (tipo `cnpj`, opcional); `Ativo` → `ativo` (tipo `boolean`, opcional). `setUniqueColumns(['titulo'])` faz `titulo` compor o hash de duplicidade; num consumidor real, essa mesma lista precisa ser passada ao Blade em `uniqueColumns` para o destaque no cliente coincidir.

```php
// V2/Services/ExcelImporter/Services/ImportExcelDataValidator.php:45-56 — mapeamento de cabeçalho:
// aceita chave interna, label literal e label "slugado"; aliases entram por último.
$possibleNames = array_merge([$key, $column->label, Str::slug($column->label, '_')], $column->aliases);
$found = false;

foreach ($possibleNames as $name) {
    foreach ($headers as $h) {
        if (strtolower(trim((string)$h)) === strtolower(trim((string)$name))) {
            $headerMapping[$h] = $key;
            $matchedHeaders[] = $h;
            $found = true;
            break 2;
        }
    }
}
```

Como o Maatwebsite aplica `Str::slug($value, '_')` por default no cabeçalho (`vendor/maatwebsite/excel/src/Imports/HeadingRowFormatter.php:96-97`, config `vendor/maatwebsite/excel/config/excel.php:111-113`), o match efetivo costuma acontecer pela chave interna ou pelo slug do label — nunca pelo label acentuado.

```php
// V2/Services/ExcelImporter/Services/ImportExcelDataValidator.php:196-210 — chave de unicidade:
// lowercase + trim dos valores normalizados, group_id opcional, md5.
$parts = [];
foreach ($uniqueCols as $col) {
    $parts[] = strtolower(trim((string)($cells[$col]['normalized_value'] ?? '')));
}

if ($groupId !== null) {
    $parts[] = $groupId;
}

// Se todos os campos estiverem vazios, não gera chave única
if (empty(array_filter($parts))) {
    return null;
}

return md5(implode('|', $parts));
```

A ordenação anterior (`:132-143`) é o que define **qual** linha sobrevive: o comparador desempata por `$b['line'] <=> $a['line']` (linha maior primeiro), então a **última** ocorrência é marcada como canônica e as anteriores viram duplicatas (`:154-161`).

```php
// V2/Services/ExcelImporter/Services/ImportExcelDataValidator.php:216-229 — validação atômica:
// obrigatoriedade primeiro (retorno antecipado), tipo só se houver valor.
public function validateCell($value, ImportExcelSchemaColumn $column): array
{
    $res = ['valid' => true, 'errors' => [], 'normalized' => $value];

    // 1. Validação de Obrigatoriedade
    if ($column->obrigatorio && ($value === null || $value === '')) {
        $res['errors'][] = "O campo {$column->label} é obrigatório.";
        $res['valid'] = false;
        return $res;
    }

    // 2. Validação de Tipo (apenas se houver valor)
    if ($value !== null && $value !== '') {
        $typeResult = $this->validateImportExcelCellByType($value, $column->tipo, $column->label);
```

Os tipos estão em `validateImportExcelCellByType` (`:245-344`): `cnpj` (14 dígitos), `date` (serial Excel, `d/m/Y`, fallback `Carbon::parse`), `currency`, `boolean`, `integer`, `url`; qualquer outro valor de `tipo` passa sem validação (`:249-341`).

```php
// V2/Services/ExcelImporter/Services/ImportExcelService.php:74-90 — preview: uma única aba,
// erros de arquivo vazio como exceção, validação delegada.
$schema = $importer->getImportExcelSchema();

try {
    $data = Excel::toCollection($importer, $file);

    if (!$data || $data->isEmpty()) {
        throw new \Exception("O arquivo enviado está vazio ou não possui abas de dados válidas.");
    }

    $rows = $data->first();

    if (!$rows || $rows->isEmpty()) {
        throw new \Exception("A primeira aba da planilha não contém dados processáveis.");
    }

    // 3. Validação
    $report = $this->validator->validateImportExcelData($rows, $schema, $context);
```

`Excel::toCollection` devolve uma coleção de abas; `->first()` descarta todas as demais (`:83`).

```php
// V2/Services/ExcelImporter/Services/ImportExcelService.php:129-148 — lote de 25,
// transação por linha, erro não interrompe o restante.
foreach ($chunks as $chunk) {
    foreach ($chunk as $row) {
        DB::beginTransaction();
        try {
            $data = [];
            foreach ($row['cells'] as $key => $cell) {
                $data[$key] = $cell['normalized_value'] ?? null;
            }

            // Executa o callback customizado de persistência
            $persister($data, $row);

            DB::commit();
            $results['success']++;
        } catch (\Exception $e) {
            DB::rollBack();
            $errorMessage = "Linha {$row['line']}: " . $e->getMessage();
            $results['errors'][] = $errorMessage;
            Log::error("[IMPORT > SERVICE] Erro de persistência: " . $errorMessage);
        }
    }
```

Só `\Exception` é capturada; um `\Error` (`TypeError`, `Error`) escapa do `try`, deixa a transação aberta e aborta o stream.

```php
// V2/Services/ExcelImporter/Services/ExcelTranspilerService.php:25-38 — transpilação CSV:
// aba 0, delimitador brasileiro, aspas, CRLF, captura via output buffer.
$spreadsheet = $this->loadSpreadsheet($file);
$writer = new Csv($spreadsheet);

// Define o delimitador padrão para o padrão brasileiro (ponto e vírgula) ou vírgula
$writer->setDelimiter(';');
$writer->setEnclosure('"');
$writer->setLineEnding("\r\n");
$writer->setSheetIndex(0);

ob_start();
$writer->save('php://output');
$csvContent = ob_get_clean();

return $csvContent;
```

O carregamento tenta leitores em cascata (`:122-186`): `Xlsx`; `Xls` depois `Html` (XLS falsificado); `Csv`; `Ods`; se nada casar, `IOFactory::load` e, no limite, `Html`. Cada tentativa de `Csv`/`Html` detecta encoding numa amostra de 4096 bytes (`:145-160`).

### 2.4 Algoritmos e custos

- **Complexidade do validador**: O(L·C + L·U) por linha/coluna (L linhas, C colunas, U colunas únicas), mais `usort` O(L log L) duas vezes (`:132`, `:179`). Tudo em memória — o arquivo inteiro é materializado por `toCollection`.
- **Memória**: `memory_limit` forçado a 512M (`:28`); não há `WithChunkReading`, então o custo cresce com o número de linhas e colunas da aba.
- **I/O**: uma leitura do arquivo por chamada ao transpilador (cada `convertToCsv`/`convertToHtmlTable` abre a planilha de novo); uma gravação do upload no preview.
- **Streaming**: `array_chunk($rows, 25)` (`:127`), `ob_flush(); flush();` após cada lote (`:163-164`). O cliente recebe progresso incremental, não um resultado final.
- **Concorrência/idempotência**: nenhum lock; duas requisições simultâneas com o mesmo arquivo geram nomes distintos apenas se `time()` mudar — colisão possível no mesmo segundo.
- **Consultas**: o serviço não consulta banco; as consultas são as do `$persister` do consumidor.

## 3. Particularidades

### 3.1 O que é exclusivo deste serviço

É o único ponto do V2 que combina **schema declarativo em PHP + relatório de validação serializável + persistência por callback com streaming**. Os outros importadores (p.ex. `V2/Modules/Support/NoticiasImport/Controllers/NoticiasImportController.php:22-40`) são rotas de transporte para importação por diretório/API e não usam este contrato. `SVC-MEDIA` reutiliza **apenas** o transpilador (`SpreadsheetOptimizer.php:48-59`).

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-EXCELIMPORTER-01 | O objeto passado a `previewImportExcelData` deve implementar `ImportExcelableSchema` **e** `Maatwebsite\Excel\Concerns\WithHeadingRow` | sem `WithHeadingRow` o cabeçalho não é extraído (`HeadingRowExtractor::extract` devolve `[]`), a linha 1 vira dado com chaves numéricas e todo schema com coluna obrigatória resulta em `missing_columns` | `vendor/maatwebsite/excel/src/Imports/HeadingRowExtractor.php:35-64`; `V2/Services/ExcelImporter/Services/ImportExcelService.php:77` |
| RN-EXCELIMPORTER-02 | Só a **primeira aba** é importada | abas adicionais são ignoradas silenciosamente | `ImportExcelService.php:83` |
| RN-EXCELIMPORTER-03 | Coluna obrigatória ausente aborta a validação antes de ler linhas | `summary.has_errors = true`, `rows = []`, `total_rows = 0` | `ImportExcelDataValidator.php:59-70` |
| RN-EXCELIMPORTER-04 | Coluna não mapeada é descartada, salvo `allowExtraColumns(true)` | em schema dinâmico a chave da célula é o cabeçalho cru e o tipo cai em `string` na UI | `ImportExcelDataValidator.php:56,89-98` |
| RN-EXCELIMPORTER-05 | Duplicidade é **intra-arquivo** e não invalida a linha | a última ocorrência prevalece; a anterior recebe `is_duplicate = true` e continua `is_valid` | `ImportExcelDataValidator.php:132-166` |
| RN-EXCELIMPORTER-06 | `line` = índice da linha na coleção + 2 | assume cabeçalho na linha 1; `WithStartRow`/cabeçalho multi-linha desalinham o número exibido | `ImportExcelDataValidator.php:76-77` |
| RN-EXCELIMPORTER-07 | `validateCell` para na obrigatoriedade | valor vazio e obrigatório produz **um** erro, sem avaliar tipo | `ImportExcelDataValidator.php:221-225` |
| RN-EXCELIMPORTER-08 | `cnpj` valida apenas comprimento | 14 dígitos sem cálculo de dígitos verificadores; máscara é removida | `ImportExcelDataValidator.php:250-257` |
| RN-EXCELIMPORTER-09 | `integer` aceita qualquer `is_numeric` | `"12.9"` normaliza para `12` no servidor; o JS rejeita ponto | `ImportExcelDataValidator.php:312-318`; `V2/Services/ExcelImporter/Assets/js/importer-class.js:336-340` |
| RN-EXCELIMPORTER-10 | `url` completa o protocolo com `http://` | entrada sem esquema nunca é inválida por ausência de protocolo | `ImportExcelDataValidator.php:321-333` |
| RN-EXCELIMPORTER-11 | Persistência é por linha, não por lote | falha no meio mantém as linhas já commitadas | `ImportExcelService.php:131-148` |
| RN-EXCELIMPORTER-12 | O arquivo do upload é gravado **antes** do parsing | planilha inválida ainda deixa arquivo em `imports/` e auditoria de preview | `ImportExcelService.php:52-72` |
| RN-EXCELIMPORTER-13 | `clear_current`, `log_id`, `group_id` e `is_valid` são ignorados no processamento | o servidor só lê `rows[].cells[].normalized_value` e `rows[].line` | `ImportExcelService.php:115,134-136` |
| RN-EXCELIMPORTER-14 | `getHeaders()` devolve apenas `label` | template e importação compartilham os mesmos rótulos legíveis, sem chave interna | `ImportExcelSchema.php:81-86` |
| RN-EXCELIMPORTER-15 | O CSV transpilado sempre usa `;` e CRLF | arquivos gerados não são idênticos ao CSV de origem | `ExcelTranspilerService.php:29-31` |

### 3.3 Casos-limite

- **Arquivo corrompido ou formato divergente** → a exceção do leitor do Maatwebsite sobe após `Log::error("[IMPORT > SERVICE] Falha crítica no processamento")` (`ImportExcelService.php:101-103`); o arquivo **já foi gravado** e o preview **já foi auditado** antes da falha.
- **Reimportação do mesmo arquivo** → não há idempotência: o serviço reenvia todas as linhas ao `$persister`, e a decisão "criar vs atualizar" é inteiramente do consumidor; a checagem de duplicidade é apenas intra-arquivo (RN-EXCELIMPORTER-05).
- **Planilha vazia / sem abas** → exceção `"O arquivo enviado está vazio ou não possui abas de dados válidas."`; **aba 1 vazia** → `"A primeira aba da planilha não contém dados processáveis."` (`ImportExcelService.php:79-87`).
- **Coluna obrigatória faltante** → relatório sem `rows`; a UI abre a mesa de validação vazia e **não mostra** `missing_columns` (o JS nunca lê `report.summary`) — ver `LG-EXCELIMPORTER-02`.
- **Cabeçalho com acento** → casa por slug/chave, não pelo label literal (`ImportExcelDataValidator.php:45`).
- **Schema dinâmico** → colunas extras passam a integrar `cells` e são enviadas ao `$persister` com o cabeçalho como chave.
- **Todas as células de uma linha vazias** → linha existe (o iterador inclui linhas vazias), todas as obrigatórias falham e as opcionais ficam válidas; a linha é `is_valid = false`.
- **Duplicata com chave toda vazia** → `unique_key = null`, nenhuma duplicidade é marcada (`:206-208`).
- **Linha duplicada e inválida** → a UI abre a linha para edição (`row-invalid row-unlocked`) e o botão "Processar e Importar" fica bloqueado enquanto houver linha inválida ou desbloqueada (`V2/Services/ExcelImporter/Assets/js/importer-ui.js:36-38`).
- **Ignorar linha** → o JS remove do envio (`tr:not(.row-ignored)`, `importer-class.js:136`), mas o servidor não sabe que a linha foi ignorada; apenas recebe menos linhas.
- **Falha de persistência em uma linha** → `"Linha {line}: {mensagem}"` no log/auditoria e `errors_count++`; o fluxo continua (`ImportExcelService.php:143-148`).
- **Zero linhas no processamento** → o `stream` não emite nenhum JSON (`array_chunk([],25)` = `[]`) e ainda grava `"Importação - Concluída"` com 0/0 (`:167`). O JS bloqueia esse caso antes (`importer-class.js:166-169`).
- **`\Error` no persister** → não é capturado (só `\Exception`), a transação fica aberta e o stream morre sem `is_final`.
- **Arquivo `.xls` que é HTML** → o leitor `Html` é tentado depois de `Xls` (`ExcelTranspilerService.php:131-133`), mas `previewImportExcelData` **não** usa essa cascata: usa o detector do Maatwebsite (`Excel::toCollection`) e pode falhar onde o transpilador funciona.
- **CSV Latin-1** → convertido para UTF-8 antes de gravar; o aviso é devolvido em `summary.encoding_warning`, mas a UI não o exibe (`ImportExcelService.php:93-95`).
- **Colisão de nome** → `time()` + slug permite sobrescrita se dois uploads do mesmo nome ocorrerem no mesmo segundo e no mesmo mês (`:54-55`).
- **`Sentinel::getUser()` nulo** (CLI/queue) → `activity()->causedBy(null)` é aceito pelo spatie, mas a auditoria fica sem causador (`:61`).

### 3.4 Armadilhas de reimplementação

- **Não confundir as duas validações**: o servidor valida no preview e o navegador revalida ao editar (`importer-class.js:300-371`). As regras divergem (integer com ponto, CNPJ formatado, data serial) — ver RN-EXCELIMPORTER-09.
- **`normalized_value` é o contrato de envio**: o JS reenvia **apenas** `cells[key].normalized_value` (texto editado), perdendo `original_value`; qualquer reimplementação que espere o valor original no processamento quebra.
- **Ordem do schema define a ordem visual**: `getColumns()` preserva a ordem de inserção e a UI usa as chaves da primeira linha do relatório (`importer-ui.js:107-127`).
- **Encoding é mutado no arquivo temporário** antes do `storeAs`; portanto o arquivo salvo já é UTF-8 — mas só para `.csv` com extensão em minúsculas (`:38`).
- **`ImportExcelService` é singleton** (`ExcelImporterServiceProvider.php:29-31`) e guarda `$validator`; não há estado por requisição — não adicione estado mutável.
- **Transpilador não é idempotente em memória**: `loadSpreadsheet` recarrega o arquivo a cada chamada; chamar CSV e HTML custa duas leituras completas.
- **`convertToHtmlTable` assume que a primeira linha do iterador é a 1**: a troca `<thead>`→`<tbody>` depende de `$rowIndex === 2` (`ExcelTranspilerService.php:73`); planilhas com `startRow` diferente produzem HTML sem `tbody`.
- **Caption duplamente escapado**: o default já passa por `e()` (`:58`) e o valor é escapado de novo (`:63`), então nomes com `&` viram `&amp;amp;`.
- **`getMimeType()` no `UploadedFile`** pode ser `null`/vazio em uploads de teste; a cascata depende também da extensão (`:124-138`).

### 3.5 Dívidas e divergências

- **Nenhum consumidor de produção do fluxo de importação**: não existe classe em `V2` que implemente `ImportExcelableSchema` nem controller que chame `previewImportExcelData`/`processImportExcelData`; só o teste usa o validador diretamente. O teste ainda importa `V2\Modules\Exemplo\Imports\ExemploImport` (`tests/Feature/V2/Services/ExcelImporter/ExcelImporterTest.php:10`), classe inexistente — referência morta.
- **Blade duplica a inicialização**: `excel-transpiler.blade.php` inclui `components.scripts` (`:225`) e ainda faz `@push('scripts')` com o mesmo bloco (`:227-341`), criando duas instâncias de `LotepImporter` e handlers duplicados.
- **JS referencia DOM inexistente**: `#chkClearCurrentData` (`importer-class.js:44`) e `#import_group_id` (`:83`) não são renderizados por nenhum dos componentes.
- **Relatório rico, UI pobre**: `summary.missing_columns`, `summary.encoding_warning`, `file_url`, `duplicate_type = 'database'` e `is_ignored` são produzidos/previstos mas não exibidos ou nunca emitidos.
- **Assets não publicados**: as views carregam `asset('assets/js/V2/services/excel-importer/*.js')` (`scripts.blade.php:1-3`), caminho que não existe em `public/assets/js` (lista: `cms-alerts.js`, `cms-layout.js`, `content-rich-editor-bundle.js`, `masks.js`, `media-group-input.js`, `media-input.js`, `portal-init.js`).
- **Provider não segue o padrão de descoberta de serviços**: é registrado manualmente em `config/app.php:186` e não tem `Routes/`, ao contrário de `SEO`, `Gallery` e `FeedRSS`.

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `ImportExcelableSchema::getImportExcelSchema` | `(): ImportExcelSchema` | schema | — | `ImportExcelService.php:74` |
| `ImportExcelSchema::__construct` | `(string $title)` | instância | — | testes/consumidores |
| `ImportExcelSchema::allowExtraColumns` | `(bool $allow = true): $this` | self | — | — |
| `ImportExcelSchema::isDynamic` | `(): bool` | bool | — | `ImportExcelDataValidator.php:32` |
| `ImportExcelSchema::addColumn` | `(ImportExcelSchemaColumn $column): $this` | self | — | declaração de schema |
| `ImportExcelSchema::getColumns` | `(): ImportExcelSchemaColumn[]` | mapa `nome_interno => column` | — | validator, template |
| `ImportExcelSchema::getHeaders` | `(): string[]` | labels na ordem | — | `ImportExcelTemplateGenerator.php:32-35` |
| `ImportExcelSchema::setUniqueColumns` | `(array $columns): $this` | self | — | declaração |
| `ImportExcelSchema::addRowValidator` | `(callable $callback): $this` | self | — | validação cross-column |
| `ImportExcelSchemaColumn::make` | `(string $nome_interno, string $label): static` | column | — | declaração |
| `ImportExcelSchemaColumn::tipo/obrigatorio/exemplo/descricao/aliases` | fluent setters | self | — | declaração |
| `ImportExcelDataValidator::validateImportExcelData` | `(iterable $rows, ImportExcelSchema $schema, array $context = []): array` | relatório | — | `ImportExcelService.php:90`; teste |
| `ImportExcelDataValidator::validateCell` | `($value, ImportExcelSchemaColumn $column): array` | `{valid, errors, normalized}` | — | validação por célula |
| `ImportExcelService::previewImportExcelData` | `(UploadedFile, ImportExcelableSchema, array $context = []): array` | relatório | `\Exception` (arquivo vazio, aba vazia, falha de leitura) | consumidor HTTP |
| `ImportExcelService::processImportExcelData` | `(array $input, callable $persister, array $context = []): StreamedResponse` | NDJSON | não propaga falha de linha | consumidor HTTP |
| `ExcelTranspilerService::convertToCsv` | `(UploadedFile): string` | CSV | `"Falha ao converter Excel para CSV: ..."` | `SpreadsheetOptimizer.php:49-53` |
| `ExcelTranspilerService::convertToHtmlTable` | `(UploadedFile, array $options = []): string` | HTML | `"Falha ao converter Excel para Tabela HTML: ..."` | `SpreadsheetOptimizer.php:55-59` |
| `ImportExcelTemplateGenerator::download` | `(?string $fileName = null): BinaryFileResponse` | download | — | rota do consumidor |

`ImportExcelSchemaColumn` expõe propriedades públicas (`nome_interno`, `label`, `tipo`, `obrigatorio`, `exemplo_descricao`, `descricao_interna`, `aliases`) lidas diretamente pelo validador; esses valores chegam ao JS pelo relatório (`column_label`, `column_type`, `is_required`, `hint`). Não há encapsulamento (`V2/Services/ExcelImporter/Services/ImportExcelSchemaColumn.php:7-13`).

### 4.2 Componentes Blade e contrato de UI

Componente `excel-importer::components.excel-transpiler`; props `title`, `routePreview` (obrigatória), `routeProcess` (obrigatória), `routeTemplate` (opcional), `uniqueColumns` (array). Contrato completo de DOM, eventos e estados em `ui-contrato.md`, nesta mesma pasta.

### 4.3 JS e ciclo de vida

Três arquivos globais carregados em ordem por `scripts.blade.php:1-3`: `ImporterValidator` (funções puras), `ImporterUI` (DOM/modais), `LotepImporter` (orquestração, instanciada em `window.lotepImporterInstance`). Requer jQuery, Bootstrap 5 (`bootstrap.Modal`), `bootstrap-icons` e as classes CSS de `styles.blade.php`. Sem JS, o modal de upload existe no HTML mas o botão "Enviar para Validação" não faz nada (é `type="button"` com handler jQuery), e não há `<form action>` de fallback — ver `LG-EXCELIMPORTER-04`.

### 4.4 Ordem de inicialização e registro

1. `config/app.php:186` registra `ExcelImporterServiceProvider` (depois de `V2ServiceProvider`, `:185`).
2. `boot()` publica as views no namespace `excel-importer` (`ExcelImporterServiceProvider.php:16-20`).
3. `register()` cria os singletons `ImportExcelService` e `ExcelTranspilerService` (`:27-36`).
4. `Maatwebsite\Excel\ExcelServiceProvider` é registrado antes (`config/app.php:177`); `spatie/laravel-activitylog` é dependência (`composer.json`, `spatie/laravel-activitylog ^2.8`) e fornece `activity()`.
5. O consumidor deve declarar as rotas e importar/incluir o Blade depois do layout CMS (stacks `scripts`/`modals` — `V2/layouts/cms/layouts/app.blade.php:140-142`).
6. `v2_storage_url()` precisa existir (`V2/Core/Helpers/helpers.php:259-269`) para `file_url`.

### 4.5 Receitas de uso

**Receita 1 — declarar o schema em um model/importador do módulo (padrão canônico).**

```php
// Model do módulo: implementa o contrato do ExcelImporter e o concern do Maatwebsite.
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use V2\Services\ExcelImporter\Contracts\ImportExcelableSchema;
use V2\Services\ExcelImporter\Services\ImportExcelSchema;
use V2\Services\ExcelImporter\Services\ImportExcelSchemaColumn;

class AssociadoImport extends \Illuminate\Database\Eloquent\Model implements ImportExcelableSchema, WithHeadingRow
{
    public function getImportExcelSchema(): ImportExcelSchema
    {
        return (new ImportExcelSchema('Associados'))
            ->addColumn(ImportExcelSchemaColumn::make('nome', 'Nome')
                ->tipo('string')->obrigatorio()->exemplo('Maria da Silva'))
            ->addColumn(ImportExcelSchemaColumn::make('cnpj', 'CNPJ')
                ->tipo('cnpj')->aliases(['CNPJ/CPF'])->exemplo('30.999.856/0001-83'))
            ->addColumn(ImportExcelSchemaColumn::make('fundacao', 'Data de Fundação')
                ->tipo('date')->exemplo('01/01/1990'))
            ->setUniqueColumns(['cnpj']);
    }
}
```

Resultado esperado: cabeçalho `Nome`/`CNPJ`/`Data de Fundação` (ou `nome`, `cnpj`, `data_de_fundacao`) → chaves `nome`, `cnpj`, `fundacao`; `cnpj` é a chave de duplicidade e o type hint exibido na UI.

**Receita 2 — controller de preview.**

```php
public function preview(Request $request)
{
    $request->validate(['import_file' => 'required|file|mimes:xlsx,xls,csv|max:10240']);

    $report = app(\V2\Services\ExcelImporter\Services\ImportExcelService::class)
        ->previewImportExcelData(
            $request->file('import_file'),
            new \App\Imports\AssociadoImport(),
            ['model_class' => \App\Models\Associado::class]
        );

    return response()->json($report);
}
```

Resultado: JSON com `summary`, `rows[]` e `file_url`; o JS usa `rows` para montar a tabela. O `UploadedFile` é o mesmo `import_file` do formulário.

**Receita 3 — controller de processamento com `$persister`.**

```php
public function process(Request $request, ImportExcelService $service)
{
    $payload = $request->all();

    return $service->processImportExcelData($payload, function (array $data, array $row) {
        \App\Models\Associado::updateOrCreate(
            ['cnpj' => $data['cnpj']],
            ['nome' => $data['nome'], 'fundacao' => $data['fundacao'] ?? null]
        );
    }, ['file_name' => $request->input('file_name', 'N/A')]);
}
```

Resultado: resposta NDJSON; cada linha de negócio é gravada na própria transação e o cliente mostra progresso até `is_final = true`.

**Receita 4 — template XLSX.**

```php
public function template()
{
    $schema = (new \App\Imports\AssociadoImport())->getImportExcelSchema();
    return (new \V2\Services\ExcelImporter\Services\ImportExcelTemplateGenerator($schema))->download();
}
```

Resultado: download `associados_template.xlsx` (slug do título + `_template.xlsx`), com cabeçalhos = labels e uma linha de exemplo.

**Receita 5 — reuso do transpilador.**

```php
$transpiler = app(\V2\Services\ExcelImporter\Services\ExcelTranspilerService::class);
$csv  = $transpiler->convertToCsv($uploadedFile);        // "Nome";"Email"\r\n...
$html = $transpiler->convertToHtmlTable($uploadedFile, ['caption' => 'Planilha enviada']);
```

### 4.6 Anti-uso

- **Implementar só `ImportExcelableSchema`, sem `WithHeadingRow`** — `Excel::toCollection` não extrai cabeçalho; todas as colunas obrigatórias são reportadas como ausentes e o relatório volta sem linhas (RN-EXCELIMPORTER-01). O sintoma é uma tabela vazia, sem mensagem.
- **Esperar rollback global do lote** — o serviço commita por linha; usar `processImportExcelData` para uma importação "tudo ou nada" deixa dados parciais após falha no meio (RN-EXCELIMPORTER-11). Para atomicidade, o `$persister` precisa de estratégia própria (pré-validação total ou tabela de staging).
- **Contar com `clear_current`, `group_id` ou `log_id` no servidor** — o payload é aceito e ignorado (`ImportExcelService.php:115`); implementar a limpeza "antes de importar" no cliente não tem efeito.
- **Confiar no número de linha do arquivo** — `line` é índice da coleção + 2; planilhas com linha de título deslocam todos os números (RN-EXCELIMPORTER-06).
- **Usar o transpilador como pré-processador do import** — o preview usa o leitor do Maatwebsite, não `loadSpreadsheet`; converter para CSV antes não garante que o preview aceite o mesmo arquivo.

### 4.7 Custos e limites

| Limite | Valor | Fonte |
|---|---|---|
| Memória | `512M` forçado na requisição | `ImportExcelService.php:28,112` |
| Tempo | `300s` por preview/processamento | `ImportExcelService.php:29,113` |
| Lote de progresso | 25 linhas | `ImportExcelService.php:117` |
| Buffer do stream | 200 bytes (segundo argumento de `response()->stream`) | `ImportExcelService.php:168` |
| Erros auditados | primeiros 50 | `ImportExcelService.php:189` |
| Extensões aceitas na UI | `.xlsx,.xls,.csv` | `excel-transpiler.blade.php:45` |
| Extensões no transpilador | xlsx, xls, csv, ods (+ HTML disfarçado) | `ExcelTranspilerService.php:129-138` |
| Sem paginação/chunk reading | arquivo inteiro em memória | `ImportExcelService.php:77` |
| Sem throttle próprio | depende da rota do consumidor | — |

## 5. Rotas e endpoints

O diretório **não declara rotas** (não existe `V2/Services/ExcelImporter/Routes/`; a descoberta de serviços só mapeia `Routes/{cms,web,api}.php` quando presentes). O contrato esperado pelo componente é:

| Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|
| POST | definida pelo consumidor | `v2.modulo.import.preview` | consumidor (`web`/`CmsAuthenticate` + permissão do módulo) | consumidor → `ImportExcelService@previewImportExcelData` | do módulo | grava arquivo, audita e devolve relatório JSON | `V2/Services/ExcelImporter/Views/components/excel-transpiler.blade.php:6,232` |
| POST | definida pelo consumidor | `v2.modulo.import.process` | consumidor | consumidor → `ImportExcelService@processImportExcelData` | do módulo | persiste linhas via callback e devolve NDJSON | `V2/Services/ExcelImporter/Views/components/excel-transpiler.blade.php:7,233` |
| GET | definida pelo consumidor | `v2.modulo.import.template` | consumidor | consumidor → `ImportExcelTemplateGenerator@download` | do módulo | baixa XLSX do template | `V2/Services/ExcelImporter/Views/components/excel-transpiler.blade.php:8,95` |

O componente só usa `$routePreview`/`$routeProcess`; `$routeTemplate` é opcional e, quando ausente, o card "Modelo Oficial" é substituído por um aviso (`excel-transpiler.blade.php:83-110`).

**Validação server-side**: o serviço não valida o upload (tipo, tamanho, mimes) — `previewImportExcelData` recebe `UploadedFile` já validado. O exemplo de `mimes:xlsx,xls,csv|max:10240` é receita do consumidor, não código existente. CSRF: o JS envia `X-CSRF-TOKEN` (`importer-class.js:93-95,183-184`) e o form tem `csrf_field()` (`excel-transpiler.blade.php:113`).

## 6. Dados e migrations

Não se aplica: a unidade não tem `Migrations/` nem models próprios; nenhuma tabela é criada. Escritas indiretas:

| Recurso | Papel | Quem escreve | Fonte |
|---|---|---|---|
| `activity_log` (spatie) | auditoria de preview e conclusão | `ImportExcelService` | `ImportExcelService.php:60-72,180-191` |
| disco default (`imports/AAAA/MM/`) | arquivo original do upload | `ImportExcelService` | `ImportExcelService.php:55` |
| tabelas do módulo | dados importados | `$persister` do consumidor | `ImportExcelService.php:139` |

`SpreadsheetOptimizer` grava `*_variant.csv` e `*_variant.html` via `StorageService` (`V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php:48-53`), mas essa escrita pertence a `SVC-MEDIA`.

## 7. Configuração e permissões

| Chave | Default | Efeito | Fonte |
|---|---|---|---|
| `excel.imports.heading_row.formatter` | `slug` (default do pacote; `config/excel.php` não publicado) | define as chaves do cabeçalho entregues ao validador (`Str::slug($value,'_')`); mudar para `none` altera o matching de labels | `vendor/maatwebsite/excel/config/excel.php:111-113`; `vendor/maatwebsite/excel/src/Imports/HeadingRowFormatter.php:96-97` |
| `filesystems.default` (`FILESYSTEM_DRIVER`) | `public` | destino de `storeAs('imports/AAAA/MM/...')` | `ImportExcelService.php:55`; `config/filesystems.php:16` |

- **Permissões**: nenhuma. O serviço não chama `Sentinel::hasAccess` nem `v2_can`; a autorização é da rota do consumidor.
- **Flags de módulo**: não se aplica (serviço, não módulo; não há `ModuleProvider`).
- **Contexto**: `context['model_class']`, `context['group_id']`, `context['file_name']` são apenas metadados de auditoria/`line` (o `group_id` entra no hash de unicidade via `validateImportExcelData`, `ImportExcelDataValidator.php:201-203`).

## 8. Observabilidade e erros

**Logs**

| Nível | Mensagem | Dados | Fonte |
|---|---|---|---|
| info | `[IMPORT > SERVICE] Iniciando processamento de arquivo para preview` | arquivo, model | `ImportExcelService.php:31-34` |
| warning | `[IMPORT > SERVICE] CSV com encoding {enc} convertido para UTF-8.` | — | `ImportExcelService.php:47` |
| error | `[IMPORT > SERVICE] Falha crítica no processamento` | `error` | `ImportExcelService.php:102` |
| error | `[IMPORT > SERVICE] Erro de persistência: Linha {n}: {msg}` | — | `ImportExcelService.php:147` |
| info | `[IMPORT > SERVICE] Importação finalizada` | `success`, `errors` | `ImportExcelService.php:193` |
| error | `[SpreadsheetOptimizer] Erro ao gerar variantes de planilha: ...` | — | `V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php:62` |

**Auditoria** — dois eventos em `activity_log`, ambos com `v2_audit => true`, `ip_address`, `user_agent` e `Detalhe` em pt-BR: `"Importação - Preview"` e `"Importação - Concluída"` (`:66,186`). O segundo guarda até 50 mensagens de erro (`:189`).

**Mensagens exatas (contrato)**

| Situação | Mensagem | HTTP |
|---|---|---|
| arquivo sem abas | `O arquivo enviado está vazio ou não possui abas de dados válidas.` | 500 (exceção re-lançada) |
| primeira aba vazia | `A primeira aba da planilha não contém dados processáveis.` | 500 |
| campo obrigatório vazio | `O campo {label} é obrigatório.` | 200 (relatório) |
| CNPJ | `CNPJ deve conter 14 dígitos.` | 200 |
| data | `Data inválida (Use DD/MM/AAAA).` / `Data serial inválida.` | 200 |
| moeda | `Valor monetário inválido.` | 200 |
| booleano | `Use Sim ou Não.` | 200 |
| inteiro | `Deve ser um número inteiro.` | 200 |
| URL | `Endereço de site inválido.` | 200 |
| coluna faltante | `summary.missing_columns` (sem mensagem ao usuário) | 200 |
| falha de linha no lote | `Linha {n}: {mensagem da exceção}` (log/auditoria) | 200 no corpo NDJSON |
| CSS | `Formato de planilha não reconhecido: ...` (transpilador) | exceção |
| JS, preview falhou | `Erro ao processar arquivo.` (fallback de `xhr.responseJSON`) | `alert()` |
| JS, linhas vazias | `Nenhuma linha válida selecionada para importar.` | modal |
| JS, sucesso | `{n} registros importados com êxito.` + ` Houve {m} erro(s) de persistência.` | modal + `location.reload()` |
| JS, falha crítica | `Ocorreu um erro crítico durante o processamento.` | modal |
| JS, init | `Erro ao processar dados da planilha: {msg}` | `alert()` |

Fail-open: erro de linha não interrompe o lote; fail-closed: coluna obrigatória ausente impede qualquer processamento (o relatório vem sem `rows`, e a UI desabilita o botão porque não há linhas).

## 9. Consumidores (mapa de integração)

| Consumidor | O que usa | Como usa | Risco de quebra |
|---|---|---|---|
| `SVC-MEDIA` (`SpreadsheetOptimizer`) | `ExcelTranspilerService::convertToCsv`/`convertToHtmlTable` | injeta no construtor e grava variantes via `StorageService` | alto se o transpilador mudar delimitador/estrutura HTML; `generateVariants` engole exceção e retorna `[]` |
| `tests/Feature/V2/Services/ExcelImporter/ExcelTranspilerTest.php` | CSV/HTML | instancia direto | asserções de string exata (`"Nome";"Email";"Idade"`) |
| `tests/Feature/V2/Services/ExcelImporter/ExcelImporterTest.php` | `ImportExcelDataValidator` + schema | instancia direto | depende de ordem de duplicata (linha posterior prevalece) |
| Nenhum módulo de `V2/Modules` | — | não implementa `ImportExcelableSchema`, não inclui o Blade, não chama `preview`/`process` | — |

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| LG-EXCELIMPORTER-01 | Nenhum consumidor de produção do fluxo de importação; o teste referencia `V2\Modules\Exemplo\Imports\ExemploImport`, inexistente | o contrato não é exercitado end-to-end; reimplementação sem caso real de módulo | `tests/Feature/V2/Services/ExcelImporter/ExcelImporterTest.php:10`; busca por `ImportExcelableSchema` em `V2/` |
| LG-EXCELIMPORTER-02 | `summary.missing_columns` e `summary.encoding_warning` não são renderizados pelo JS | usuário vê tabela vazia sem explicação para coluna obrigatória ausente | `V2/Services/ExcelImporter/Assets/js/importer-ui.js:129-262` (nenhuma leitura de `report.summary`) |
| LG-EXCELIMPORTER-03 | JS espera `#chkClearCurrentData` e `#import_group_id`, que nenhum componente renderiza | `clear_current` sempre 0 e `group_id` sempre vazio | `V2/Services/ExcelImporter/Assets/js/importer-class.js:44,83` |
| LG-EXCELIMPORTER-04 | Sem comportamento sem JS: não há `action`/`method` no form nem fallback de submissão | importação impossível com JS desabilitado | `V2/Services/ExcelImporter/Views/components/excel-transpiler.blade.php:19` |
| LG-EXCELIMPORTER-05 | Assets JS referenciados em `public/assets/js/V2/services/excel-importer/` não existem no repositório e não há passo de build/cópia documentado | `asset()` devolve 404; `LotepImporter` indefinido em produção | `V2/Services/ExcelImporter/Views/components/scripts.blade.php:1-3`; `public/assets/js/` |
| LG-EXCELIMPORTER-06 | `duplicate_type = 'database'` é tratado no JS mas nunca emitido pelo servidor (não há consulta ao banco) | o destaque de "duplicata no banco" nunca ocorre; reimplementação precisa decidir se cria essa checagem | `V2/Services/ExcelImporter/Assets/js/importer-class.js:250-259`; validator (só `'file'`, `:157`) |
| LG-EXCELIMPORTER-07 | Duplicação do bloco de init no Blade (include de `scripts` + `@push` idêntico) | duas instâncias/handlers; comportamento não determinístico ao clicar | `V2/Services/ExcelImporter/Views/components/excel-transpiler.blade.php:225-341` |

## 11. Checklist de paridade

- [ ] `PC-EXCELIMPORTER-01` — Schema declarativo: `ImportExcelSchema` aceita colunas ordenadas, `tipo`, `obrigatorio`, `aliases`, `exemplo`, `descricao`, `uniqueColumns` e `addRowValidator`; `getHeaders()` devolve labels na ordem. Fonte: `ImportExcelSchema.php:59-119`.
- [ ] `PC-EXCELIMPORTER-02` — Cabeçalho: `Título`, `titulo` e `titulo` normalizado casam com a coluna; header desconhecido é descartado sem `allowExtraColumns` e preservado com ele. Fonte: `ImportExcelDataValidator.php:45-66,89-91`.
- [ ] `PC-EXCELIMPORTER-03` — Obrigatoriedade: campo vazio e obrigatório gera exatamente `O campo {label} é obrigatório.` e marca a linha inválida. Fonte: `ImportExcelDataValidator.php:221-225`.
- [ ] `PC-EXCELIMPORTER-04` — Tipos: `cnpj` (14 dígitos), `date` (serial, `d/m/Y`, fallback parse → `Y-m-d`), `currency` (`R$`/`.`/espaço removidos, `,`→`.`), `boolean` (`sim/não/1/0/...`), `integer`, `url` (auto `http://`). Fonte: `ImportExcelDataValidator.php:245-341`.
- [ ] `PC-EXCELIMPORTER-05` — Coluna obrigatória ausente: resposta 200 com `summary.has_errors=true`, `missing_columns` preenchido e `rows=[]`. Fonte: `ImportExcelDataValidator.php:59-70`.
- [ ] `PC-EXCELIMPORTER-06` — Duplicidade intra-arquivo: com `uniqueColumns`, a última ocorrência prevalece; anteriores recebem `is_duplicate`, `duplicate_type='file'` e mensagem com o número da linha canônica. Fonte: `ImportExcelDataValidator.php:132-166`.
- [ ] `PC-EXCELIMPORTER-07` — Preview persiste o original em `imports/AAAA/MM/` e audita `Importação - Preview` com IP/user-agent/file/model/group. Fonte: `ImportExcelService.php:52-72`.
- [ ] `PC-EXCELIMPORTER-08` — CSV não-UTF-8 é convertido para UTF-8 e o aviso volta em `summary.encoding_warning`. Fonte: `ImportExcelService.php:38-50,93-95`.
- [ ] `PC-EXCELIMPORTER-09` — Processamento emite uma linha NDJSON por lote de 25 com `type=progress`, `processed`, `total`, `success`, `errors_count`, `is_final`; `Content-Type: application/x-ndjson`. Fonte: `ImportExcelService.php:154-168`.
- [ ] `PC-EXCELIMPORTER-10` — Falha de uma linha reverte apenas aquela transação, incrementa `errors_count` e acrescenta `Linha {n}: {msg}` ao log/auditoria (até 50). Fonte: `ImportExcelService.php:143-148,189`.
- [ ] `PC-EXCELIMPORTER-11` — Template XLSX: nome `slug(title)_template.xlsx`, cabeçalhos = labels, uma linha com os exemplos. Fonte: `ImportExcelTemplateGenerator.php:23-50`.
- [ ] `PC-EXCELIMPORTER-12` — Transpilador CSV: `;`, aspas `"`, CRLF, aba 0; HTML: `table-responsive`, `table table-striped table-bordered table-hover`, `role=grid`, `<caption>`, primeira linha `<th scope="col">`. Fonte: `ExcelTranspilerService.php:29-36,61-96`.
- [ ] `PC-EXCELIMPORTER-13` — Leitor em cascata: `xlsx`→Xlsx; `xls`→Xls/HTML; `csv`/`text/csv`/`text/plain`→Csv; `ods`→Ods; fallback autodetect + HTML; erro final `Formato de planilha não reconhecido: ...`. Fonte: `ExcelTranspilerService.php:129-184`.
- [ ] `PC-EXCELIMPORTER-14` — Componente Blade instancia `LotepImporter` com `previewUrl`, `processUrl`, `csrfToken` e `uniqueColumns`, e o envio usa `X-CSRF-TOKEN` e `Accept: application/json`. Fonte: `excel-transpiler.blade.php:227-238`; `importer-class.js:87-96`.

## 12. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `V2/Services/ExcelImporter/Contracts/ImportExcelableSchema.php` | 1-13 (integral) |
| `V2/Services/ExcelImporter/Providers/ExcelImporterServiceProvider.php` | 1-37 (integral) |
| `V2/Services/ExcelImporter/Services/ImportExcelSchema.php` | 1-121 (integral) |
| `V2/Services/ExcelImporter/Services/ImportExcelSchemaColumn.php` | 1-67 (integral) |
| `V2/Services/ExcelImporter/Services/ImportExcelDataValidator.php` | 1-345 (integral) |
| `V2/Services/ExcelImporter/Services/ImportExcelService.php` | 1-195 (integral) |
| `V2/Services/ExcelImporter/Services/ImportExcelTemplateGenerator.php` | 1-52 (integral) |
| `V2/Services/ExcelImporter/Services/ExcelTranspilerService.php` | 1-187 (integral) |
| `V2/Services/ExcelImporter/Views/components/*.blade.php` | integral (4 arquivos) |
| `V2/Services/ExcelImporter/Assets/js/*.js` | integral (3 arquivos) |
| `V2/Services/Media/Services/Midia/SpreadsheetOptimizer.php` | 1-67 |
| `V2/Core/Helpers/helpers.php` | 250-270 |
| `V2/layouts/cms/layouts/app.blade.php` | 40,140-142 |
| `config/app.php` | 171-188 |
| `tests/Feature/V2/Services/ExcelImporter/*.php` | integral (2 arquivos) |
| `vendor/maatwebsite/excel/src/Reader.php`, `Sheet.php`, `Imports/HeadingRowExtractor.php`, `Imports/HeadingRowFormatter.php`, `config/excel.php` | trechos citados |

Verificação de ausência: `grep -rn "ImportExcelableSchema\|getImportExcelSchema"` e `grep -rn "ImportExcelService\|excel-transpiler"` em `sucesu-cms/V2` só retornam a própria unidade e `SpreadsheetOptimizer`; não há `Routes/` na unidade e não há `public/assets/js/V2`.

## 13. Checklist de extração (A–M)

| Item | Resposta |
|---|---|
| A1 | Ver §0/§1.1: upload → arquivo gravado + relatório de validação; depois linhas → persistência por callback com progresso. |
| A2 | Entradas: `UploadedFile` (`import_file`), schema declarado, JSON `{rows, log_id, group_id, clear_current}`, `$context`; precondições: consumidor implementa o contrato + `WithHeadingRow`, rota autenticada. |
| A3 | Saídas: JSON de relatório, NDJSON de progresso, XLSX de template, string CSV, string HTML, arquivo em `imports/`. |
| A4 | Auditoria `activity_log`, logs info/warning/error, arquivo em disco; sem cache/busca/e-mail. |
| A5 | §1.3 e §8 (tabela de mensagens exatas). |
| A6 | §1.4: sem rotas, sem autorização, sem model, sem transação de lote, sem limpeza. |
| A7 | Público/portal não acessa (não há rota); CMS apenas via consumidor; não há API própria. |
| B1 | §2.1/§2.2 (mermaid). |
| B2 | §2.2 com classes e linhas. |
| B3 | §2.1: schema declarativo para permitir que módulos definam importação sem parser próprio [I]. |
| B4 | §2.3 (8 trechos). |
| B5 | §2.4. |
| B6 | §3.3/§3.4; não há cache nem ciclo de remoção. |
| B7 | §1.4 e §3.3 (transação por linha; exceção de arquivo re-lançada; `\Error` não capturado). |
| C1 | Único ponto com schema declarativo + relatório Poka-Yoke + callback (§3.1). |
| C2 | `Support/NoticiasImport` importa por diretório/API e não reutiliza este contrato; `SVC-MEDIA` só usa transpilador. |
| C3 | §3.2 (RN-01..15). |
| C4 | §3.3. |
| C5 | Duplicata mais recente é a canônica; arquivo é salvo antes de validar; coluna ausente silenciosa na UI. |
| C6 | §3.4. |
| C7 | §3.5. |
| D1 | §4.1. |
| D2 | §4.4. |
| D3 | Provider em `config/app.php:186`; views `excel-importer`; Maatwebsite e activitylog antes. |
| D4 | §4.2 e `ui-contrato.md`. |
| D5 | §4.5 (5 receitas). |
| D6 | §4.6 (5 anti-usos). |
| D7 | §4.7. |
| D8 | §9. |
| D9 | Novos tipos de célula exigem `switch` em `validateImportExcelData`/`validateCell` e espelho em `importer-class.js`; novos validadores de linha usam `addRowValidator`; schema dinâmico usa `allowExtraColumns`. |
| E1 | §5 (nenhuma rota própria). |
| E2 | Não se aplica: não há query, paginação nem filtros. |
| E3 | Não se aplica no serviço; validação de upload é do consumidor. |
| E4 | §1.3/§8. |
| E5 | CSRF no cliente (`X-CSRF-TOKEN`, `csrf_field`); auth é da rota do consumidor. |
| E6 | Não há throttle/cache; processamento não é idempotente (o `$persister` decide). |
| E7 | Não se aplica: rotas não são geradas por algoritmo. |
| F1–F6 | Não se aplica: unidade sem migrations/models/tabelas (§6). |
| G1 | Telas: dois modais (upload e mesa de validação) + progresso/confirmação — ver `ui-contrato.md`. |
| G2 | Campos: `import_file` obrigatório, `.xlsx/.xls/.csv`; células `contenteditable` por tipo; ver `ui-contrato.md`. |
| G3 | Não se aplica: não há listagem paginada. |
| G4 | Modais `modalImportExcelProgress` e `modalImportExcelConfirm`; mensagens exatas em §8. |
| G5 | `importer-class.js` (AJAX + XHR), `importer-ui.js` (DOM), `importer-validator.js` (funções puras). |
| G6 | Ver seção de acessibilidade em `ui-contrato.md`. |
| H1–H6 | Não se aplica: serviço de CMS, sem superfície de portal. |
| I1–I8 | `ui-contrato.md`. |
| J1 | `excel.imports.heading_row.formatter` (§7). |
| J2 | Nenhuma permissão própria. |
| J3 | Não se aplica: sem `ModuleProvider`. |
| J4 | Não se aplica: sem flag de módulo. |
| J5 | Não se aplica: sem feature toggle. |
| K1 | §8. |
| K2 | §8 (dois eventos). |
| K3 | §8. |
| K4 | §4.7. |
| K5 | Upload e audit guardam nome/caminho/IP/user-agent; não há segredos. |
| L1–L3 | Não se aplica: unidade sem seeders. |
| M1 | §11 (14 PCs). |
| M2 | §10 (7 LGs). |
| M3 | §12. |
| M4 | `lint_docs.py`, `coverage.py --strict`, `merge_indices.py --check` executados; `git status V2` vazio. |
