diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ee2e497 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +### Overview + +OrderFlow Commerce is a monorepo with two services: + +| Service | Path | Tech | Dev Port | +|---------|------|------|----------| +| API (backend) | `api/` | Spring Boot 3.2 / Java 21 / Maven | 8080 | +| Web (frontend) | `web/` | React 19 / Vite 8 / TypeScript | 5173 | + +Infrastructure (PostgreSQL 15, RabbitMQ 3) runs via Docker Compose. + +### Starting infrastructure + +```bash +sudo dockerd &>/tmp/dockerd.log & # if Docker daemon is not already running +sudo docker compose up -d postgres rabbitmq +``` + +Wait for both containers to become healthy before starting the API. + +### Running the API locally + +The default `application.properties` uses Docker Compose hostnames (`postgres`, `rabbitmq`). For local dev outside Docker, use the `local` Spring profile (which reads `application-local.properties` with `localhost` hostnames): + +```bash +cd api && ./mvnw spring-boot:run -Dspring-boot.run.profiles=local +``` + +### Running the Web frontend + +```bash +cd web && npm run dev +``` + +Vite proxies `/api` requests to `http://localhost:8080` (see `vite.config.ts`). + +### Tests and Lint + +- **API tests**: `cd api && ./mvnw test` +- **Web lint**: `cd web && npm run lint` +- **Web build**: `cd web && npm run build` + +### Key endpoints when running + +- API: http://localhost:8080 +- Swagger UI: http://localhost:8080/swagger-ui/index.html +- Web: http://localhost:5173 +- RabbitMQ Management: http://localhost:15672 (user: `orderflow` / pass: `orderflow123`) + +### Full Docker development (with hot reload) + +```bash +sudo docker compose up --build +``` + +This starts all services with hot reload enabled: +- **API**: `dev-entrypoint.sh` uses `inotifywait` to watch `src/` for `.java`/`.properties`/`.xml` changes, triggers `mvn compile`, and DevTools auto-restarts the app (~0.5s). +- **Web**: Runs Vite dev server (not nginx) with full HMR via volume-mounted source files. +- **Debug port**: JDWP on port 5005 (controlled by `SPRING_BOOT_JVM_ARGS` env var). + +The `vite.config.ts` reads `API_PROXY_TARGET` env var (defaults to `http://localhost:8080`). In Docker Compose it's set to `http://app:8080`. + +### Non-obvious notes + +- Security is currently set to `permitAll()` — no auth required for any endpoint. +- The `mvnw` wrapper must be `chmod +x` before first use (it's committed without execute bit). +- Hibernate `ddl-auto=update` auto-creates schema on first run — no migration step needed. +- The `application-local.properties` file (added for Cloud dev) overrides DB/RabbitMQ hosts to `localhost`. If you need to run the API inside Docker Compose, use `-Dspring-boot.run.profiles=docker` instead. +- `spring-boot-devtools` is enabled: the API auto-restarts on class changes, but not on `pom.xml` changes. +- The original `web/Dockerfile` is for production (build + nginx). `web/Dockerfile.dev` is for development with HMR. diff --git a/README.md b/README.md index 29d4937..0482d43 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,490 @@ -# Monorepo — frontend e API -ChatGPT Image 30 de abr  de 2026, 13_52_47 (1) +# OrderFlow Commerce -Este repositório contém: +

+ OrderFlow Commerce +

-| Pasta | Descrição | -|-------|-----------| -| [`api/`](api/) | Spring Boot (OrderFlow Commerce) | -| [`web/`](web/) | Frontend Vite + React | +

+ + + + + + + +

-## Subir tudo com Docker +

+ Event-driven e-commerce platform — plataforma de e-commerce orientada a eventos
+ Built with Java 21, Spring Boot, RabbitMQ, and React +

-Na **raiz** do repositório: +--- + +## What is OrderFlow? · O que é o OrderFlow? + +OrderFlow Commerce is an **event-driven e-commerce REST API** that processes orders asynchronously. +When a customer checks out, an `OrderCreated` event is published to RabbitMQ — inventory reservation and email notifications happen **in the background**, without blocking the client. + +O OrderFlow demonstra uma arquitetura **moderna e escalável**: mensageria assíncrona, API RESTful documentada com Swagger, e um monorepo com frontend React + backend Spring Boot, tudo orquestrado via Docker Compose com **hot reload** para desenvolvimento. + +--- + +## Architecture · Arquitetura + +> Diagramas seguem o [**C4 Model**](https://c4model.com/) — do contexto geral até o código. + +### C1 — System Context · Contexto do Sistema + +*Who interacts with the system? What are the external dependencies?* + +```mermaid +graph TB + subgraph External + USER["🧑 User / Cliente"] + PG["🐘 PostgreSQL 15
Relational Database"] + RMQ["🐇 RabbitMQ 3
Message Broker"] + end + + subgraph OrderFlow["⚡ OrderFlow Commerce"] + API["Spring Boot API
REST + Event Publishing"] + WEB["React SPA
Vite + TypeScript + Tailwind"] + end + + USER -- "HTTP / Browser" --> WEB + WEB -- "REST /api/*" --> API + API -- "JDBC" --> PG + API -- "AMQP" --> RMQ + RMQ -- "consume events" --> API + + style OrderFlow fill:#1a1a2e,stroke:#16213e,color:#eee + style API fill:#6DB33F,stroke:#4a8a2a,color:#fff + style WEB fill:#61DAFB,stroke:#3a8fb7,color:#000 + style PG fill:#4169E1,stroke:#2a4494,color:#fff + style RMQ fill:#FF6600,stroke:#cc5200,color:#fff + style USER fill:#f5f5f5,stroke:#999,color:#333 +``` + +### C2 — Containers · Contêineres + +*What applications/services run? How do they communicate?* + +```mermaid +graph LR + subgraph Docker Compose + direction TB + + PG[("🐘 PostgreSQL 15
Port 5432
orderflow DB")] + RMQ["🐇 RabbitMQ 3
AMQP 5672 · UI 15672
Exchange: order.events"] + + subgraph API["⚡ orderflow-app · Port 8080"] + direction TB + REST["REST Controllers
/categories · /products · /test"] + PUB["OrderEventPublisher
Publishes OrderCreated"] + INV["InventoryConsumer
Queue: order.inventory"] + EMAIL["EmailConsumer
Queue: order.email"] + end + + subgraph WEB["🌐 orderflow-web · Port 5173"] + VITE["Vite Dev Server
React 19 + Tailwind 4"] + end + end + + VITE -- "/api/* proxy" --> REST + REST -- "JPA / Hibernate" --> PG + PUB -- "AMQP publish
routing key: order.created" --> RMQ + RMQ -- "consume" --> INV + RMQ -- "consume" --> EMAIL + + style PG fill:#4169E1,stroke:#2a4494,color:#fff + style RMQ fill:#FF6600,stroke:#cc5200,color:#fff + style API fill:#1b4332,stroke:#2d6a4f,color:#eee + style WEB fill:#0d1b2a,stroke:#1b3a5c,color:#eee + style REST fill:#6DB33F,stroke:#4a8a2a,color:#fff + style PUB fill:#81b29a,stroke:#588b76,color:#000 + style INV fill:#e07a5f,stroke:#c25a3f,color:#fff + style EMAIL fill:#e07a5f,stroke:#c25a3f,color:#fff + style VITE fill:#61DAFB,stroke:#3a8fb7,color:#000 +``` + +### C3 — Components · Componentes da API + +*Internal modules of the Spring Boot application.* + +```mermaid +graph TB + subgraph Controllers["🎯 Controllers (REST)"] + CC["CategoryController
/categories"] + PC["ProductController
/products"] + TC["TestController
/test"] + end + + subgraph Messaging["📨 Messaging (RabbitMQ)"] + OEP["OrderEventPublisher"] + IC["InventoryConsumer"] + EC["EmailConsumer"] + RMC["RabbitMQConfig
Exchange + Queues + Bindings"] + end + + subgraph Data["💾 Data Layer"] + CR["CategoryRepository"] + PR["ProductRepository"] + OR["OrderRepository"] + OIR["OrderItemRepository"] + end + + subgraph Entities["📦 Domain Entities"] + CAT["Category"] + PROD["Product"] + ORD["Order"] + OI["OrderItem"] + end + + subgraph Config["⚙️ Cross-Cutting"] + SC["SecurityConfig
CSRF off · Stateless · permitAll"] + OAC["OpenApiConfig
Swagger UI"] + GEH["GlobalExceptionHandler
400 · 404 · 500"] + end + + CC --> CR --> CAT + PC --> PR --> PROD + TC --> OEP + OEP --> RMC + CR --> CAT + PR --> PROD + OR --> ORD + OIR --> OI + PROD -.->|"@ManyToOne"| CAT + OI -.->|"@ManyToOne"| ORD + OI -.->|"@ManyToOne"| PROD + + style Controllers fill:#6DB33F,stroke:#4a8a2a,color:#fff + style Messaging fill:#FF6600,stroke:#cc5200,color:#fff + style Data fill:#4169E1,stroke:#2a4494,color:#fff + style Entities fill:#2d6a4f,stroke:#1b4332,color:#fff + style Config fill:#555,stroke:#333,color:#eee +``` + +### C4 — Entity Relationship · Modelo de Dados + +```mermaid +erDiagram + tb_category { + bigint id PK "IDENTITY" + varchar name UK "NOT NULL" + } + + tb_product { + bigint id PK "IDENTITY" + varchar name "NOT NULL" + text description + decimal price "NOT NULL" + int stock_quantity + bigint category_id FK + } + + tb_order { + bigint id PK "IDENTITY" + timestamp created_at "auto @PrePersist" + varchar status "PENDING | CONFIRMED | SHIPPED | DELIVERED | CANCELLED" + decimal total "sum(items)" + } + + tb_order_item { + bigint id PK "IDENTITY" + int quantity + decimal unit_price "snapshot do preco" + bigint order_id FK "NOT NULL" + bigint product_id FK "NOT NULL" + } + + tb_category ||--o{ tb_product : "has many" + tb_product ||--o{ tb_order_item : "referenced by" + tb_order ||--o{ tb_order_item : "contains" +``` + +--- + +## Event Flow · Fluxo de Eventos + +```mermaid +sequenceDiagram + actor Client + participant API as Spring Boot API + participant PG as PostgreSQL + participant RMQ as RabbitMQ + participant INV as InventoryConsumer + participant MAIL as EmailConsumer + + Client->>API: GET /test/publish-sample-order + API->>RMQ: publish OrderCreatedEvent
exchange: order.events
routing key: order.created + + par Async Processing + RMQ->>INV: queue: order.inventory + INV->>INV: Reserve stock (simulated) + and + RMQ->>MAIL: queue: order.email + MAIL->>MAIL: Send confirmation (simulated) + end + + API-->>Client: 200 { published: true, orderId: 1000 } +``` + +--- + +## REST API Endpoints + +| Method | Endpoint | Description | Request Body | +|--------|----------|-------------|--------------| +| `GET` | `/categories` | List all categories | — | +| `GET` | `/categories/{id}` | Get category by ID | — | +| `POST` | `/categories` | Create category | `{ "name": "..." }` | +| `PUT` | `/categories/{id}` | Update category | `{ "name": "..." }` | +| `DELETE` | `/categories/{id}` | Delete category | — | +| `GET` | `/products` | List all products | — | +| `GET` | `/products/{id}` | Get product by ID | — | +| `POST` | `/products` | Create product | `{ "name", "description", "price", "stockQuantity", "category": {"id": n} }` | +| `PUT` | `/products/{id}` | Update product | same as create | +| `DELETE` | `/products/{id}` | Delete product | — | +| `GET` | `/test/ping` | Health check | — | +| `GET` | `/test/publish-sample-order` | Publish test event to RabbitMQ | — | + +> 📖 **Interactive docs:** [Swagger UI](http://localhost:8080/swagger-ui/index.html) (after starting the API) + +--- + +## Tech Stack + +| Layer | Technology | Purpose | +|-------|-----------|---------| +| **Language** | Java 21 | Backend runtime | +| **Framework** | Spring Boot 3.2 | REST API, DI, auto-config | +| **Database** | PostgreSQL 15 | Persistent storage | +| **ORM** | Spring Data JPA / Hibernate 6 | Object-relational mapping | +| **Messaging** | RabbitMQ 3 | Async event processing (AMQP) | +| **Security** | Spring Security | Auth framework (currently `permitAll`) | +| **API Docs** | SpringDoc OpenAPI 2.5 | Swagger UI generation | +| **Frontend** | React 19 + TypeScript | Single Page Application | +| **Styling** | Tailwind CSS 4 | Utility-first CSS | +| **Build (FE)** | Vite 8 | Dev server + bundler with HMR | +| **Build (BE)** | Maven (wrapper) | Dependency management + build | +| **DevOps** | Docker Compose | Local orchestration | +| **Testing** | JUnit 5 | Unit tests | +| **Dev Tools** | spring-boot-devtools + inotifywait | Hot reload in Docker | + +--- + +## Quick Start · Como Rodar + +### Prerequisites · Pré-requisitos + +- **Docker Engine 24+** & **Docker Compose** +- **Git** + +### Option 1: Docker Compose (recommended · recomendado) ```bash +git clone https://github.com/dbfcode/commerce-async-platform.git +cd commerce-async-platform + docker compose up --build ``` -- API: http://localhost:8080 -- Swagger: http://localhost:8080/swagger-ui.html -- Web (nginx): http://localhost:4173 -- RabbitMQ UI: http://localhost:15672 +| Service | URL | Credentials | +|---------|-----|-------------| +| **API** | http://localhost:8080 | — | +| **Swagger UI** | http://localhost:8080/swagger-ui/index.html | — | +| **Web (Vite)** | http://localhost:5173 | — | +| **RabbitMQ UI** | http://localhost:15672 | `orderflow` / `orderflow123` | +| **PostgreSQL** | `localhost:5432` | `orderflow` / `orderflow123` / db: `orderflow` | +| **Debug (JDWP)** | `localhost:5005` | — | + +> ♻️ **Hot reload** is enabled for both API (auto-recompile + DevTools restart) and Web (Vite HMR). + +### Option 2: Local Development · Desenvolvimento Local + +Start only infrastructure via Docker, run API and Web natively: + +```bash +# Infrastructure +docker compose up -d postgres rabbitmq + +# API (terminal 1) +cd api && ./mvnw spring-boot:run -Dspring-boot.run.profiles=local + +# Web (terminal 2) +cd web && npm install && npm run dev +``` + +### Stopping · Parando + +```bash +docker compose down +``` + +--- + +## Project Structure · Estrutura do Projeto + +``` +commerce-async-platform/ +├── docker-compose.yml # Orchestrates all services +├── AGENTS.md # Dev environment instructions +│ +├── api/ # ⚡ Spring Boot Backend +│ ├── Dockerfile # Dev image (JDK + Maven + inotify) +│ ├── dev-entrypoint.sh # File watcher for hot reload +│ ├── pom.xml # Maven dependencies +│ └── src/ +│ ├── main/java/com/orderflow/ecommerce/ +│ │ ├── Application.java +│ │ ├── config/ # Security, OpenAPI, RabbitMQ +│ │ ├── controllers/ # REST endpoints +│ │ ├── dtos/ # Data transfer objects +│ │ ├── entities/ # JPA entities + enums +│ │ ├── exceptions/ # Global error handler +│ │ ├── messaging/ # Publisher, consumers, events +│ │ └── repositories/ # Spring Data JPA interfaces +│ └── main/resources/ +│ ├── application.properties +│ ├── application-docker.properties +│ └── application-local.properties +│ +└── web/ # 🌐 React Frontend + ├── Dockerfile # Production build (nginx) + ├── Dockerfile.dev # Development (Vite HMR) + ├── package.json + ├── vite.config.ts # Proxy /api → backend + └── src/ + ├── App.tsx # Main component + ├── lib/api.ts # API client + └── index.css # Tailwind imports +``` + +--- + +## RabbitMQ Topology · Topologia de Mensageria + +| Resource | Name | Type | Notes | +|----------|------|------|-------| +| **Exchange** | `order.events` | Topic (durable) | Central event hub | +| **Queue** | `order.inventory` | Durable | Inventory reservation | +| **Queue** | `order.email` | Durable | Email notifications | +| **Routing Key** | `order.created` | — | Binds both queues | + +Both queues receive the same `OrderCreatedEvent` (fan-out via shared routing key), enabling **independent, parallel processing**. + +--- + +## Roadmap · Evolução Planejada + +> Detalhes em [`api/docs/microservices-migration.md`](api/docs/microservices-migration.md) + +| Phase | Description | Status | +|-------|-------------|--------| +| 1 | **Baseline** — Monolith with event-driven order processing | ✅ Done | +| 2 | **Contracts** — Shared event schemas (`orderflow-contracts`) | 🔜 Planned | +| 3 | **Extract Workers** — Inventory & notification as separate services | 🔜 Planned | +| 4 | **Extract APIs** — Catalog & orders as independent microservices | 🔜 Planned | +| 5 | **Hardening** — DLQ, idempotency, observability, CI pipeline | 🔜 Planned | + +**Planned integrations:** Redis caching (cart), JWT authentication, Resilience4j circuit breakers. + +--- + +## Tests · Testes + +```bash +# Backend unit tests +cd api && ./mvnw test + +# Frontend lint +cd web && npm run lint + +# Frontend build check +cd web && npm run build +``` + +--- + +## Contribution Workflow · Como contribuir + +Para manter o monorepo organizado e o histórico do Git limpo, adotamos padrões estritos para a nomenclatura de **branches** e **mensagens de commit** (baseado em Conventional Commits). + +--- + +### Branch Naming · Padrão de Branches + +Toda nova alteração deve partir da branch principal utilizando a seguinte estrutura em **inglês** e com letras **minúsculas**: + +#### Formato +``` +padrão: [tipo-abreviado]/[escopo-opcional]-[breve-descrição] +``` + +#### Tipos Permitidos (Prefixos) +* `feat/` : Nova funcionalidade (ex: `feat/cart-page`) +* `fix/` : Correção de bug (ex: `fix/rabbitmq-retry`) +* `docs/` : Alterações exclusivas de documentação (ex: `docs/separate-swagger-docs`) +* `refactor/` : Refatoração de código que não altera o comportamento (ex: `refactor/clean-controllers`) +* `chore/` : Atualizações de build, dependências ou ferramentas (ex: `chore/update-docker-compose`) + +--- + +### Semantic Commits · Commits Semânticos + +As mensagens de commit devem ser escritas obrigatoriamente em **inglês**, utilizando letras **minúsculas** e o verbo no **imperativo** (ex: *add*, *fix*, *remove*, em vez de *added*, *fixed*, *removing*). + +#### Formato +``` +padrão: [tipo-abreviado](escopo): +``` + +#### Tabela de Tipos e Escopos + +| Tipo | Uso | Escopo | Significado | +|:-----|:----| :--- | :--- | +| **feat** | Nova funcionalidade | **auth** | Autenticação, JWT | +| **fix** | Correção de bug | **produto** | CRUD de produtos | +| **docs** | Documentação | **categoria** | CRUD de categorias | +| **style** | Formatação, espaços, lint (não altera código) | **usuario** | CRUD de usuários | +| **refactor** | Refatoração de código | **carrinho** | Carrinho de compras | +| **test** | Adicionar ou corrigir testes | **pedido** | Checkout e pedidos | +| **chore** | Configuração, dependências, build | **messaging** | Fila, RabbitMQ, consumidores | +| **chore** | Configuração, dependências, build | **docker** | Dockerfile, docker-compose | +| **chore** | Configuração, dependências, build | **infra** | Configurações gerais | + +#### Exemplos Práticos · Examples + +* **Funcionalidades e Correções:** + * `feat(auth): implement login with JWT` + * `feat(messaging): configure RabbitMQ and publish order event` + * `fix(carrinho): avoid duplicate items in cart` + * `fix(auth): fix expired token validation` + +* **Refatoração, Testes e Outros:** + * `refactor(produto): extract validation logic to service` + * `test(pedido): add integration tests with testcontainers` + * `docs: add architecture diagram to README` + * `chore: configure docker-compose with PostgreSQL and RabbitMQ` + +#### Regras de Ouro +1. **Inglês sempre!** +2. **Minúsculo** – Tudo em letras minúsculas. +3. **Imperativo** – "add" e não "added" ou "adding". +4. **Curto** – Até 50 caracteres na mensagem principal. +5. **Sem ponto final** – Não termine a linha de resumo com ponto `.`. + +## Contributors · Colaboradores + +| Name | Role | Contributions | +|------|------|---------------| +| **Diego Ferreira** | Advanced Developer | RabbitMQ, checkout, Docker, architecture | +| **Giovanna Caxias** | Junior Developer | CRUD, JWT auth, cart, Swagger | -## Desenvolvimento local +--- -- **API:** `cd api && ./mvnw spring-boot:run` (Windows: `mvnw.cmd`) -- **Web:** `cd web && npm install && npm run dev` +## License · Licença -Detalhes da API, stack e variáveis: [`api/README.md`](api/README.md). +Portfolio project. Not licensed for commercial use. +Projeto de portfólio. Não licenciado para uso comercial. diff --git a/api/Dockerfile b/api/Dockerfile index 21ca42e..a5c06f9 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -2,13 +2,13 @@ FROM eclipse-temurin:21-jdk-alpine WORKDIR /app -RUN apk add --no-cache maven +RUN apk add --no-cache maven inotify-tools COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src +COPY dev-entrypoint.sh /app/dev-entrypoint.sh +RUN chmod +x /app/dev-entrypoint.sh -CMD ["mvn", "spring-boot:run", \ - "-Dspring-boot.run.profiles=docker", \ - "-Dspring-boot.run.jvmArguments=-Dspring.devtools.restart.poll-interval=2000 -Dspring.devtools.restart.quiet-period=1000"] \ No newline at end of file +CMD ["/app/dev-entrypoint.sh"] \ No newline at end of file diff --git a/api/dev-entrypoint.sh b/api/dev-entrypoint.sh new file mode 100755 index 0000000..b63841b --- /dev/null +++ b/api/dev-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +echo "[dev] Compiling project..." +mvn compile -q -B + +echo "[dev] Starting file watcher on src/..." +( + while inotifywait -r -q -e modify,create,delete,move \ + --include '\.java$|\.properties$|\.yml$|\.yaml$|\.xml$' \ + src/ 2>/dev/null; do + echo "[dev] Source change detected — recompiling..." + mvn compile -q -B 2>&1 || echo "[dev] Compilation failed (check logs above)." + done +) & + +echo "[dev] Starting Spring Boot..." +exec mvn spring-boot:run \ + -Dspring-boot.run.profiles=${SPRING_PROFILES_ACTIVE:-docker} \ + -Dspring-boot.run.jvmArguments="${SPRING_BOOT_JVM_ARGS:--agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005}" diff --git a/api/mvnw b/api/mvnw old mode 100644 new mode 100755 diff --git a/api/src/main/resources/application-local.properties b/api/src/main/resources/application-local.properties new file mode 100644 index 0000000..85b077f --- /dev/null +++ b/api/src/main/resources/application-local.properties @@ -0,0 +1,10 @@ +spring.config.activate.on-profile=local + +spring.datasource.url=jdbc:postgresql://localhost:5432/orderflow +spring.datasource.username=orderflow +spring.datasource.password=orderflow123 + +spring.rabbitmq.host=localhost +spring.rabbitmq.port=5672 +spring.rabbitmq.username=orderflow +spring.rabbitmq.password=orderflow123 diff --git a/docker-compose.yml b/docker-compose.yml index d065089..4fa4dda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: postgres: image: postgres:15 @@ -8,8 +6,6 @@ services: POSTGRES_DB: orderflow POSTGRES_USER: orderflow POSTGRES_PASSWORD: orderflow123 - SPRING_DEVTOOLS_RESTART_POLL_INTERVAL: "2000" - SPRING_DEVTOOLS_RESTART_QUIET_PERIOD: "1000" ports: - "5432:5432" volumes: @@ -57,8 +53,7 @@ services: SPRING_RABBITMQ_PASSWORD: orderflow123 SPRING_DEVTOOLS_RESTART_ENABLED: "true" SPRING_DEVTOOLS_LIVERELOAD_ENABLED: "true" - # Debug apenas no processo filho (fork do spring-boot:run) - MAVEN_OPTS: "-Dspring-boot.run.jvmArguments=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" + SPRING_BOOT_JVM_ARGS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" ports: - "8080:8080" - "5005:5005" @@ -79,12 +74,17 @@ services: web: build: context: ./web - dockerfile: Dockerfile - args: - VITE_API_BASE_URL: /api + dockerfile: Dockerfile.dev container_name: orderflow-web + environment: + VITE_API_BASE_URL: /api + API_PROXY_TARGET: http://app:8080 ports: - - "4173:80" + - "5173:5173" + volumes: + - ./web/src:/app/src + - ./web/index.html:/app/index.html + - ./web/vite.config.ts:/app/vite.config.ts depends_on: app: condition: service_started diff --git a/scripts/test-hot-reload.sh b/scripts/test-hot-reload.sh new file mode 100755 index 0000000..68cf62e --- /dev/null +++ b/scripts/test-hot-reload.sh @@ -0,0 +1,328 @@ +#!/usr/bin/env bash +# =========================================================================== +# OrderFlow Commerce — E2E Hot-Reload Test +# =========================================================================== +# Validates the full hot-reload pipeline in Docker: +# +# 1. API is healthy and responds with the ORIGINAL message. +# 2. A Java source file is modified (PingResponse message changed). +# 3. inotifywait detects the change → mvn compile → DevTools restarts. +# 4. API now responds with the MODIFIED message. +# 5. The source file is reverted to the original. +# 6. inotifywait detects the revert → mvn compile → DevTools restarts. +# 7. API responds with the ORIGINAL message again. +# +# Usage: +# ./scripts/test-hot-reload.sh # uses running containers +# ./scripts/test-hot-reload.sh --start # docker compose up first +# ./scripts/test-hot-reload.sh --full # up, test, then down +# +# Exit codes: +# 0 = all assertions passed +# 1 = assertion failed +# 2 = environment error (API unreachable, timeout, etc.) +# =========================================================================== +set -euo pipefail + +# ── Configuration ────────────────────────────────────────────────────────── +API_URL="${API_URL:-http://localhost:8080}" +PING_ENDPOINT="${API_URL}/test/ping" + +TARGET_FILE="api/src/main/java/com/orderflow/ecommerce/controllers/TestController.java" +ORIGINAL_MSG='versão 1.' +MODIFIED_MSG='versão 2 — hot reload e2e test' + +STARTUP_TIMEOUT=120 # max seconds to wait for API to be ready +HOT_RELOAD_TIMEOUT=60 # max seconds to wait for hot-reload cycle +POLL_INTERVAL=2 # seconds between health polls + +# ── Colors ───────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# ── Helpers ──────────────────────────────────────────────────────────────── +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[PASS]${NC} $*"; } +fail() { echo -e "${RED}[FAIL]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +step() { echo -e "\n${BOLD}── $* ──${NC}"; } +divider() { echo -e "${CYAN}═══════════════════════════════════════════════════════${NC}"; } + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +assert_eq() { + local label="$1" expected="$2" actual="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ "$expected" = "$actual" ]; then + ok "$label" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + fail "$label" + echo " expected: \"$expected\"" + echo " actual: \"$actual\"" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi +} + +assert_contains() { + local label="$1" needle="$2" haystack="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + if echo "$haystack" | grep -qF "$needle"; then + ok "$label" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + fail "$label" + echo " expected to contain: \"$needle\"" + echo " actual: \"$haystack\"" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi +} + +get_ping_message() { + curl -sf "$PING_ENDPOINT" 2>/dev/null \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('message',''))" 2>/dev/null \ + || echo "" +} + +wait_for_api() { + local timeout=$1 + local elapsed=0 + while [ $elapsed -lt $timeout ]; do + if curl -sf "$PING_ENDPOINT" > /dev/null 2>&1; then + return 0 + fi + sleep "$POLL_INTERVAL" + elapsed=$((elapsed + POLL_INTERVAL)) + done + return 1 +} + +wait_for_message() { + local expected="$1" timeout="$2" + local elapsed=0 + while [ $elapsed -lt $timeout ]; do + local msg + msg=$(get_ping_message) + if [ "$msg" = "$expected" ]; then + info "Detected in ${elapsed}s" + return 0 + fi + sleep "$POLL_INTERVAL" + elapsed=$((elapsed + POLL_INTERVAL)) + done + return 1 +} + +cleanup() { + if [ -f "$TARGET_FILE" ]; then + if grep -qF "$MODIFIED_MSG" "$TARGET_FILE" 2>/dev/null; then + warn "Reverting source file (cleanup)..." + sed -i "s|$MODIFIED_MSG|$ORIGINAL_MSG|g" "$TARGET_FILE" + fi + fi +} +trap cleanup EXIT + +# ── Parse arguments ──────────────────────────────────────────────────────── +DO_START=false +DO_STOP=false + +for arg in "$@"; do + case "$arg" in + --start) DO_START=true ;; + --full) DO_START=true; DO_STOP=true ;; + --help|-h) + echo "Usage: $0 [--start | --full]" + echo " --start run 'docker compose up --build -d' before testing" + echo " --full start before and stop after testing" + exit 0 + ;; + *) warn "Unknown argument: $arg" ;; + esac +done + +# ── Main ─────────────────────────────────────────────────────────────────── +divider +echo -e "${BOLD} OrderFlow Commerce — E2E Hot-Reload Test${NC}" +divider + +cd "$(git rev-parse --show-toplevel 2>/dev/null || echo /workspace)" + +# Optionally start Docker Compose +if $DO_START; then + step "Starting Docker Compose" + docker compose up --build -d 2>&1 | tail -5 +fi + +# ── Phase 1: Wait for API ────────────────────────────────────────────────── +step "Phase 1 · Waiting for API to be ready" +info "Endpoint: $PING_ENDPOINT (timeout: ${STARTUP_TIMEOUT}s)" + +if ! wait_for_api "$STARTUP_TIMEOUT"; then + fail "API did not become ready within ${STARTUP_TIMEOUT}s" + exit 2 +fi +ok "API is responding" + +# ── Phase 2: Verify original state ──────────────────────────────────────── +step "Phase 2 · Verify original response" + +CURRENT_MSG=$(get_ping_message) +assert_eq "Ping message is original" "$ORIGINAL_MSG" "$CURRENT_MSG" + +FULL_RESPONSE=$(curl -sf "$PING_ENDPOINT" 2>/dev/null || echo "{}") +assert_contains "Response has status field" '"status"' "$FULL_RESPONSE" +assert_contains "Response has timestamp field" '"timestamp"' "$FULL_RESPONSE" +assert_contains "Status is ok" '"ok"' "$FULL_RESPONSE" + +if [ $TESTS_FAILED -gt 0 ]; then + fail "Original state verification failed — aborting" + exit 1 +fi + +# ── Phase 3: Modify source file ─────────────────────────────────────────── +step "Phase 3 · Modify Java source (trigger hot reload)" + +if [ ! -f "$TARGET_FILE" ]; then + fail "Target file not found: $TARGET_FILE" + exit 2 +fi + +info "Changing message: \"$ORIGINAL_MSG\" → \"$MODIFIED_MSG\"" +sed -i "s|$ORIGINAL_MSG|$MODIFIED_MSG|g" "$TARGET_FILE" + +if grep -qF "$MODIFIED_MSG" "$TARGET_FILE"; then + ok "Source file modified successfully" +else + fail "Source file modification failed" + exit 2 +fi + +# ── Phase 4: Wait for hot reload with modified message ───────────────────── +step "Phase 4 · Waiting for hot reload (timeout: ${HOT_RELOAD_TIMEOUT}s)" +info "Expecting message: \"$MODIFIED_MSG\"" + +RELOAD_START=$(date +%s) + +if wait_for_message "$MODIFIED_MSG" "$HOT_RELOAD_TIMEOUT"; then + RELOAD_END=$(date +%s) + RELOAD_TIME=$((RELOAD_END - RELOAD_START)) + assert_eq "Hot reload delivered modified message" "$MODIFIED_MSG" "$(get_ping_message)" + ok "Hot reload completed in ${RELOAD_TIME}s ⚡" +else + ACTUAL=$(get_ping_message) + fail "Hot reload did not deliver modified message within ${HOT_RELOAD_TIMEOUT}s" + echo " expected: \"$MODIFIED_MSG\"" + echo " actual: \"$ACTUAL\"" + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) +fi + +# ── Phase 5: Revert source file ─────────────────────────────────────────── +step "Phase 5 · Revert source file (trigger second hot reload)" + +info "Changing message: \"$MODIFIED_MSG\" → \"$ORIGINAL_MSG\"" +sed -i "s|$MODIFIED_MSG|$ORIGINAL_MSG|g" "$TARGET_FILE" + +if grep -qF "$ORIGINAL_MSG" "$TARGET_FILE"; then + ok "Source file reverted successfully" +else + fail "Source file revert failed" + exit 2 +fi + +# ── Phase 6: Wait for hot reload with original message ───────────────────── +step "Phase 6 · Waiting for second hot reload (timeout: ${HOT_RELOAD_TIMEOUT}s)" +info "Expecting message: \"$ORIGINAL_MSG\"" + +RELOAD_START=$(date +%s) + +if wait_for_message "$ORIGINAL_MSG" "$HOT_RELOAD_TIMEOUT"; then + RELOAD_END=$(date +%s) + RELOAD_TIME=$((RELOAD_END - RELOAD_START)) + assert_eq "Hot reload delivered original message" "$ORIGINAL_MSG" "$(get_ping_message)" + ok "Second hot reload completed in ${RELOAD_TIME}s ⚡" +else + ACTUAL=$(get_ping_message) + fail "Second hot reload did not deliver original message within ${HOT_RELOAD_TIMEOUT}s" + echo " expected: \"$ORIGINAL_MSG\"" + echo " actual: \"$ACTUAL\"" + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) +fi + +# ── Phase 7: Verify API is still healthy ─────────────────────────────────── +step "Phase 7 · Post-reload health check" + +if curl -sf "$PING_ENDPOINT" > /dev/null 2>&1; then + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_PASSED=$((TESTS_PASSED + 1)) + ok "API is still healthy after two reload cycles" +else + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + fail "API is not responding after reload cycles" +fi + +# Verify full CRUD still works after reloads +CATEGORY_RESPONSE=$(curl -sf -X POST "$API_URL/categories" \ + -H "Content-Type: application/json" \ + -d '{"name": "HotReloadTest"}' 2>/dev/null || echo "") + +if echo "$CATEGORY_RESPONSE" | grep -qF "HotReloadTest"; then + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_PASSED=$((TESTS_PASSED + 1)) + ok "CRUD operations work after hot reload (POST /categories)" + + CATEGORY_ID=$(echo "$CATEGORY_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") + if [ -n "$CATEGORY_ID" ]; then + curl -sf -X DELETE "$API_URL/categories/$CATEGORY_ID" > /dev/null 2>&1 || true + fi +else + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + fail "CRUD operations broken after hot reload" +fi + +# Verify RabbitMQ integration still works +RABBIT_RESPONSE=$(curl -sf "$API_URL/test/publish-sample-order" 2>/dev/null || echo "") +if echo "$RABBIT_RESPONSE" | grep -qF '"published":true'; then + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_PASSED=$((TESTS_PASSED + 1)) + ok "RabbitMQ event publishing works after hot reload" +else + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + fail "RabbitMQ event publishing broken after hot reload" +fi + +# ── Optionally stop Docker Compose ───────────────────────────────────────── +if $DO_STOP; then + step "Stopping Docker Compose" + docker compose down 2>&1 | tail -3 +fi + +# ── Results ──────────────────────────────────────────────────────────────── +divider +echo -e "${BOLD} Results${NC}" +divider +echo -e " Tests run: ${BOLD}$TESTS_RUN${NC}" +echo -e " Passed: ${GREEN}${BOLD}$TESTS_PASSED${NC}" +if [ $TESTS_FAILED -gt 0 ]; then + echo -e " Failed: ${RED}${BOLD}$TESTS_FAILED${NC}" +fi +divider + +if [ $TESTS_FAILED -eq 0 ]; then + echo -e "\n${GREEN}${BOLD}✅ All hot-reload E2E tests passed!${NC}\n" + exit 0 +else + echo -e "\n${RED}${BOLD}❌ $TESTS_FAILED test(s) failed.${NC}\n" + exit 1 +fi diff --git a/web/Dockerfile.dev b/web/Dockerfile.dev new file mode 100644 index 0000000..f4ecb48 --- /dev/null +++ b/web/Dockerfile.dev @@ -0,0 +1,12 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/web/package-lock.json b/web/package-lock.json index bf516aa..ecf3cd9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -59,7 +59,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -269,6 +268,29 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1127,7 +1149,6 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1138,7 +1159,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1198,7 +1218,6 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -1429,7 +1448,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1520,7 +1538,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1670,7 +1687,6 @@ "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2586,7 +2602,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2648,7 +2663,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2833,7 +2847,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2920,7 +2933,6 @@ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3045,7 +3057,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/vite.config.ts b/web/vite.config.ts index e8c19bb..0f20c6e 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -2,13 +2,15 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' +const apiTarget = process.env.API_PROXY_TARGET || 'http://localhost:8080' + export default defineConfig({ plugins: [react(), tailwindcss()], server: { port: 5173, proxy: { '/api': { - target: 'http://localhost:8080', + target: apiTarget, changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), },