Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
target/
*.class
*.jar
*.war
*.ear
.idea/
.vscode/
*.iml
.project
.classpath
.settings/
.DS_Store
Thumbs.db
*.log
*.tmp
logs/
*.duckdb

# Frontend
portability-frontend/node_modules/
portability-frontend/dist/
.angular/
35 changes: 35 additions & 0 deletions Dockerfile-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Fase 2 — Imagem da API REST hexagonal (Spring Boot 3.3 / Java 21)
# ---- Stage 1: build ----
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build

# Copia os POMs do multi-módulo primeiro para aproveitar o cache do Docker
COPY pom.xml .
COPY portability-core/pom.xml portability-core/
COPY portability-adapters/pom.xml portability-adapters/
COPY portability-batch/pom.xml portability-batch/
COPY portability-api/pom.xml portability-api/
RUN mvn -B -q dependency:go-offline || true

# Copia o código-fonte e compila (boot jar do módulo portability-api)
COPY portability-core/src portability-core/src
COPY portability-adapters/src portability-adapters/src
COPY portability-batch/src portability-batch/src
COPY portability-api/src portability-api/src
RUN mvn -B -q clean package -DskipTests

# ---- Stage 2: runtime ----
FROM eclipse-temurin:21-jre
WORKDIR /app

COPY --from=build /build/portability-api/target/portability-api-*-boot.jar /app/portability-api.jar

# Dicionários/prefixos embarcados; a base DuckDB é montada como volume.
COPY Operadoras.csv /app/Operadoras.csv
COPY Operadoras_completo.csv /app/Operadoras_completo.csv
COPY prefixos_operadora.json /app/prefixos_operadora.json

VOLUME ["/app/saida", "/app/logs"]
EXPOSE 8080

CMD ["java", "-jar", "/app/portability-api.jar"]
30 changes: 17 additions & 13 deletions PLANO_MELHORIAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,24 @@ Objetivo: alinhar toolchain e criar rede de segurança antes de refatorar.

**Resultado:** `mvn clean verify` verde — 55 testes (34 core + 11 adapters/benchmark + ... incluindo batch), Checkstyle + JaCoCo ok, benchmark automatizado como gate de regressão de performance (<30s; atual ~1,2s).

### Fase 2 — API REST com arquitetura hexagonal (~2–3 semanas)
- [ ] Módulo `api` (Spring Boot 3.x, Java 21):
- `POST /api/v1/portabilidade/consultar` — 1..N números (sync, limitado)
- `POST /api/v1/portabilidade/batch` — upload multipart CSV/XLSX → retorna job ID
### Fase 2 — API REST com arquitetura hexagonal (~2–3 semanas) ✅ CONCLUÍDA
- [x] Módulo `portability-api` (Spring Boot 3.3, Java 21, boot jar `-boot.jar`):
- `POST /api/v1/portabilidade/consultar` — 1..N números (sync, máx. 1000, deduplicados)
- `POST /api/v1/portabilidade/batch` — upload multipart CSV/XLSX → retorna job ID (`202`)
- `GET /api/v1/portabilidade/batch/{id}` — status/progresso
- `GET /api/v1/portabilidade/batch/{id}/resultado` — download
- [ ] Respeitar a hexagonal na prática:
- `application/` = use cases (`ConsultPortabilityUseCase`, `SubmitBatchUseCase`) dependendo só de ports
- `infrastructure/web` = controllers + DTOs/mappers; `infrastructure/persistence` = adapter DuckDB existente
- Composição via `@Configuration` (elimina DI manual do `main`)
- [ ] Contratos: **OpenAPI 3** (`springdoc-openapi`) + versões de API (`/v1`), erros padronizados (**RFC 7807 ProblemDetails**)
- [ ] Validação de entrada (`jakarta.validation`), limites de payload, deduplicação de números na request
- [ ] **Cache**: resultados de portabilidade são relativamente estáveis → Caffeine (L1, hot numbers) com TTL configurável; avaliar Redis (L2) para múltiplas réplicas
- [ ] Testes: contrato (`@WebMvcTest`), integração com DuckDB embarcado, carga básica (k6/Gatling) — meta p95 < 50 ms single lookup com cache
- `GET /api/v1/portabilidade/batch/{id}/resultado` — download CSV
- [x] Hexagonal na prática:
- `application/usecase` = `ConsultPortabilityUseCase`, `SubmitBatchUseCase` dependendo só de ports
- `infrastructure/web` = controllers + DTOs/mappers; `infrastructure/repository` = adapter DuckDB + cache; `infrastructure/batch` = executor de jobs
- Composição via `@Configuration`/Spring DI
- [x] Contratos: **OpenAPI 3** (`springdoc-openapi`, `/v3/api-docs` + Swagger UI), versionamento `/v1`, erros **RFC 7807 ProblemDetails**
- [x] Validação de entrada (`jakarta.validation`), limites de payload (multipart 100 MB), deduplicação de números
- [x] **Cache**: Caffeine L1 com TTL configurável (Redis L2 avaliado na Fase 4)
- [x] Testes: web layer (`MockMvc` standalone), use cases unitários, integração batch E2E — build verde com 70 testes no total
- [x] Virtual threads habilitadas (`spring.threads.virtual.enabled=true`) + compressão gzip
- [x] Docs/entrega: seção "API REST" no README, `Dockerfile-api` + serviço `portability-api` no docker-compose

**Resultado:** `mvn clean verify` BUILD SUCCESS — 70 testes (51 legados + 19 da API), Checkstyle/JaCoCo ok, smoke test do boot jar validado (endpoint consultar + swagger).

### Fase 3 — BFF + Frontend Angular 17+ (~2–4 semanas)
- [ ] Módulo **`bff`** (Spring Boot dedicado ao frontend):
Expand Down
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,17 @@ portabilidade-java/ # Parent POM (multi-módulo, Java 21)
│ ├── output/ (CsvOutputWriter)
│ ├── repository/ (DuckDbPortabilityRepository)
│ └── resolver/ (JsonPrefixResolver)
└── portability-batch/ # ← Camada de entrada (main + orquestração)
├── portability-batch/ # ← Camada de entrada (main + orquestração)
│ └── src/main/java/com/portabilidade/
│ ├── PortabilityApplication.java # Ponto de entrada (main)
│ └── application/PortabilityBatchProcessor.java
└── portability-api/ # ← API REST hexagonal (Spring Boot 3.3 / Java 21)
└── src/main/java/com/portabilidade/
├── PortabilityApplication.java # Ponto de entrada (main)
└── application/PortabilityBatchProcessor.java
├── application/usecase/ # Casos de uso (ConsultPortability, SubmitBatch)
└── infrastructure/
├── web/ # Controllers, DTOs, RFC 7807, OpenAPI
├── repository/ # Adapter DuckDB + cache Caffeine
└── batch/ # Executor de jobs de batch (multipart CSV/XLSX)
```

---
Expand Down Expand Up @@ -339,6 +346,42 @@ Para usar um dicionário de operadoras externo, basta colocar um

---

## API REST (Fase 2 — Spring Boot 3.3 / Java 21)

O módulo `portability-api` expõe a mesma lógica de negócio do batch como serviço HTTP,
com arquitetura hexagonal (use cases no centro, adapters de web/persistência nas bordas).

### Executar

```bash
mvn -pl portability-api -am package
java -jar portability-api/target/portability-api-1.0.0-boot.jar
# porta padrão: 8080 | Swagger UI: http://localhost:8080/swagger-ui.html
```

### Endpoints

| Método | Rota | Descrição |
|--------|------|-----------|
| `POST` | `/api/v1/portabilidade/consultar` | Consulta 1..N números (sync, máx. 1000 por request, deduplicados, cache Caffeine) |
| `POST` | `/api/v1/portabilidade/batch` | Upload multipart CSV/XLSX → retorna job ID (`202 Accepted`) |
| `GET` | `/api/v1/portabilidade/batch/{id}` | Status/progresso do job |
| `GET` | `/api/v1/portabilidade/batch/{id}/resultado` | Download do CSV de resultado |

Exemplo:

```bash
curl -s localhost:8080/api/v1/portabilidade/consultar \
-H 'Content-Type: application/json' \
-d '{"numeros":["11987654321","2125551234"]}'
```

Erros seguem **RFC 7807 ProblemDetails** (`application/problem+json`) e o contrato
OpenAPI 3 está em `/v3/api-docs`. Requisições são atendidas em **virtual threads**
(Java 21) com compressão gzip habilitada.

---

## Solução de problemas

| Sintoma | Causa provável | Solução |
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,21 @@ services:
- ./prefixos_operadora.json:/app/prefixos_operadora.json:ro
# Roda o batch uma vez e encerra (--rm remove o container ao final)
restart: "no"

# Fase 2: API REST hexagonal (Spring Boot 3.3 / Java 21) — porta 8080
portability-api:
build:
context: .
dockerfile: Dockerfile-api
image: portability-api:latest
container_name: portability-api
ports:
- "8080:8080"
volumes:
- ./base_portabilidade:/app/base_portabilidade
- ./saida:/app/saida
- ./logs:/app/logs
- ./Operadoras.csv:/app/Operadoras.csv:ro
- ./Operadoras_completo.csv:/app/Operadoras_completo.csv:ro
- ./prefixos_operadora.json:/app/prefixos_operadora.json:ro
restart: unless-stopped
17 changes: 17 additions & 0 deletions logs/portabilidade.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
2026-10-31 01:27:58.031 [main] INFO c.p.PortabilityApplication - Portabilidade Numérica Batch — iniciando
2026-10-31 01:27:58.035 [main] INFO c.p.application.config.AppPaths - BASE_DIR resolvido: /workspace/portability-batch/target
2026-10-31 01:27:58.037 [main] INFO c.p.PortabilityApplication - Caminhos configurados:
2026-10-31 01:27:58.037 [main] INFO c.p.PortabilityApplication - entrada/ : /workspace/portability-batch/target/entrada
2026-10-31 01:27:58.037 [main] INFO c.p.PortabilityApplication - saida/ : /workspace/portability-batch/target/saida
2026-10-31 01:27:58.037 [main] INFO c.p.PortabilityApplication - DuckDB : /workspace/portability-batch/target/base_portabilidade/portabilidade.duckdb
2026-10-31 01:27:58.037 [main] INFO c.p.PortabilityApplication - Operadoras.csv : /workspace/portability-batch/target/Operadoras.csv
2026-10-31 01:27:58.037 [main] INFO c.p.PortabilityApplication - prefixos JSON : /workspace/portability-batch/target/prefixos_operadora.json
2026-10-31 01:27:58.038 [main] WARN c.p.i.loader.CsvOperatorLoader - Arquivo Operadoras.csv não encontrado em: /workspace/portability-batch/target/Operadoras.csv
2026-10-31 01:27:58.038 [main] INFO c.p.PortabilityApplication - Dicionário de operadoras (exemplo): 0 entradas
2026-10-31 01:27:58.038 [main] WARN c.p.PortabilityApplication - Nenhum dicionário completo de operadoras encontrado — usando o de exemplo (baixa cobertura).
2026-10-31 01:27:58.329 [main] WARN c.p.i.loader.JsonPrefixMapLoader - Arquivo de prefixos não encontrado em: /workspace/portability-batch/target/prefixos_operadora.json
2026-10-31 01:27:58.330 [main] INFO c.p.PortabilityApplication - Mapa de prefixos: 0 entradas
2026-10-31 01:27:58.334 [main] INFO c.p.a.PortabilityBatchProcessor - === Portabilidade Numérica Batch - Iniciando ===
2026-10-31 01:27:58.340 [main] WARN c.p.infrastructure.file.FileScanner - Nenhum arquivo .csv ou .xlsx encontrado em: /workspace/portability-batch/target/entrada
2026-10-31 01:27:58.341 [main] WARN c.p.a.PortabilityBatchProcessor - Nenhum arquivo encontrado em '/workspace/portability-batch/target/entrada'. Encerrando.
2026-10-31 01:27:58.341 [main] INFO c.p.PortabilityApplication - Portabilidade Numérica Batch — concluído com sucesso
7 changes: 5 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
<module>portability-core</module>
<module>portability-adapters</module>
<module>portability-batch</module>
<!-- Fase 2: <module>portability-api</module> -->
<!-- Fase 3: <module>portability-bff</module> -->
<module>portability-api</module>
<module>portability-bff</module>
</modules>

<properties>
Expand All @@ -44,6 +44,7 @@
<mockito.version>5.12.0</mockito.version>

<jacoco.version>0.8.12</jacoco.version>
<springdoc.version>2.6.0</springdoc.version>
<checkstyle.version>10.17.0</checkstyle.version>
<maven-checkstyle-plugin.version>3.5.0</maven-checkstyle-plugin.version>
<maven-surefire.version>3.3.1</maven-surefire.version>
Expand Down Expand Up @@ -138,6 +139,8 @@
<configuration>
<release>${maven.compiler.release}</release>
<encoding>UTF-8</encoding>
<!-- Spring 6.1 exige -parameters para resolver nomes de @PathVariable/@RequestParam -->
<parameters>true</parameters>
</configuration>
</plugin>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,51 @@ public StreamingCsvWriter open(String originalName, String outputDir) throws IOE
}

/** Writer de CSV em streaming: grava linha a linha, header automático. */
public static final class StreamingCsvWriter implements Closeable {
public static class StreamingCsvWriter implements Closeable {
private final Path outputPath;
private final BufferedWriter writer;
private String[] headers; // definidos na primeira linha
final BufferedWriter writer;
String[] headers;
private long rowsWritten;
private boolean closed;

StreamingCsvWriter(Path outputPath) throws IOException {
this(outputPath, true);
}

/**
* Construtor protegido para uso por subclasses em outros módulos (API/Fase 2).
* @param withBom BOM UTF-8 apenas para outputs "Excel-friendly" do batch legado;
* resultados de API usam CSV puro.
*/
public StreamingCsvWriter(Path outputPath, boolean withBom) throws IOException {
this.outputPath = outputPath;
// UTF-8 com BOM para compatibilidade com Excel
this.writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(outputPath.toFile()),
StandardCharsets.UTF_8), WRITE_BUFFER_BYTES);
this.writer.write('\uFEFF');
if (withBom) {
this.writer.write('\uFEFF');
}
}

/** Escreve um header fixo antes da primeira linha (usado pela API — Fase 2). */
public void writeHeader(List<String> columns) throws IOException {
if (headers != null || rowsWritten > 0) {
throw new IllegalStateException("header ja escrito");
}
headers = columns.toArray(new String[0]);
StringBuilder hl = new StringBuilder();
for (int i = 0; i < headers.length; i++) {
if (i > 0) hl.append(SEPARATOR);
hl.append(escapeField(headers[i]));
}
writer.write(hl.toString());
writer.newLine();
}

/** Libera o buffer para o disco sem fechar o arquivo (progresso visível durante execução). */
public void flush() throws IOException {
writer.flush();
}

public void writeRow(Map<String, Object> row) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.portabilidade.infrastructure.repository;



import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

/**
* Provisiona a base DuckDB de portabilidade (Fase 1/2 do PLANO_MELHORIAS).
*
* Uso: java -cp ... DuckDbDatabaseProvisioner &lt;base_dir&gt; &lt;arquivo_portabilidade.csv&gt; [out.duckdb]
*
* O arquivo de origem deve ter colunas telefone (BIGINT) e rn1 (VARCHAR),
* separadas por ; ou , (detectado automaticamente). A carga é feita via
* COPY + JOIN contra um CSV gerado com tipos normalizados, e a tabela final
* já sai com índice em telefone (query path read-only nunca escreve).
*/
public final class DuckDbDatabaseProvisioner {

private DuckDbDatabaseProvisioner() {
}

public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.err.println("Uso: DuckDbDatabaseProvisioner <baseDir> <portabilidade.csv> [saida.duckdb]");
System.exit(2);
}
Path baseDir = Path.of(args[0]);
Path source = Path.of(args[1]);
Path out = args.length > 2 ? baseDir.resolve(args[2])
: baseDir.resolve("portabilidade.duckdb");
Files.createDirectories(out.getParent());
long t0 = System.currentTimeMillis();
long rows = provision(source, out);
System.out.printf("Base provisionada: %s (%d linhas, %d ms)%n",
out, rows, System.currentTimeMillis() - t0);
}

/** Cria (sobrescrevendo) a base portabilidade(telefone BIGINT, rn1 VARCHAR) + índice. */
public static long provision(Path sourceCsv, Path duckdbFile) throws Exception {
String sep = detectSeparator(sourceCsv);
Path tmp = Files.createTempFile("prov_portabilidade_", ".csv");
try (BufferedWriter w = Files.newBufferedWriter(tmp, StandardCharsets.UTF_8)) {
normalizeToCsv(sourceCsv, sep, w);
}
try (Connection c = DriverManager.getConnection("jdbc:duckdb:" + duckdbFile);
Statement st = c.createStatement()) {
// duckdb_jdbc 1.1.x: o separador é passado por nome ('sep'), nao 'delimiter'.
st.execute("CREATE TABLE portabilidade AS "
+ "SELECT CAST(telefone AS BIGINT) AS telefone, rn1 "
+ "FROM read_csv('" + tmp.toAbsolutePath().toString().replace("'", "''")
+ "', sep=',', header=true)");
st.execute("CREATE INDEX IF NOT EXISTS idx_port_telefone ON portabilidade(telefone)");
var rs = st.executeQuery("SELECT count(*) FROM portabilidade");
rs.next();
long rows = rs.getLong(1);
c.close();
return rows;
} finally {
Files.deleteIfExists(tmp);
}
}

private static String detectSeparator(Path csv) throws Exception {
try (var lines = Files.lines(csv, StandardCharsets.UTF_8)) {
String first = lines.findFirst().orElse(",");
int semi = first.indexOf(';'), comma = first.indexOf(',');
return (semi >= 0 && semi < comma) || comma < 0 ? ";" : ",";
}
}

/** Copia telefone/rn1 (colunas 0 e 1 após header) escapando aspas duplas. */
private static void normalizeToCsv(Path sourceCsv, String sep, BufferedWriter w) throws Exception {
w.write("telefone,rn1\n");
try (var r = Files.newBufferedReader(sourceCsv, StandardCharsets.UTF_8)) {
String line;
boolean header = true;
while ((line = r.readLine()) != null) {
if (header) { header = false; continue; }
if (line.isBlank()) continue;
int idx = line.indexOf(sep);
if (idx <= 0) continue;
String tel = line.substring(0, idx).trim();
String rn1 = line.substring(idx + sep.length()).trim();
if (!tel.matches("\\d+")) continue;
w.write(tel);
w.write(',');
w.write('"');
w.write(rn1.replace("\"", "\"\""));
w.write('"');
w.write('\n');
}
}
}
}
Loading
Loading