Sistema backend para gerenciamento de estacionamento: controle de vagas, entrada/saída de veículos e cálculo de receita por setor.
- Java 21
- Spring Boot 4.x
- Hibernate / Spring Data JPA
- MySQL 8
- Docker
- Maven (Wrapper incluso)
- Java 21 instalado
- Docker Desktop instalado e rodando
- Portas
3000,3003e3306disponíveis na máquina
┌─────────────────────────────────────────────────────┐
│ Docker │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ garage-sim │ │ garage-mysql │ │
│ │ :3000 │ │ :3306 │ │
│ └──────┬───────┘ └──────────────────────────┘ │
│ │ webhook POST localhost:3003 │
└─────────┼───────────────────────────────────────────┘
│
┌─────────▼────────────────────────────────────────────┐
│ Aplicação Spring Boot (rodando no host) │
│ :3003 │
│ - GET localhost:3000/garage (bootstrap) │
│ - POST /webhook (recebe eventos) │
│ - GET /revenue (consulta receita) │
└──────────────────────────────────────────────────────┘
Importante: a aplicação deve rodar fora do Docker (
mvnw spring-boot:run) porque o simulador envia webhooks paralocalhost:3003de forma hardcoded, ignorando variáveis de ambiente.
docker network create garage-netSe já existir, ignore o erro.
docker run -d \
--name garage-mysql \
--network garage-net \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=garage \
-p 3306:3306 \
mysql:8.0Se o container já existir:
docker start garage-mysql
Aguarde ~15 segundos para o MySQL inicializar completamente antes do próximo passo.
docker run -d \
--name garage-sim \
--network garage-net \
--add-host=localhost:host-gateway \
-p 3000:3000 \
cfontes0estapar/garage-sim:1.0.0O flag --add-host=localhost:host-gateway faz com que localhost dentro do container resolva para o IP da máquina host, onde a aplicação estará rodando.
Se o container já existir:
docker start garage-sim
# Windows CMD
mvnw spring-boot:run
# Windows PowerShell
.\mvnw spring-boot:run
# Linux / macOS
./mvnw spring-boot:runA aplicação sobe na porta 3003. No startup, ela busca automaticamente a configuração da garagem do simulador e persiste as vagas no banco.
Log esperado:
Bootstrap OK: inserted 30 spots from simulator.
Após a aplicação subir, chame o endpoint do simulador para iniciar o envio de webhooks:
curl http://localhost:3000/garageA partir desse momento o simulador começará a enviar eventos ENTRY, PARKED e EXIT automaticamente para http://localhost:3003/webhook.
Se precisar reiniciar do zero (limpar banco + reiniciar simulador):
# Limpa o banco
docker exec -it garage-mysql mysql -u root -proot garage -e \
"SET FOREIGN_KEY_CHECKS=0; \
TRUNCATE TABLE parking_session_events; \
TRUNCATE TABLE parking_sessions; \
TRUNCATE TABLE parking_spots; \
SET FOREIGN_KEY_CHECKS=1;"
# Reinicia o simulador
docker restart garage-simDepois pare e suba a aplicação novamente (Ctrl+C + mvnw spring-boot:run), e chame curl http://localhost:3000/garage para iniciar a simulação.
POST http://localhost:3003/webhook
{
"license_plate": "ZUL0001",
"entry_time": "2025-01-01T12:00:00.000Z",
"event_type": "ENTRY"
}{
"license_plate": "ZUL0001",
"lat": -23.561684,
"lng": -46.655981,
"event_type": "PARKED"
}{
"license_plate": "ZUL0001",
"exit_time": "2025-01-01T13:30:00.000Z",
"event_type": "EXIT"
}Todos retornam HTTP 200 em caso de sucesso.
GET http://localhost:3003/revenue?sector=A&date=2025-01-01
{
"amount": 150.00,
"currency": "BRL",
"timestamp": "2025-01-01T12:00:00.000Z"
}Também aceita requisição via body:
curl -X GET http://localhost:3003/revenue \
-H "Content-Type: application/json" \
-d '{"sector": "A", "date": "2025-01-01"}'- Primeiros 30 minutos são gratuitos
- Após 30 minutos, cobra-se uma tarifa por hora com arredondamento para cima (ceiling)
- O valor base por hora vem do campo
basePriceconfigurado por setor no simulador
Exemplo: entrada às 00:00, saída às 02:00
- 120 minutos total − 30 minutos gratuitos = 90 minutos faturáveis
- Arredondado para cima = 2 horas
- Setor A (base R$ 40,50) com ocupação baixa (×0,90) = R$ 72,90
O multiplicador é calculado com base na ocupação do setor no momento do PARKED:
| Ocupação do setor | Multiplicador |
|---|---|
| < 25% | 0.90 (desconto de 10%) |
| 25% a 50% | 1.00 (sem alteração) |
| 50% a 75% | 1.10 (acréscimo de 10%) |
| 75% a 100% | 1.25 (acréscimo de 25%) |
- Com 100% de ocupação no setor, novas entradas são recusadas com
HTTP 409até que uma vaga seja liberada
- Eventos duplicados (mesma placa + tipo + timestamp) são ignorados silenciosamente via constraint único na tabela
parking_session_events
# Windows CMD/PowerShell
mvnw test
# Linux / macOS
./mvnw test26 testes cobrindo:
- Eventos ENTRY, PARKED e EXIT (sucesso e falhas)
- Idempotência de eventos duplicados
- Cálculo de cobrança (gratuidade, arredondamento, multiplicadores)
- Preço dinâmico nas 4 faixas de ocupação
- Casos de borda: placa nula,
event_typeinválido, timestamps com e sem timezone, setor lotado
Definidas em src/main/resources/application.properties:
| Propriedade | Padrão | Descrição |
|---|---|---|
server.port |
3003 |
Porta da aplicação |
spring.datasource.url |
jdbc:mysql://localhost:3306/garage |
URL do banco |
spring.datasource.username |
root |
Usuário do banco |
spring.datasource.password |
root |
Senha do banco |
garage.simulator.base-url |
http://localhost:3000 |
URL base do simulador |
garage.bootstrap.enabled |
true |
Habilita carga inicial das vagas |
spring.jpa.hibernate.ddl-auto |
update |
Gerenciamento do schema (cria/atualiza tabelas automaticamente) |
- O simulador (
garage-sim) tem a URL do webhook hardcoded comohttp://localhost:3003/webhooke ignora a variável de ambienteEXTERNAL_API_URL. Por isso a aplicação precisa rodar no host e não em container. - O
ddl-auto=updategarante que alterações na entidade (como tornar colunas nullable) sejam aplicadas automaticamente no banco ao subir a aplicação. - O campo
price_multiplier_appliedé gravado no evento PARKED (não no ENTRY) pois a ocupação do setor só é relevante quando o veículo efetivamente ocupa a vaga. - Os campos
spot_ideprice_multiplier_appliedsão nullable na sessão pois no evento ENTRY ainda não se sabe qual vaga será ocupada nem qual multiplicador será aplicado.