Skip to content

IuryDevJava/api-hanami

Repository files navigation

Projeto Hanami Backend — API de Análise de Dados

API backend desenvolvida em Java com Spring Boot para processamento de arquivos CSV/XLSX e geração de relatórios analíticos de vendas.
Este projeto faz parte do Projeto Hanami, uma iniciativa de impacto social voltada ao uso de tecnologia para análise de dados.


📌 Visão Geral do Projeto

  • Prazo total: 40 dias
  • Metodologia: Desenvolvimento incremental por sprints 1 e 2
  • Status atual: Finalizado

🎯 Objetivo Geral

Desenvolver uma API robusta capaz de:

  • Receber arquivos CSV/XLSX
  • Usar um arquivo CSV vendas_ficticias_10000_linhas
  • Processar dados de vendas
  • Armazenar informações em banco de dados
  • Gerar relatórios analíticos

🗂️ Planejamento por Sprints

Sprint 1 — Fundação e Início do Desenvolvimento

Foco Principal Entregas
Setup do projeto e arquitetura base Estrutura inicial do projeto
Configuração de ambiente Perfis dev e prod
Início do backend Parser de dados e endpoint de upload
Persistência Entidades e repositórios iniciais

Sprint 2 — Conclusão do desenvolvimento e refinamento (planejada)

Foco Principal Entregas
Finalização da lógica de análise Algoritmos completos
Relatórios Geração de relatórios PDF
Documentação README final e instruções de uso
Docker Ambiente produtivo

🛠️ Tecnologias Utilizadas

  • Java 17
  • Spring Boot
  • MySQL
  • Maven
  • Docker

📁 Estrutura do Projeto

src/main/java/com/hanami/iurydev/apiHanami
├── controller     # Camada de controle (endpoints REST)
├── dto            # Objetos de transferência de dados
├── entity
│   ├── embeddable # Objetos incorporáveis
│   ├── enums      # Enumerações do domínio
│   └── Venda      # Entidade principal de vendas
├── repository     # Interfaces JPA
├── service        # Regras de negócio
└── ApiHanamiApplication

Configuração para ambiente de desenvolvimento e produção

Vá em resources e crie um arquivo application-dev.properties adicione:

    spring.datasource.url=jdbc:mysql://localhost:3306/hanamiapidb
    spring.datasource.username=seu-login-mysql
    spring.datasource.password=sua-senha-mysql
    
    spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.jpa.show-sql=true
    spring.jpa.hibernate.ddl-auto=update
    
    # Aumenta o limite de tamanho do arquivo individual
    spring.servlet.multipart.max-file-size=50MB
    
    # Aumenta o limite total da requisição (arquivo + dados extras)
    spring.servlet.multipart.max-request-size=50MB
    
    spring.jackson.date-format=yyyy-MM-dd
    spring.jackson.time-zone=America/Sao_Paulo

Na mesma pasta crie um arquivo application-prod.properties(para ambiente de produção) adicione:

    spring.datasource.url=jdbc:mysql://${MYSQLHOST}:${MYSQLPORT}/${MYSQLDATABASE}
    spring.datasource.username=${MYSQLUSER}
    spring.datasource.password=${MYSQLPASSWORD}
    
    spring.jpa.hibernate.ddl-auto=update
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.jpa.show-sql=false
    spring.jpa.properties.hibernate.format_sql=true
    
    spring.jackson.date-format=yyyy-MM-dd
    spring.jackson.time-zone=America/Sao_Paulo

No application.properties adicione:

    spring.profiles.active=dev

▶️ Como Rodar o Projeto

Pré-requisitos

  • Java 17 (JDK)
  • MySQL
  • Docker

Passo a passo

  1. Clone o repositório
   git clone https://github.com/IuryDevJava/api-hanami.git
  1. Entre no diretório
   cd api-hanami
  1. Execute a aplicação
   ./mvnw spring-boot:run

Veja se as dependências necessárias para a leitura de arquivos estão no arquivo pom.xml

   <!-- Leitura de arquivos XLSX -->
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>5.2.3</version>
    </dependency>
    
    <!-- Leitura de arquivos CSV -->
    <dependency>
      <groupId>com.opencsv</groupId>
      <artifactId>opencsv</artifactId>
      <version>5.5.2</version>
    </dependency>

Endpoints REST - Sprint 1

Upload de Arquivo de Vendas

Endpoint responsável por receber arquivos CSV ou XLSX, validar os dados, processar as vendas e persistir no banco de dados


Post /vendas/upload

Descrição

Realiza o upload de um arquivo CSV ou XLSX contendo dados de vendas

  • Valida a estrutura do arquivo
  • Valida regras de negócio campo a campo
  • Evita duplicidade por id_transacao
  • Persiste apenas registros válidos
  • Marca registros inválidos com observações

Requisição

  • URL: /vendas/upload
  • Método: POST
  • Content-Type: multipart/form-data

Parâmetro (Body)

Nome Tipo descrição
file File Arquivo CSV ou XLSX com os dados de vendas

Chamada (Postman)

Body

  • Type: form-data
  • Key: file
  • Type: File
  • Value: vendas_ficticias_10000_linhas.csv

Respostas da API - métodos POST

✅ 200 OK - Upload feito com sucesso

Retornado quando o arquivo é processado de forma correta e contêm registros validados
   {
  "Status": "sucesso",
  "Linhas_processadas": 10000
   }

⚠️ 200 OK — Nenhuma nova linha processada

Retornado quando o arquivo é válido, mas não há novas vendas para persistir (ex: dados duplicados)
   {
  "Status": "Aviso: Nenhuma nova linha processada",
  "Linhas_processadas": 0
   }

400 Bad Request — Arquivo não enviado

Retornado quando o parâmetro file não é enviado ou está vazio
   {
  "Status": "erro",
  "Linhas_processadas": 0
   }

422 Unprocessable Entity - Estrutura inválida

Retornado quando o arquivo não possui colunas obrigatórias.
   {
  "Status": "Coluna obrigatória ausente: id_transacao",
  "Linhas_processadas": 0
   }

Métodos GET

http://localhost:8080/vendas/reports/sales-summary - Retorna total de vendas e a média por transação.
   {
  "Receita_liquida": 5243176617.89,
  "Lucro_bruto": 3099751358.11,
  "Total_vendas": 8342927976.00,
  "Media_por_transacao": 928849.70,
  "Custo_total": 5243176617.89,
  "Numero_transacoes": 8982
   }

http://localhost:8080/vendas/reports/product-analysis - Retorna uma lista de produtos sem ordenação.
   [
  {
    "Nome_produto": "Carregador Wireless",
    "Quatidade_vendida": 1006,
    "Total_arrecadado": 288235871.00
  },
  {
    "Nome_produto": "iPhone 15",
    "Quatidade_vendida": 787,
    "Total_arrecadado": 246201201.00
  },
  {
    "Nome_produto": "Apple Watch",
    "Quatidade_vendida": 858,
    "Total_arrecadado": 249837283.00
  }
  ]

http://localhost:8080/vendas/reports/product-analysis?sort_by=quantidade - Retorna os produtos de forma ordenada e por quantidade.
   [
  {
    "Nome_produto": "Cabo USB-C",
    "Quatidade_vendida": 1061,
    "Total_arrecadado": 339386041.00
  },
  {
    "Nome_produto": "Webcam HD",
    "Quatidade_vendida": 1026,
    "Total_arrecadado": 319378902.00
  },
  {
    "Nome_produto": "Carregador Wireless",
    "Quatidade_vendida": 1006,
    "Total_arrecadado": 288235871.00
  }
  ]

http://localhost:8080/vendas/reports/product-analysis?sort_by=valor - Retorna os produtos de forma ordenada e por valor.
   [
  {
    "Nome_produto": "Cabo USB-C",
    "Quatidade_vendida": 1061,
    "Total_arrecadado": 339386041.00
  },
  {
    "Nome_produto": "Webcam HD",
    "Quatidade_vendida": 1026,
    "Total_arrecadado": 319378902.00
  },
  {
    "Nome_produto": "Chromecast",
    "Quatidade_vendida": 934,
    "Total_arrecadado": 294529780.00
  }
  ]

http://localhost:8080/vendas/reports/financial-metrics - Retorna um JSON com lucro_bruto, receita_liquida e custo_total.
   {
  "Receita_liquida": 5243176617.89,
  "Lucro_bruto": 3099751358.11,
  "Custo_total": 5243176617.89
   }

http://localhost:8080/vendas/reports/regional-performance - Retorna um JSON com cada região como chave e suas métricas como valor.

http://localhost:8080/vendas/reports/customer-profile - Retorna um JSON com as distribuições demográficas.

http://localhost:8080/vendas/reports/download?format=json - Retorna um arquivo report.json e faz o download completo do arquivo com as métricas em formato JSON.

http://localhost:8080/vendas/reports/download?format=pdf - Retorna um arquivo report.pdf e faz o download com as métricas em tabela e um gráfico de barras de vendas por região.

Regras de Validação Aplicadas

Durante o processamento do arquivo, são aplicadas validações como:

  • Formato do id_transacao (ex: TXN12345678)
  • Margem de lucro mínima e máxima
  • Idade do cliente
  • Formato de IDs de cliente, produto e vendedor
  • Datas válidas
  • Campos obrigatórios não nulos
  • Enumerações normalizadas (canal de venda, forma de pagamento, região, status de entrega)

Banco de Dados MySQL

Modelo de Dados

A aplicação utiliza o modelo de persistência onde os dados do Produto são tratados como objetos incorporáveis (@Embeddable), resultando em uma tabela única de vendas para otimização de performance analítica.

Criar e usar o banco (não esqueça que o nome do banco precisa ser o mesmo no arquivo properties em spring.datasource.url=jdbc:mysql://localhost:3306/hanamiapidb)

   CREATE DATABASE hanamiapidb;
   USE hanamiapidb;

Listar tabelas

   SHOW TABLES;

Mostra o total de registros

   SELECT COUNT(*) FROM vendas;

Mostra em tabelas com dados os 10 primeiros registros

   SELECT * FROM vendas LIMIT 10;

Registros inválidos

   SELECT id_transacao, observacao_validada
   FROM vendas
   WHERE processado_sucesso = false;

Estatística de processamento

   SELECT processado_sucesso, COUNT(*)
   FROM vendas
   GROUP BY processado_sucesso;

(Apenas para testes) Limpar tabela

   DROP TABLE hanamiapidb.vendas;

Valida o endpoint que retorna os 6 campos. Faz a soma total, calcula a média e conta as transações

   SELECT
       SUM(valor_final) AS total_vendas,
       SUM(valor_final * (margem_lucro / 100)) AS lucro_bruto,
       SUM(valor_final) - SUM(valor_final * (margem_lucro / 100)) AS receita_liquida,
       SUM(valor_final - (valor_final * (margem_lucro / 100))) AS custo_total,
       AVG(valor_final) AS media_por_transacao,
       COUNT(*) AS numero_transacoes
   FROM vendas
   WHERE processado_sucesso = 1;

Valida a lista de produtos, a quantidade vendida e o total arrecadado em ordem decrescente

   SELECT
       nome_produto,
       COUNT(*) AS quantidade_vendida,
       SUM(valor_final) AS total_arrecadado
   FROM vendas
   WHERE processado_sucesso = 1
   GROUP BY nome_produto
   ORDER BY total_arrecadado DESC;

Retorna lucro_bruto, receita_liquida e custo_total

   SELECT
       SUM(valor_final * (margem_lucro / 100)) AS lucro_bruto,
       SUM(valor_final) - SUM(valor_final * (margem_lucro / 100)) AS receita_liquida,
       SUM(valor_final - (valor_final * (margem_lucro / 100))) AS custo_total
   FROM vendas
   WHERE processado_sucesso = 1;

Logs e Observabilidade

Visão Geral

A API utiliza logging estruturado para registrar eventos importantes durante o processamento de arquivos, facilitando:

  • Monitoramento da aplicação
  • Debug de erros
  • Auditoria de processamento
  • Análise de falhas em produção

Insira em cima da classe controller a seguinte anotação:

   @Slf4j
   @RestController
   @RequestMapping("/vendas")
   public class VendaController {
   }

Logs de Sucesso

   200 OK. Arquivo 'vendas_ficticias_10000_linhas.csv' foi processado com sucesso. Total: 10000 linhas

Quando ocorre?

  • Upload válido
  • Arquivo lido corretamente
  • Processamento finalizado sem erros

Logs de Erro - Requisição Inválida (400)

   Erro 400. Ao tentar fazer o upload sem arquivo foi retornado um erro

Quando ocorre?

  • Parâmetro file não enviado
  • Arquivo vazio

Logs de Erro — Estrutura de Arquivo Inválida (422)

   Erro 422. Arquivo enviado não contém uma ou mais colunas obrigatórias Coluna obrigatória ausente: id_transacao

Quando ocorre?

  • CSV/XLSX não possui colunas obrigatórias
  • Estrutura incompatível com o parser

Logs de Erro Crítico — Falha Interna (500)

   Erro crítico durante o processamento de upload

Quando ocorre?

  • Exceções inesperadas
  • Falhas de I/O, parsing ou banco de dados

Documentação da API com Swagger/OpenAPI

Dependência para usar a interface do Swagger

   <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
   </dependency>

Adicione no arquivo application-dev.properties

   springdoc.swagger-ui.path=/docs

Todos os endpoints terão um sumário e uma descrição, então adicione esse exemplo e mude de acordo com o endpoint e sua responsabilidade

   @Operation(
           summary = "Upload",
           description = "Faz o upload de arquivo CSV/XLSX"
   )

Os que possuem parâmetros de query como o sort_by e format são documentados

No endpoit de analisar o produto que tem @GetMapping("/reports/product-analysis"), antes do @RequestParam adicione:

   @Parameter(description = "Critério de ordenação", example = "nome", schema = @Schema(allowableValues = {"nome", "preco", "quantidade"}))

No endpoit desenvolvido pra fazer o downloiad do relatório, que tem @GetMapping("/reports/download"), antes do @RequestParam adicione:

   @Parameter(description = "Formato do arquivo", required = true, schema = @Schema(allowableValues = {"json", "pdf"}))

Implementação de Filtros via Query Methods

Os filtros abaixo utilizam Query Methods do Spring Data JPA, onde as consultas são derivadas automaticamente a partir do nome do método.

No VendaRepository adicione o método de query JPA do estado:

   List<Venda> findByProcessadoSucessoTrueAndCliente_Estado(String estado);

No endpoint /reports/regional-performance, adicione um filtro opcional por sigla do estado (ex: SP):

   @Operation(
        summary = "Clientes e Região",
        description = "Retorna métricas agrupadas por região, com filtro opcional por estado."
   ) 
   @GetMapping("/reports/regional-performance")
   public ResponseEntity<Map<String, MetricasRegiaoDTO>> getRegionalPerformance(
            @RequestParam(required = false) String estado) {
    
        List<Venda> vendas;
        if(estado != null && !estado.trim().isEmpty()) {
            vendas = vendaRepository.findByProcessadoSucessoTrueAndCliente_Estado(estado.toUpperCase());
        } else {
            vendas = vendaRepository.findByProcessadoSucessoTrue();
        }
    
        List<MetricasRegiaoDTO> lista = vendaCalcularService.calcularMetricasPorRegiao(vendas);
    
        // FILTRO ADICIONAL: Se o usuário pediu um estado, garantimos que o mapa
        // contenha apenas a região daquele estado.
        Map<String, MetricasRegiaoDTO> mapa = lista.stream()
                .collect(Collectors.toMap(
                        MetricasRegiaoDTO::getRegiao,
                        dto -> dto
                ));
    
        // Se houver filtro de estado, podemos filtrar o mapa final também
        if (estado != null && !estado.trim().isEmpty() && !vendas.isEmpty()) {
            // Pega a região do primeiro item da lista filtrada (já que todos são do mesmo estado)
            String regiaoFiltrada = vendas.get(0).getLogistica().getRegiao().toString();
            return ResponseEntity.ok(Map.of(regiaoFiltrada, mapa.get(regiaoFiltrada)));
        }
    
        return ResponseEntity.ok(mapa);
   }

No VendaRepository adicione o método de query JPA para filtrar vendas por intervalo de datas:

   List<Venda> findByDataVendaBetween(LocalDate startDate, LocalDate endDate);

No endpoint /reports/sales-summary adicione filtros opcionais por data de início e data fim:

   @Operation(
        summary = "Relatório de Vendas",
        description = "Retorna o resumo financeiro consolidado, com filtro opcional por período."
   )
   @GetMapping("/reports/sales-summary")
   public ResponseEntity<RelatorioFinanceiroDTO> getSalesSumary(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
    
        List<Venda> vendas;
    
        if (startDate != null && endDate != null) {
            // Conversão obrigatória: String -> LocalDate
            LocalDate dataInicio = LocalDate.parse(startDate);
            LocalDate dataFim = LocalDate.parse(endDate);
    
            vendas = vendaRepository.findByDataVendaBetween(dataInicio, dataFim);
        } else {
            vendas = vendaRepository.findAll();
        }
    
        return ResponseEntity.ok(vendaCalcularService.calculaFinanceiro(vendas));
   }

Preparação para Deploy com Docker

Empacotar a aplicação para facilitar a execução em qualquer ambiente

Limpa o projeto

   mvn clean

Limpa o projeto e verifica se o código compila e se não tem erros de sintaxe

   mvn clean compile

**Limpa, compila e empacota a aplicação dentro de um arquivo .jar

   mvn clean package -DskipTests

Faça um teste e observe os logs para ver se está tudo certo

   mvn test

Crie um arquivo Dockerfile na raíz do projeto e adicione:

   FROM maven:3.9.6-eclipse-temurin-17 AS build
   WORKDIR /app
   COPY pom.xml .

   RUN mvn dependency:go-offline
   COPY src ./src

   RUN mvn clean package -DskipTests

   FROM eclipse-temurin:17-jre-alpine
   WORKDIR /app

   COPY --from=build /app/target/*.jar app.jar
   EXPOSE 8080
   ENTRYPOINT ["java", "-jar", "app.jar"]

Crie um arquivo docker-compose.yml na raíz do projeto e adicione:

   services:
  db:
    image: mysql:8.0
    container_name: hanami-db
    restart: always
    environment:
      MYSQL_DATABASE: hanamiapidb
      MYSQL_ROOT_PASSWORD: root
    ports:
      - "3307:3306"

  app:
    build: .
    container_name: hanami-app
    ports:
      - "8080:8080"
    depends_on:
      - db
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/hanamiapidb?createDatabaseIfNotExist=true
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=root
      - SPRING_JPA_HIBERNATE_DDL_AUTO=update

Observação: Se quiser acessar o banco via Workbench/DBeaver enquanto o Docker roda, use a porta 3307

  • Nota: Para demonstração, as credenciais do banco estão no arquivo, mas em produção devem ser usadas em variáveis de ambiente.

Rode esse comando para buildar o Dockerfile e criar uma imagem com o nome projeto-apihanami

   docker build -t projeto-apihanami .

Lê o arquivo docker-compose.yml e inicia todos os serviços descritos nele

   docker compose up -d

Acesse a API via Swagger UI

   http://localhost:8080/docs

Limpando o ambiente. Para parar os contêiners e remover os volumes, use:

   docker-compose down -v

Check-list completo do projeto:

[X] Upload: Os arquivos com a extensão .CSV e .XLSX estão sendo aceitos corretamente? (Sim)

[X] Código: O projeto compila sem erros? (Sim)

[X] Testes: Os endpoints no Postman e Swagger batem com os resultados do SQL? (Sim)

[X] Documentação: O README reflete a realidade do código? (Sim)

[X] Swagger/OpenAPI: Os endpoints estão documentados? (Sim)

[X] Docker: A aplicação roda corretamente via Docker? (Sim)

[X] Logs: Mensagens de sucesso e de erro foram registradas? (Sim)

[X] Tratamento de Exceções: Se eu enviar um CSV corrompido ou com colunas erradas, a API retorna um erro claro (ex: 400 Bad Request) (Sim)

[X] Validação na lógica de negócio: Filtros de data e ordenação funcionando via Query Param

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors