📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Java e Spring Boot. Recomendado revisar antes de começar.

🚀 Controle de Gastos — Trabalho em Equipe Ágil

v4.0

Trilha de Aprendizado — Projeto 4 de 4

🎓 Nível profissional simulado: Sênior. A diferença de sênior para pleno raramente é “sabe mais sintaxe” — é saber lidar com ambiguidade. Aqui você não recebe uma lista de features prontas: recebe um cliente confuso, extrai requisitos você mesmo, decide o que fica de fora do escopo (e por quê), prioriza um backlog, e entrega através de um processo que outras pessoas conseguem revisar e confiar (branches, PRs, code review). Sênior pensa em produto e em time, não só em código.

Pré-requisito: Ter concluído o Projeto 3 (v3) — este projeto não reensina Spring Boot/HTMX do zero. O foco aqui é como um time de verdade decide o que construir e entrega isso com processo, não só “mais uma feature”.`

🎯 Objetivo

Nos projetos 1 a 3 você sempre recebeu a lista de features prontas (“implemente X”). Na vida real, ninguém te entrega isso pronto — alguém (um cliente, um product owner) descreve um problema de forma confusa, em linguagem natural, e é trabalho do time extrair os requisitos daí, organizar em um backlog, dividir em sprints e entregar via um fluxo de Git colaborativo (branches, Pull Requests, code review).

Este projeto simula esse ciclo completo, do zero ao deploy, evoluindo o código do Projeto 3 com 3 novas funcionalidades reais.

—`

🧑‍💼 Fase 1 — Levantamento de Requisitos

O Briefing do Cliente

Dona Marta é dona de uma papelaria de bairro e usa o Controle de Gastos (v3) há um mês. Ela te manda a seguinte mensagem, sem nenhuma organização técnica — é assim que requisitos chegam no mundo real:

“Oi! Então, tá funcionando bem o sisteminha, mas preciso de umas coisas. Primeiro: eu lanço tudo junto, contas de luz, compra de mercadoria, salário dos funcionários… fica uma bagunça só, queria separar por tipo de gasto sabe, tipo ‘Fornecedores’, ‘Contas Fixas’, ‘Salários’, essas coisas, e poder ver só de um tipo quando eu quiser. Segundo, meu contador vive pedindo uma planilha com tudo pra ele conferir no Excel dele, hoje eu tenho que copiar linha por linha, um Saco. Terceiro — e essa é importante — mês passado eu estourei o orçamento sem perceber, queria que o sistema me avisasse quando eu tô gastando demais no mês, tipo um alerta vermelho. Ah, e também queria trocar a cor do sistema pra roxo porque é a cor da minha loja, mas isso é só um desejo, não é urgente. Precisa ser rápido viu, meu computador da loja é meio lento.”

Técnica de Elicitação: Extraindo Requisitos do Briefing

Um briefing como esse nunca vira código diretamente. O primeiro passo de qualquer levantamento de requisitos é separar o que a Dona Marta disse em Requisitos Funcionais (o que o sistema deve fazer) e Requisitos Não-Funcionais (que qualidades o sistema deve ter).

Requisitos Funcionais (RF):

ID Requisito Origem no briefing
RF01 O sistema deve permitir classificar cada lançamento em uma categoria (ex.: Fornecedores, Contas Fixas, Salários) “queria separar por tipo de gasto”
RF02 O sistema deve permitir filtrar a lista de lançamentos por categoria “poder ver só de um tipo quando eu quiser”
RF03 O sistema deve exportar os lançamentos em um formato que abra no Excel “uma planilha com tudo pra ele conferir no Excel”
RF04 O sistema deve alertar visualmente quando os gastos do mês ultrapassarem um limite definido “me avisasse quando eu tô gastando demais”
RF05 (fora do escopo desta sprint) Permitir personalizar a cor do tema “trocar a cor do sistema pra roxo”

Requisitos Não-Funcionais (RNF):

ID Requisito Origem no briefing
RNF01 O sistema deve responder rapidamente mesmo em hardware modesto “meu computador da loja é meio lento”
RNF02 A exportação deve usar um formato universalmente compatível com Excel (CSV) “conferir no Excel dele”

💡 Por que separar RF de RNF? Um Requisito Funcional vira uma feature (algo que aparece no backlog como User Story). Um Requisito Não-Funcional geralmente não vira uma US isolada — ele vira uma restrição de qualidade que se aplica a todas as USs (ex.: RNF01 significa “toda query nova precisa ter índice/paginação”, não é uma tarefa separada).

RF05 foi explicitamente marcado como fora do escopo desta sprint — Dona Marta mesma disse “isso é só um desejo, não é urgente”. Saber dizer não agora (sem descartar para sempre) é parte do levantamento de requisitos: RF05 vai para o backlog do produto, mas não para a sprint atual.

Das Requisitos às User Stories

RF01-RF04 viram as User Stories que alimentam o backlog da Fase 2. Note que um requisito às vezes vira mais de uma US (RF01 e RF02 estão relacionados, mas RF01 é “guardar a categoria” e RF02 é “filtrar por ela” — são entregas incrementais separadas):

Requisito User Story
RF01 US02a — Categorizar lançamentos
RF02 US02b — Filtrar por categoria
RF03 + RNF02 US03 — Exportar lançamentos em CSV
RF04 US04 — Alerta de orçamento mensal

—`

📋 Fase 2 — Backlog do Produto e Sprint Planning

Product Backlog

ID User Story Prioridade
US01 (já entregue no Projeto 3) Como usuário, quero ver meu saldo atualizado em tempo real ✅ Feito
US02a Como Dona Marta, quero classificar cada lançamento em uma categoria, para organizar meus gastos por tipo Alta
US02b Como Dona Marta, quero filtrar a lista por categoria, para ver só os gastos de um tipo específico Alta
US03 Como Dona Marta, quero exportar os lançamentos em CSV, para conferir com meu contador no Excel Média
US04 Como Dona Marta, quero um alerta visual quando ultrapassar meu orçamento mensal, para não estourar o limite sem perceber Alta
RF05 (backlog futuro, fora desta sprint) Personalizar cor do tema Baixa

Sprint Planning

Duas sprints de uma “semana” cada (na prática, o tempo que você levar para implementar cada bloco):

Quadro Sprint (Kanban)

Copie esta tabela para acompanhar seu progresso — mova cada linha de coluna conforme avança (na prática, edite o Markdown ou mantenha um arquivo SPRINT_BOARD.md no seu repositório):

User Story To Do Doing Done
US02a — Categorizar      
US02b — Filtrar por categoria      
US03 — Exportar CSV      
US04 — Alerta de orçamento      

💡 Por que Markdown e não um Trello de verdade? Um quadro real (Trello, Jira, GitHub Projects) é essencial em equipe, mas para um projeto autoguiado individual, manter o board dentro do próprio repositório (versionado junto com o código) evita depender de conta externa e mantém tudo no mesmo lugar. Em um time real, use a ferramenta que a empresa usar — o conceito (colunas de status, uma US por vez em “Doing”) é o mesmo.

—`

🌿 Fase 3 — Fluxo de Trabalho em Equipe (Git Flow)

Em uma equipe real, ninguém commita direto na main. Cada User Story vira uma branch, passa por um Pull Request revisado por outra pessoa, e só então é mesclada. Como você está sozinho neste projeto, vai simular os dois papéis: quem abre a PR (autor) e quem revisa (revisor) — na prática, você mesmo vai preencher o checklist de revisão antes de mesclar.

Estratégia de Branches

# Sempre parta de uma main atualizada
git checkout main
git pull origin main

# Crie a branch da US que vai trabalhar
git checkout -b feature/US02a-categoria

Passo a Passo por User Story

  1. Criar a branch (git checkout -b feature/US0Xx-nome).
  2. Implementar a mudança (código na Fase 4).
  3. Commitar com mensagem semântica — prefixo feat:, fix:, docs: etc., descrevendo o porquê, não só o o quê:
    git add .
    git commit -m "feat: adiciona campo categoria ao lancamento (US02a)"
    
  4. Enviar a branch para o GitHub:
    git push -u origin feature/US02a-categoria
    
  5. Abrir o Pull Request no GitHub (botão “Compare & pull request” aparece automaticamente após o push), usando o template abaixo.
  6. Auto-revisar com o checklist (Fase 3, seção seguinte) antes de aprovar.
  7. Mesclar (squash merge) pela interface do GitHub, e apagar a branch:
    git checkout main
    git pull origin main
    git branch -d feature/US02a-categoria
    
  8. Mover a US para “Done” no quadro Sprint.

Repita esse ciclo para cada User Story da sprint.

Template de Pull Request

Use este modelo na descrição de cada PR que você abrir (cole no corpo do PR no GitHub):

## O que mudou
<!-- Resumo de 1-2 frases da mudança -->`

## User Story
US0Xx — <descrição da US>`

## Como testar
1.
2.`

## Checklist
- [ ] Código compila sem warnings novos
- [ ] `./mvnw test` passa (adicionei teste novo se introduzi regra de negócio)
- [ ] Testei manualmente o fluxo feliz
- [ ] Testei um caso de erro/borda
- [ ] Não deixei código comentado/de debug
- [ ] Se tomei uma decisão técnica relevante, registrei um ADR

Checklist de Code Review

Antes de aprovar (mesmo revisando seu próprio código), pergunte-se:

—`

🛠️ Fase 4 — Implementação das User Stories

US02a + US02b — Categorizar e Filtrar Lançamentos

1. Nova categoria no modelo — crie src/main/java/br/com/controledegastos/model/Categoria.java:

package br.com.controledegastos.model;

public enum Categoria {
    FORNECEDORES,
    CONTAS_FIXAS,
    SALARIOS,
    OUTROS
}

2. Adicione o campo em Lancamento.java:

@Enumerated(EnumType.STRING)
@NotNull(message = "A categoria é obrigatória")
private Categoria categoria;

// Getter e Setter
public Categoria getCategoria() { return categoria; }
public void setCategoria(Categoria categoria) { this.categoria = categoria; }

Nota: como spring.jpa.hibernate.ddl-auto=update, a nova coluna é criada automaticamente no H2/PostgreSQL na próxima subida da aplicação — não precisa de migração manual neste projeto didático (em um projeto real com Flyway/Liquibase, isso viraria um script de migração versionado).

3. Filtro no repositório — em LancamentoRepository.java:

Page<Lancamento> findByCategoria(Categoria categoria, Pageable pageable);

4. Controller (LancamentoController.java) — adicione o parâmetro opcional de categoria:

private void carregarDados(Model model, int page, Categoria filtroCategoria) {
    Pageable pageable = PageRequest.of(page, 5, Sort.by("data").descending());
    Page<Lancamento> lancamentosPage = filtroCategoria != null
            ? lancamentoRepository.findByCategoria(filtroCategoria, pageable)
            : lancamentoRepository.findAll(pageable);

    // ... resto do método igual, mais:
    model.addAttribute("filtroCategoria", filtroCategoria);
    model.addAttribute("categorias", Categoria.values());
}

Atualize a assinatura de index() para receber @RequestParam(required = false) Categoria categoria e repassar para carregarDados(model, page, categoria). Não esqueça de propagar o mesmo parâmetro nos outros métodos que chamam carregarDados (addLancamento, deleteLancamento, updateLancamento), senão o filtro “reseta” sozinho depois de qualquer ação.

5. Formulário e filtro na view (index.html) — adicione um <select> de categoria no formulário de novo lançamento (igual ao <select> de tipo que já existe) e um filtro no topo da lista:

<form hx-get="/" hx-target="body">
    <select name="categoria">
        <option value="">Todas as categorias</option>
        <option th:each="cat : ${categorias}" th:value="${cat}" th:text="${cat}" th:selected="${cat == filtroCategoria}"></option>
    </select>
    <button type="submit">Filtrar</button>
</form>

US03 — Exportar Lançamentos em CSV

Novo endpoint que gera um arquivo CSV para download, atendendo RF03 + RNF02 (formato universal, compatível com Excel):

Repare que a lógica de gerar CSV é regra de negócio pura (não depende de HTTP nem de banco) — se ela ficar dentro do @Controller, fica difícil testar sem subir o contexto Spring inteiro. Um dev pleno faria funcionar; um dev sênior já extrai isso para uma classe testável antes de terminar a US (ver seção “Refatorando para Testabilidade” logo abaixo). O controller fica assim, já chamando o service:

@Autowired
private RelatorioService relatorioService;

@GetMapping("/lancamentos/exportar")
public ResponseEntity<String> exportarCsv() {
    String csv = relatorioService.gerarCsv(lancamentoRepository.findAll());
    return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename=lancamentos.csv")
            .header("Content-Type", "text/csv; charset=UTF-8")
            .body(csv);
}

💡 Por que ; e não , como separador? O Excel em configuração de idioma Português (Brasil) usa , como separador decimal (19,90), então interpreta , como separador de coluna e ; como delimitador de campo — usar , faria valores decimais quebrarem em duas colunas.

Adicione um botão <a href="/lancamentos/exportar">📥 Exportar CSV</a> na tela principal.


US04 — Alerta de Orçamento Mensal

1. Limite configurável — em application.properties:

app.orcamento-mensal=2000.00

2. Cálculo do gasto do mês atual e comparação com o limite — também vai para o RelatorioService (ver próxima seção), chamado a partir do carregarDados:

@Value("${app.orcamento-mensal}")
private BigDecimal orcamentoMensal;

// dentro de carregarDados(...):
BigDecimal gastoMesAtual = relatorioService.calcularGastoDoMes(lancamentoRepository.findAll(), YearMonth.now());

model.addAttribute("gastoMesAtual", gastoMesAtual);
model.addAttribute("orcamentoEstourado", relatorioService.orcamentoEstourado(gastoMesAtual, orcamentoMensal));

3. Alerta visual no index.html:

<div th:if="${orcamentoEstourado}" class="alerta-orcamento"> ⚠️ Atenção! Você já gastou <span th:text="${gastoMesAtual}"></span> este mês,
    acima do limite de <span th:text="${orcamentoMensal}"></span>.
</div>

Adicione uma classe .alerta-orcamento no CSS (fundo vermelho claro, texto vermelho escuro) para o alerta chamar atenção de verdade.

—`

🧹 Refatorando para Testabilidade (Clean Code)

Antes de fechar US03/US04, extraia a lógica de CSV e de cálculo de orçamento do controller para uma classe própria — RelatorioService. Isso é Single Responsibility na prática: o controller cuida de HTTP (rotas, parâmetros, status code), o service cuida de regra de negócio (matemática, formatação). Como bônus direto, o service vira testável sem precisar subir o Spring inteiro.

package br.com.controledegastos.service;

import br.com.controledegastos.model.Lancamento;
import br.com.controledegastos.model.TipoLancamento;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.YearMonth;
import java.util.Comparator;
import java.util.List;

@Service
public class RelatorioService {

    public String gerarCsv(List<Lancamento> lancamentos) {
        List<Lancamento> ordenados = lancamentos.stream()
                .sorted(Comparator.comparing(Lancamento::getData))
                .toList();

        StringBuilder csv = new StringBuilder("Data;Descricao;Categoria;Tipo;Valor\n");
        for (Lancamento l : ordenados) {
            csv.append(l.getData()).append(";")
               .append(l.getDescricao()).append(";")
               .append(l.getCategoria()).append(";")
               .append(l.getTipo()).append(";")
               .append(l.getValor()).append("\n");
        }
        return csv.toString();
    }

    public BigDecimal calcularGastoDoMes(List<Lancamento> lancamentos, YearMonth mes) {
        return lancamentos.stream()
                .filter(l -> l.getTipo() == TipoLancamento.DESPESA)
                .filter(l -> YearMonth.from(l.getData()).equals(mes))
                .map(Lancamento::getValor)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public boolean orcamentoEstourado(BigDecimal gastoDoMes, BigDecimal limite) {
        return gastoDoMes.compareTo(limite) > 0;
    }
}

💡 Como saber o que extrair? Regra prática: se um método não usa nada de HttpServletRequest/Model/anotações do Spring MVC e só transforma dados de entrada em dados de saída, ele não deveria estar no controller. calcularGastoDoMes e gerarCsv não sabem (nem precisam saber) que existe uma URL /lancamentos/exportar — isso é o sinal de que pertencem a outra camada.

—`

🧭 Decisões Técnicas (ADRs)

Um ADR (Architecture Decision Record) é um registro curto de uma decisão técnica: o contexto, o que foi decidido, e as consequências (inclusive as negativas). Sênior não é quem nunca aceita dívida técnica — é quem documenta a dívida em vez de escondê-la. Guarde estes registros em docs/adr/ no seu repositório (0001-categoria-como-enum.md, etc.):

ADR 001 — Categoria como Enum fixo, não uma entidade própria

ADR 002 — Squash merge como estratégia de integração

ADR 003 — ddl-auto=update mantido como dívida técnica aceita

—`

🧪 Testes Automatizados

Com a lógica extraída para RelatorioService, testá-la é rápido e não depende de banco de dados nem de contexto Spring — são testes de unidade puros. Crie src/test/java/br/com/controledegastos/service/RelatorioServiceTest.java:

package br.com.controledegastos.service;

import br.com.controledegastos.model.Categoria;
import br.com.controledegastos.model.Lancamento;
import br.com.controledegastos.model.TipoLancamento;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class RelatorioServiceTest {

    private final RelatorioService service = new RelatorioService();

    private Lancamento criarLancamento(TipoLancamento tipo, BigDecimal valor, LocalDate data) {
        Lancamento l = new Lancamento();
        l.setDescricao("Teste");
        l.setTipo(tipo);
        l.setValor(valor);
        l.setData(data);
        l.setCategoria(Categoria.OUTROS);
        return l;
    }

    @Test
    void calcularGastoDoMes_deveSomarSoDespesasDoMesInformado() {
        List<Lancamento> lancamentos = List.of(
                criarLancamento(TipoLancamento.DESPESA, new BigDecimal("100.00"), LocalDate.of(2026, 3, 5)),
                criarLancamento(TipoLancamento.DESPESA, new BigDecimal("50.00"), LocalDate.of(2026, 3, 20)),
                criarLancamento(TipoLancamento.RECEITA, new BigDecimal("999.00"), LocalDate.of(2026, 3, 10)), // não deve contar
                criarLancamento(TipoLancamento.DESPESA, new BigDecimal("30.00"), LocalDate.of(2026, 2, 15))   // mês diferente
        );

        BigDecimal gasto = service.calcularGastoDoMes(lancamentos, YearMonth.of(2026, 3));

        assertEquals(new BigDecimal("150.00"), gasto);
    }

    @Test
    void orcamentoEstourado_deveRetornarTrueQuandoGastoUltrapassaLimite() {
        assertTrue(service.orcamentoEstourado(new BigDecimal("2500.00"), new BigDecimal("2000.00")));
    }

    @Test
    void orcamentoEstourado_deveRetornarFalseQuandoGastoIgualAoLimite() {
        // Regra de negócio: só estoura quando ULTRAPASSA, não quando iguala
        assertFalse(service.orcamentoEstourado(new BigDecimal("2000.00"), new BigDecimal("2000.00")));
    }

    @Test
    void gerarCsv_devePreservarOsDadosDoLancamentoNaLinha() {
        Lancamento l = criarLancamento(TipoLancamento.DESPESA, new BigDecimal("42.50"), LocalDate.of(2026, 1, 1));
        l.setDescricao("Papel A4");

        String csv = service.gerarCsv(List.of(l));

        assertTrue(csv.contains("Papel A4"));
        assertTrue(csv.contains("42.50"));
        assertTrue(csv.startsWith("Data;Descricao;Categoria;Tipo;Valor"));
    }
}

💡 Por que testar orcamentoEstourado(2000.00, 2000.00) == false? Esse é um caso de borda (edge case) — “estourar” o orçamento deveria significar ultrapassar, não igualar. Um teste assim documenta a regra de negócio de forma inequívoca: se alguém “corrigir” o > para >= no futuro achando que é a mesma coisa, o teste quebra e avisa que essa não era a intenção original. É clean code funcionando como documentação viva.

Rode ./mvnw test antes de cada git push — nenhuma PR deveria ser aberta com testes quebrando.


—`

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd javaweb_gastos_04_htmx

2. Execute a aplicação e os testes

./mvnw spring-boot:run
./mvnw test

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/javaweb_gastos_04_htmx`

✅ Checkpoint Final

Antes de considerar a sprint concluída:

  1. As 4 User Stories (US02a, US02b, US03, US04) estão em branches próprias, cada uma com PR mesclada via squash.
  2. O quadro Sprint está com todas as linhas em “Done”.
  3. ./mvnw test passa sem falhas — os testes de RelatorioServiceTest cobrem a regra de negócio nova.
  4. Testou manualmente: categorizar um lançamento, filtrar por categoria, exportar CSV e abrir no Excel/LibreOffice, e forçar o orçamento a estourar para ver o alerta aparecer.
  5. As 3 decisões técnicas (ADR 001-003) estão registradas em docs/adr/ no repositório.
  6. git log --oneline main mostra o histórico de merges — cada commit conta a história de uma decisão de produto, não só uma mudança de código.

—`

🏆 Conclusão da Trilha (4 de 4)

Parabéns! Você completou os quatro projetos da série Spring Boot + HTMX — do CRUD mais simples até um ciclo completo de descoberta de produto e entrega em equipe:

Versão Nível simulado Foco O que ainda falta (chega no próximo nível)
v1 Estagiário Fundamentos guiados: CRUD, H2, Docker, Render + Neon Padrões de design, testes, decisões de arquitetura
v2 Júnior Qualidade de código: SOLID, DRY, fragmentos HTMX Contrato de API, validação de entrada, escala
v3 Pleno Produto: Paginação, Validation, API REST/Swagger, primeira dose de Agile Extrair requisitos sozinho, testes automatizados, ADRs
v4 Sênior Processo: levantamento de requisitos, backlog, sprints, Git Flow (branch/PR/review/merge), testes, ADRs

O que diferencia um desenvolvedor júnior de um pleno raramente é saber mais sintaxe — é saber transformar um pedido confuso em requisitos claros, priorizar o que entregar primeiro, e trabalhar em um fluxo que outras pessoas conseguem revisar e confiar.

—`

📋 Resumo de Comandos

# Fluxo de branch por User Story
git checkout main
git pull origin main
git checkout -b feature/US0Xx-nome-curto

# Commit semântico
git add .
git commit -m "feat: descricao da mudanca (US0Xx)"

# Publicar a branch e abrir PR no GitHub
git push -u origin feature/US0Xx-nome-curto

# Após a PR ser mesclada (squash) pela interface do GitHub
git checkout main
git pull origin main
git branch -d feature/US0Xx-nome-curto

# Rodar os testes automatizados antes de cada push
./mvnw test

# Rodar localmente antes de abrir a PR
./mvnw spring-boot:run

# Testar a exportacao CSV
curl -o lancamentos.csv http://localhost:8080/lancamentos/exportar