├── .gitattributes ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .trae └── documents │ └── OrderHub_Product_Requirements.md ├── Dockerfile ├── HELP.md ├── README.md ├── docker-compose.yml ├── docs ├── ABACATEPAY_SETUP.md ├── KAFKA_LOCAL_SETUP.md ├── KAFKA_ORDER_CANCELLED_EVENT.md └── architecture-diagram.md ├── init.sql ├── mvnw ├── mvnw.cmd ├── pom.xml ├── scripts ├── start-kafka-local.sh ├── stop-kafka-local.sh └── test-kafka-integration.sh └── src ├── main ├── java │ └── com │ │ └── kipperdev │ │ └── orderhub │ │ ├── OrderhubApplication.java │ │ ├── client │ │ └── AbacatePayClient.java │ │ ├── config │ │ ├── AbacatePayFeignConfig.java │ │ ├── FeignConfig.java │ │ ├── KafkaConfig.java │ │ ├── MapStructConfig.java │ │ └── SecurityConfig.java │ │ ├── controller │ │ ├── AbacatePayWebhookController.java │ │ ├── AdminOrderController.java │ │ ├── OrderController.java │ │ ├── PublicOrderController.java │ │ └── WebhookController.java │ │ ├── dto │ │ ├── CreateOrderRequestDTO.java │ │ ├── CustomerDTO.java │ │ ├── OrderItemDTO.java │ │ ├── OrderResponseDTO.java │ │ ├── OrderStatusDTO.java │ │ └── abacate │ │ │ ├── AbacateChargeRequestDTO.java │ │ │ ├── AbacateChargeResponseDTO.java │ │ │ ├── AbacateCustomerDTO.java │ │ │ ├── AbacateCustomerResponseDTO.java │ │ │ └── AbacateWebhookDTO.java │ │ ├── entity │ │ ├── Customer.java │ │ ├── Order.java │ │ ├── OrderItem.java │ │ └── OrderStatus.java │ │ ├── event │ │ ├── InvoiceGeneratedEvent.java │ │ ├── OrderCreatedEvent.java │ │ ├── PaymentConfirmedEvent.java │ │ └── StockReservedEvent.java │ │ ├── mapper │ │ └── OrderMapper.java │ │ ├── repository │ │ ├── CustomerRepository.java │ │ ├── OrderItemRepository.java │ │ └── OrderRepository.java │ │ ├── service │ │ ├── AbacatePayService.java │ │ ├── CustomerService.java │ │ ├── KafkaConsumerService.java │ │ ├── KafkaProducerService.java │ │ └── OrderService.java │ │ └── specification │ │ └── OrderSpecification.java └── resources │ ├── application-local.yml │ ├── application-prod.yml │ ├── application.properties │ └── application.yml └── test └── java └── com └── kipperdev └── orderhub └── OrderhubApplicationTests.java /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | # Maven 27 | target/ 28 | pom.xml.tag 29 | pom.xml.releaseBackup 30 | pom.xml.versionsBackup 31 | pom.xml.next 32 | release.properties 33 | dependency-reduced-pom.xml 34 | buildNumber.properties 35 | .mvn/timing.properties 36 | .mvn/wrapper/maven-wrapper.jar 37 | 38 | # Gradle 39 | .gradle 40 | build/ 41 | !gradle/wrapper/gradle-wrapper.jar 42 | !**/src/main/**/build/ 43 | !**/src/test/**/build/ 44 | 45 | # Spring Boot 46 | *.original 47 | 48 | # IDE 49 | .idea/ 50 | *.iws 51 | *.iml 52 | *.ipr 53 | .vscode/ 54 | *.swp 55 | *.swo 56 | *~ 57 | 58 | # Eclipse 59 | .apt_generated 60 | .classpath 61 | .factorypath 62 | .project 63 | .settings 64 | .springBeans 65 | .sts4-cache 66 | bin/ 67 | !**/src/main/**/bin/ 68 | !**/src/test/**/bin/ 69 | 70 | # NetBeans 71 | /nbproject/private/ 72 | /nbbuild/ 73 | /dist/ 74 | /nbdist/ 75 | /.nb-gradle/ 76 | 77 | # VS Code 78 | .vscode/ 79 | 80 | # Mac 81 | .DS_Store 82 | 83 | # Windows 84 | Thumbs.db 85 | ehthumbs.db 86 | Desktop.ini 87 | 88 | # Application specific 89 | *.env 90 | .env 91 | .env.local 92 | .env.*.local 93 | 94 | # Database 95 | *.db 96 | *.sqlite 97 | *.sqlite3 98 | 99 | # Logs 100 | logs/ 101 | *.log 102 | 103 | # Docker 104 | .dockerignore 105 | 106 | # Temporary files 107 | *.tmp 108 | *.temp 109 | 110 | # Generated files 111 | generated/ 112 | 113 | # Test reports 114 | /test-results/ 115 | /test-output/ 116 | 117 | # Coverage reports 118 | /coverage/ 119 | *.lcov 120 | 121 | # Node modules (if using frontend tools) 122 | node_modules/ 123 | npm-debug.log* 124 | yarn-debug.log* 125 | yarn-error.log* 126 | 127 | # MapStruct generated files 128 | **/generated-sources/ 129 | **/generated-test-sources/ -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip 20 | -------------------------------------------------------------------------------- /.trae/documents/OrderHub_Product_Requirements.md: -------------------------------------------------------------------------------- 1 | ## 1. Visão Geral do Produto 2 | 3 | OrderHub é um sistema central de processamento de pedidos desenvolvido em Java Spring Boot, integrado a múltiplos serviços internos e externos incluindo gateway de pagamentos, controle de estoque, mensageria e APIs públicas e administrativas. 4 | 5 | - O sistema resolve o problema de centralização e orquestração de pedidos em um ambiente distribuído, permitindo que empresas gerenciem todo o ciclo de vida dos pedidos de forma eficiente. 6 | - O produto atende equipes de desenvolvimento, administradores de sistema e clientes finais que precisam acompanhar seus pedidos. 7 | - Valor de mercado: solução enterprise para e-commerce e marketplaces que precisam de alta disponibilidade e integração com múltiplos sistemas. 8 | 9 | ## 2. Funcionalidades Principais 10 | 11 | ### 2.1 Papéis de Usuário 12 | 13 | | Papel | Método de Registro | Permissões Principais | 14 | |-------|-------------------|----------------------| 15 | | Cliente | Criação automática durante pedido | Consultar status de pedidos próprios | 16 | | Administrador | Acesso básico (admin:senha123) | Gerenciar todos os pedidos, filtros avançados, atualizações manuais | 17 | | Sistema Externo | Integração via webhook/API | Receber e enviar eventos de pagamento e estoque | 18 | 19 | ### 2.2 Módulos de Funcionalidade 20 | 21 | Nossos requisitos do OrderHub consistem nas seguintes páginas principais: 22 | 23 | 1. **API de Pedidos**: criação de pedidos, validação de dados, persistência no banco. 24 | 2. **API Pública de Status**: consulta reativa de status de pedidos por ID ou email. 25 | 3. **Painel Administrativo**: listagem, filtros e gerenciamento de pedidos. 26 | 4. **Webhook de Pagamento**: recebimento de notificações do gateway Abacate Pay. 27 | 5. **Sistema de Mensageria**: orquestração de eventos via Kafka. 28 | 29 | ### 2.3 Detalhes das Páginas 30 | 31 | | Nome da Página | Nome do Módulo | Descrição da Funcionalidade | 32 | |----------------|----------------|-----------------------------| 33 | | API de Pedidos | Criação de Pedidos | Receber pedido via POST /orders, validar dados do cliente e itens, salvar com status PENDING_PAYMENT, retornar ID e link de status | 34 | | API de Pedidos | Validação de Negócio | Validar estoque mínimo, formato de dados, regras de negócio usando Spring Validation | 35 | | API de Pedidos | Mapeamento DTO/Entity | Converter automaticamente entre DTOs e entidades usando MapStruct | 36 | | API de Pedidos | Eventos Kafka | Publicar evento orders.created após criação do pedido | 37 | | API Pública de Status | Consulta Reativa | Permitir consulta de status via GET /public/orders/{id}/status usando Spring WebFlux | 38 | | API Pública de Status | Rate Limiting | Proteger endpoint contra abuso com limitação de taxa | 39 | | API Pública de Status | Streaming em Tempo Real | Fornecer Flux com updates em tempo real do status | 40 | | Painel Administrativo | Listagem com Paginação | Listar pedidos com paginação usando Pageable | 41 | | Painel Administrativo | Filtros Dinâmicos | Filtrar por status, data, cliente usando Specification | 42 | | Painel Administrativo | Atualização Manual | Permitir atualização manual de status em casos de erro | 43 | | Painel Administrativo | Histórico de Pagamentos | Consultar histórico completo de transações | 44 | | Webhook de Pagamento | Recebimento de Notificações | Receber webhook /webhook/abacate do gateway de pagamento | 45 | | Webhook de Pagamento | Verificação de Segurança | Validar assinatura HMAC para segurança | 46 | | Webhook de Pagamento | Atualização de Status | Atualizar status do pedido (PAID, FAILED) baseado na resposta | 47 | | Webhook de Pagamento | Eventos de Pagamento | Publicar evento payments.confirmed no Kafka | 48 | | Sistema de Mensageria | Produção de Eventos | Emitir eventos orders.created e payments.confirmed | 49 | | Sistema de Mensageria | Consumo de Eventos | Consumir stock.reserved e invoice.generated para atualizar status | 50 | | Sistema de Mensageria | Tratamento de Falhas | Implementar retry e Dead Letter Queue (DLQ) | 51 | | Integração Abacate Pay | Cliente Feign | Interface AbacatePayClient com endpoints mockados | 52 | | Integração Abacate Pay | Gerenciamento de Clientes | Criar cliente no Abacate Pay se não existir | 53 | | Integração Abacate Pay | Criação de Cobrança | Criar cobrança e obter link de pagamento | 54 | | Integração Abacate Pay | Persistência de Transação | Salvar ID da transação Abacate no banco | 55 | 56 | ## 3. Processo Principal 57 | 58 | **Fluxo do Cliente:** 59 | 1. Cliente envia pedido via POST /orders 60 | 2. Sistema valida dados e cria pedido com status PENDING_PAYMENT 61 | 3. Sistema integra com Abacate Pay para criar cobrança 62 | 4. Cliente recebe link de pagamento 63 | 5. Cliente consulta status via GET /public/orders/{id}/status 64 | 6. Sistema recebe webhook de confirmação de pagamento 65 | 7. Status é atualizado para PAID e eventos são publicados 66 | 8. Serviços externos processam estoque e faturamento 67 | 9. Status final é atualizado para COMPLETED 68 | 69 | **Fluxo do Administrador:** 70 | 1. Admin acessa painel via autenticação básica 71 | 2. Lista pedidos com filtros (status, data, cliente) 72 | 3. Visualiza detalhes e histórico de pagamentos 73 | 4. Atualiza status manualmente se necessário 74 | 75 | ```mermaid 76 | graph TD 77 | A[POST /orders] --> B[Validação de Dados] 78 | B --> C[Salvar Pedido PENDING_PAYMENT] 79 | C --> D[Integração Abacate Pay] 80 | D --> E[Link de Pagamento] 81 | E --> F[GET /public/orders/status] 82 | F --> G[Webhook Pagamento] 83 | G --> H[Atualizar Status PAID] 84 | H --> I[Eventos Kafka] 85 | I --> J[Processamento Estoque] 86 | J --> K[Status COMPLETED] 87 | 88 | L[Admin Login] --> M[GET /admin/orders] 89 | M --> N[Filtros e Paginação] 90 | N --> O[Atualização Manual] 91 | ``` 92 | 93 | ## 4. Design da Interface do Usuário 94 | 95 | ### 4.1 Estilo de Design 96 | 97 | - **Cores Primárias**: #2563eb (azul), #059669 (verde para sucesso) 98 | - **Cores Secundárias**: #64748b (cinza), #dc2626 (vermelho para erros) 99 | - **Estilo de Botões**: Arredondados com sombra sutil 100 | - **Fonte**: Inter, tamanhos 14px (corpo), 16px (títulos), 12px (labels) 101 | - **Layout**: Card-based com navegação superior fixa 102 | - **Ícones**: Lucide icons para consistência 103 | 104 | ### 4.2 Visão Geral do Design das Páginas 105 | 106 | | Nome da Página | Nome do Módulo | Elementos da UI | 107 | |----------------|----------------|----------------| 108 | | API de Pedidos | Resposta JSON | Formato JSON limpo com ID do pedido, status e link de consulta | 109 | | API Pública de Status | Consulta de Status | Resposta JSON reativa com status atual, timestamp e detalhes do pedido | 110 | | Painel Administrativo | Lista de Pedidos | Tabela responsiva com colunas: ID, Cliente, Status, Data, Valor, Ações | 111 | | Painel Administrativo | Filtros | Formulário horizontal com campos: Status (select), Data (date picker), Cliente (input text) | 112 | | Painel Administrativo | Paginação | Componente de paginação com números de página e controles anterior/próximo | 113 | | Webhook de Pagamento | Log de Eventos | Interface de monitoramento com lista de webhooks recebidos e status de processamento | 114 | 115 | ### 4.3 Responsividade 116 | 117 | O sistema é desktop-first com adaptação para mobile. O painel administrativo é otimizado para desktop, enquanto a API pública é acessível via mobile. Não há otimização específica para touch, focando em integração via API. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage build for optimized image size 2 | FROM maven:3.9.4-eclipse-temurin-21 AS builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy pom.xml and download dependencies (for better caching) 8 | COPY pom.xml . 9 | RUN mvn dependency:go-offline -B 10 | 11 | # Copy source code 12 | COPY src ./src 13 | 14 | # Build the application 15 | RUN mvn clean package -DskipTests 16 | 17 | # Runtime stage 18 | FROM eclipse-temurin:21-jre-alpine 19 | 20 | # Install curl for health checks 21 | RUN apk add --no-cache curl 22 | 23 | # Create non-root user 24 | RUN addgroup -g 1001 -S orderhub && \ 25 | adduser -S orderhub -u 1001 -G orderhub 26 | 27 | # Set working directory 28 | WORKDIR /app 29 | 30 | # Copy jar from builder stage 31 | COPY --from=builder /app/target/*.jar app.jar 32 | 33 | # Change ownership to non-root user 34 | RUN chown -R orderhub:orderhub /app 35 | 36 | # Switch to non-root user 37 | USER orderhub 38 | 39 | # Expose port 40 | EXPOSE 8080 41 | 42 | # Health check 43 | HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ 44 | CMD curl -f http://localhost:8080/actuator/health || exit 1 45 | 46 | # JVM options for containerized environment 47 | ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+UseStringDeduplication" 48 | 49 | # Run the application 50 | ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 7 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.4/maven-plugin) 8 | * [Create an OCI image](https://docs.spring.io/spring-boot/3.5.4/maven-plugin/build-image.html) 9 | * [Spring Web](https://docs.spring.io/spring-boot/3.5.4/reference/web/servlet.html) 10 | * [Spring Data JPA](https://docs.spring.io/spring-boot/3.5.4/reference/data/sql.html#data.sql.jpa-and-spring-data) 11 | * [Spring Boot DevTools](https://docs.spring.io/spring-boot/3.5.4/reference/using/devtools.html) 12 | * [OpenFeign](https://docs.spring.io/spring-cloud-openfeign/reference/) 13 | * [Validation](https://docs.spring.io/spring-boot/3.5.4/reference/io/validation.html) 14 | * [Spring for Apache Kafka](https://docs.spring.io/spring-boot/3.5.4/reference/messaging/kafka.html) 15 | * [Spring Reactive Web](https://docs.spring.io/spring-boot/3.5.4/reference/web/reactive.html) 16 | 17 | ### Guides 18 | The following guides illustrate how to use some features concretely: 19 | 20 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 21 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 22 | * [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) 23 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 24 | * [Validation](https://spring.io/guides/gs/validating-form-input/) 25 | * [Building a Reactive RESTful Web Service](https://spring.io/guides/gs/reactive-rest-service/) 26 | 27 | ### Additional Links 28 | These additional references should also help you: 29 | 30 | * [Declarative REST calls with Spring Cloud OpenFeign sample](https://github.com/spring-cloud-samples/feign-eureka) 31 | 32 | ### Maven Parent overrides 33 | 34 | Due to Maven's design, elements are inherited from the parent POM to the project POM. 35 | While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. 36 | To prevent this, the project POM contains empty overrides for these elements. 37 | If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OrderHub - Sistema de Gerenciamento de Pedidos 2 | 3 | OrderHub é um sistema completo de gerenciamento de pedidos desenvolvido com Spring Boot, integrando pagamentos via Abacate Pay, processamento assíncrono com Kafka e APIs reativas com WebFlux. 4 | 5 | ## 🚀 Funcionalidades 6 | 7 | ### Core Features 8 | - ✅ **Criação de Pedidos**: API REST para criação de pedidos com validação 9 | - ✅ **Integração de Pagamentos**: Integração com Abacate Pay via Feign Client 10 | - ✅ **Processamento Assíncrono**: Eventos Kafka para comunicação entre serviços 11 | - ✅ **APIs Reativas**: Endpoints WebFlux para consultas em tempo real 12 | - ✅ **Painel Administrativo**: Endpoints para gerenciamento e relatórios 13 | - ✅ **Webhooks**: Processamento de webhooks do Abacate Pay 14 | 15 | ### Recursos Técnicos 16 | - 🔄 **Event-Driven Architecture**: Kafka para eventos de pedidos, pagamentos, estoque e faturas 17 | - 🔒 **Segurança**: Spring Security com autenticação básica para endpoints admin 18 | - 📊 **Monitoramento**: Spring Boot Actuator para métricas e health checks 19 | - 🗄️ **Persistência**: JPA/Hibernate com suporte a H2 (dev) e PostgreSQL (prod) 20 | - 🔍 **Filtros Dinâmicos**: JPA Specifications para consultas complexas 21 | - 📱 **Streaming**: Server-Sent Events para atualizações em tempo real 22 | 23 | ## 🏗️ Arquitetura 24 | 25 | ### Estrutura do Projeto 26 | ``` 27 | src/main/java/com/kipperdev/orderhub/ 28 | ├── config/ # Configurações (Kafka, Feign, Security, WebFlux) 29 | ├── controller/ # Controllers REST e WebFlux 30 | ├── dto/ # Data Transfer Objects 31 | ├── entity/ # Entidades JPA 32 | ├── event/ # Eventos Kafka 33 | ├── mapper/ # MapStruct mappers 34 | ├── repository/ # Repositórios JPA 35 | ├── service/ # Lógica de negócio 36 | ├── specification/ # JPA Specifications para filtros 37 | └── OrderhubApplication.java 38 | ``` 39 | 40 | ### Fluxo de Pedidos 41 | 1. **Criação**: Cliente cria pedido via API REST 42 | 2. **Pagamento**: Integração automática com Abacate Pay 43 | 3. **Eventos**: Publicação de eventos Kafka 44 | 4. **Processamento**: Consumo de eventos de estoque e faturamento 45 | 5. **Atualizações**: Webhooks do Abacate Pay atualizam status 46 | 6. **Notificações**: Streaming de status em tempo real 47 | 48 | ## 🛠️ Tecnologias 49 | 50 | - **Java 21** - Linguagem principal 51 | - **Spring Boot 3.5.4** - Framework base 52 | - **Spring Data JPA** - Persistência 53 | - **Spring Security** - Segurança 54 | - **Spring WebFlux** - APIs reativas 55 | - **Spring Kafka** - Mensageria 56 | - **Spring Cloud OpenFeign** - Cliente HTTP 57 | - **MapStruct** - Mapeamento de objetos 58 | - **Lombok** - Redução de boilerplate 59 | - **H2/PostgreSQL** - Banco de dados 60 | - **Maven** - Gerenciamento de dependências 61 | 62 | ## 🚦 Endpoints 63 | 64 | ### APIs Públicas 65 | ```http 66 | # Criar pedido 67 | POST /orders 68 | Content-Type: application/json 69 | 70 | # Consultar status do pedido 71 | GET /orders/{id} 72 | 73 | # Consultar pedidos por email (reativo) 74 | GET /public/orders/customer/{email}/status 75 | 76 | # Stream de status em tempo real 77 | GET /public/orders/{id}/status/stream 78 | Accept: text/event-stream 79 | ``` 80 | 81 | ### APIs Administrativas (Autenticação Requerida) 82 | ```http 83 | # Listar pedidos com filtros 84 | GET /admin/orders?status=PAID&customerEmail=user@example.com 85 | 86 | # Atualizar status do pedido 87 | PUT /admin/orders/{id}/status?status=SHIPPED 88 | 89 | # Estatísticas 90 | GET /admin/orders/stats 91 | 92 | # Exportar dados 93 | GET /admin/orders/export?format=csv 94 | 95 | # Cancelar pedido 96 | POST /admin/orders/{id}/cancel 97 | ``` 98 | 99 | ### Webhooks 100 | ```http 101 | # Webhook do Abacate Pay 102 | POST /webhook/abacate 103 | Content-Type: application/json 104 | X-Abacate-Signature: {signature} 105 | ``` 106 | 107 | ## 📋 Configuração 108 | 109 | ### Variáveis de Ambiente 110 | ```bash 111 | # Banco de Dados (Produção) 112 | DATABASE_URL=jdbc:postgresql://localhost:5432/orderhub 113 | DATABASE_USERNAME=orderhub 114 | DATABASE_PASSWORD=password 115 | 116 | # Kafka 117 | KAFKA_BOOTSTRAP_SERVERS=localhost:9092 118 | 119 | # Abacate Pay 120 | ABACATEAPI_TOKEN=your-api-token 121 | ABACATEMOCK_ENABLED=false 122 | ABACATEWEBHOOK_SECRET=your-webhook-secret 123 | ABACATEWEBHOOK_SIGNATURE_ENABLED=true 124 | 125 | # Admin 126 | ADMIN_USERNAME=admin 127 | ADMIN_PASSWORD=secure-password 128 | 129 | # App 130 | APP_BASE_URL=https://your-domain.com 131 | ``` 132 | 133 | ### Profiles 134 | - **default**: Desenvolvimento com H2 e mocks habilitados 135 | - **local**: Desenvolvimento local sem Kafka 136 | - **local-kafka**: Desenvolvimento local com Kafka habilitado 137 | - **prod**: Produção com PostgreSQL e integrações reais 138 | - **test**: Testes com Kafka embarcado 139 | - **docker**: Execução em containers Docker 140 | 141 | ## 🚀 Execução 142 | 143 | ### Desenvolvimento 144 | ```bash 145 | # Clonar repositório 146 | git clone 147 | cd orderhub 148 | 149 | # Executar com Maven 150 | ./mvnw spring-boot:run 151 | 152 | # Ou com profile específico 153 | ./mvnw spring-boot:run -Dspring-boot.run.profiles=prod 154 | ``` 155 | 156 | ### Docker (Opcional) 157 | ```bash 158 | # Build 159 | docker build -t orderhub . 160 | 161 | # Run 162 | docker run -p 8080:8080 orderhub 163 | ``` 164 | 165 | ### Desenvolvimento com Kafka 166 | ```bash 167 | # Iniciar infraestrutura Kafka 168 | ./scripts/start-kafka-local.sh 169 | 170 | # Executar aplicação com Kafka 171 | ./mvnw spring-boot:run -Dspring-boot.run.profiles=local-kafka 172 | 173 | # Parar infraestrutura Kafka 174 | ./scripts/stop-kafka-local.sh 175 | ``` 176 | 177 | **📖 Para setup detalhado do Kafka, veja: [docs/KAFKA_LOCAL_SETUP.md](docs/KAFKA_LOCAL_SETUP.md)** 178 | 179 | ### Desenvolvimento sem Kafka 180 | ```bash 181 | # Executar aplicação sem Kafka (padrão) 182 | ./mvnw spring-boot:run 183 | 184 | # Ou explicitamente 185 | ./mvnw spring-boot:run -Dspring-boot.run.profiles=local 186 | ``` 187 | 188 | ## 📊 Monitoramento 189 | 190 | ### Health Check 191 | ```http 192 | GET /actuator/health 193 | ``` 194 | 195 | ### Métricas 196 | ```http 197 | GET /actuator/metrics 198 | ``` 199 | 200 | ### Console H2 (Desenvolvimento) 201 | ``` 202 | URL: http://localhost:8080/h2-console 203 | JDBC URL: jdbc:h2:mem:orderhub 204 | User: sa 205 | Password: (vazio) 206 | ``` 207 | 208 | ## 🔄 Eventos Kafka 209 | 210 | ### Tópicos 211 | - `orders.created` - Pedidos criados 212 | - `payments.confirmed` - Pagamentos confirmados 213 | - `stock.reserved` - Estoque reservado 214 | - `invoice.generated` - Faturas geradas 215 | - `*.dlt` - Dead Letter Topics para falhas 216 | 217 | ### Exemplo de Evento 218 | ```json 219 | { 220 | "orderId": 123, 221 | "customerEmail": "user@example.com", 222 | "customerName": "João Silva", 223 | "totalAmount": 99.90, 224 | "paymentMethod": "PIX", 225 | "createdAt": "2024-01-15T10:30:00", 226 | "items": [ 227 | { 228 | "productSku": "PROD-001", 229 | "productName": "Produto Exemplo", 230 | "quantity": 2, 231 | "unitPrice": 49.95, 232 | "totalPrice": 99.90 233 | } 234 | ] 235 | } 236 | ``` 237 | 238 | ## 🧪 Testes 239 | 240 | ```bash 241 | # Executar todos os testes 242 | ./mvnw test 243 | 244 | # Testes com profile específico 245 | ./mvnw test -Dspring.profiles.active=test 246 | ``` 247 | 248 | ## 📝 Exemplo de Uso 249 | 250 | ### Criar Pedido 251 | ```bash 252 | curl -X POST http://localhost:8080/orders \ 253 | -H "Content-Type: application/json" \ 254 | -d '{ 255 | "customer": { 256 | "name": "João Silva", 257 | "email": "joao@example.com", 258 | "phone": "+5511999999999" 259 | }, 260 | "items": [ 261 | { 262 | "productName": "Produto A", 263 | "productSku": "PROD-A", 264 | "quantity": 2, 265 | "unitPrice": 49.99 266 | } 267 | ], 268 | "paymentMethod": "PIX" 269 | }' 270 | ``` 271 | 272 | ### Consultar Status 273 | ```bash 274 | curl http://localhost:8080/orders/1 275 | ``` 276 | 277 | ### Stream de Status 278 | ```bash 279 | curl -N http://localhost:8080/public/orders/1/status/stream 280 | ``` 281 | 282 | ## 🤝 Contribuição 283 | 284 | 1. Fork o projeto 285 | 2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`) 286 | 3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`) 287 | 4. Push para a branch (`git push origin feature/AmazingFeature`) 288 | 5. Abra um Pull Request 289 | 290 | ## 📄 Licença 291 | 292 | Este projeto está sob a licença MIT. Veja o arquivo `LICENSE` para mais detalhes. 293 | 294 | ## 📞 Suporte 295 | 296 | Para suporte e dúvidas: 297 | - 📧 Email: support@kipperdev.com 298 | - 📱 GitHub Issues: [Criar Issue](https://github.com/kipperdev/orderhub/issues) 299 | - 📖 Documentação: [Wiki do Projeto](https://github.com/kipperdev/orderhub/wiki) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | # Zookeeper for Kafka 5 | zookeeper: 6 | image: confluentinc/cp-zookeeper:7.4.0 7 | hostname: zookeeper 8 | container_name: orderhub-zookeeper 9 | ports: 10 | - "2181:2181" 11 | environment: 12 | ZOOKEEPER_CLIENT_PORT: 2181 13 | ZOOKEEPER_TICK_TIME: 2000 14 | networks: 15 | - orderhub-network 16 | 17 | # Kafka 18 | kafka: 19 | image: confluentinc/cp-kafka:7.4.0 20 | hostname: kafka 21 | container_name: orderhub-kafka 22 | depends_on: 23 | - zookeeper 24 | ports: 25 | - "9092:9092" 26 | - "9101:9101" 27 | environment: 28 | KAFKA_BROKER_ID: 1 29 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 30 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 31 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 32 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 33 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 34 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 35 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 36 | KAFKA_JMX_PORT: 9101 37 | KAFKA_JMX_HOSTNAME: localhost 38 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 39 | networks: 40 | - orderhub-network 41 | 42 | # PostgreSQL Database 43 | postgres: 44 | image: postgres:15-alpine 45 | container_name: orderhub-postgres 46 | ports: 47 | - "5432:5432" 48 | environment: 49 | POSTGRES_DB: orderhub 50 | POSTGRES_USER: orderhub 51 | POSTGRES_PASSWORD: password 52 | volumes: 53 | - postgres_data:/var/lib/postgresql/data 54 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 55 | networks: 56 | - orderhub-network 57 | 58 | # Kafka UI (Optional) 59 | kafka-ui: 60 | image: provectuslabs/kafka-ui:latest 61 | container_name: orderhub-kafka-ui 62 | depends_on: 63 | - kafka 64 | ports: 65 | - "8090:8080" 66 | environment: 67 | KAFKA_CLUSTERS_0_NAME: local 68 | KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 69 | KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 70 | networks: 71 | - orderhub-network 72 | 73 | # Redis (Optional - for caching) 74 | redis: 75 | image: redis:7-alpine 76 | container_name: orderhub-redis 77 | ports: 78 | - "6379:6379" 79 | command: redis-server --appendonly yes 80 | volumes: 81 | - redis_data:/data 82 | networks: 83 | - orderhub-network 84 | 85 | # OrderHub Application 86 | orderhub-app: 87 | build: 88 | context: . 89 | dockerfile: Dockerfile 90 | container_name: orderhub-app 91 | depends_on: 92 | - postgres 93 | - kafka 94 | ports: 95 | - "8080:8080" 96 | environment: 97 | SPRING_PROFILES_ACTIVE: docker 98 | DATABASE_URL: jdbc:postgresql://postgres:5432/orderhub 99 | DATABASE_USERNAME: orderhub 100 | DATABASE_PASSWORD: password 101 | KAFKA_BOOTSTRAP_SERVERS: kafka:29092 102 | ABACATE_MOCK_ENABLED: 'true' 103 | networks: 104 | - orderhub-network 105 | profiles: 106 | - full 107 | 108 | volumes: 109 | postgres_data: 110 | redis_data: 111 | 112 | networks: 113 | orderhub-network: 114 | driver: bridge 115 | 116 | # Para executar apenas a infraestrutura (sem a aplicação): 117 | # docker-compose up -d 118 | 119 | # Para executar com a aplicação: 120 | # docker-compose --profile full up -d -------------------------------------------------------------------------------- /docs/ABACATEPAY_SETUP.md: -------------------------------------------------------------------------------- 1 | # Configuração do Abacatepay 2 | 3 | Este documento descreve como configurar a integração com o gateway de pagamentos Abacatepay. 4 | 5 | ## Variáveis de Ambiente Obrigatórias 6 | 7 | Para usar a integração com o Abacatepay em produção, você deve configurar as seguintes variáveis de ambiente: 8 | 9 | ### API Configuration 10 | 11 | ```bash 12 | # Token de autenticação da API do Abacatepay 13 | ABACATEPAY_API_TOKEN=seu_token_aqui 14 | 15 | # URL base da API (opcional, padrão: https://api.abacatepay.com) 16 | ABACATEPAY_API_BASE_URL=https://api.abacatepay.com 17 | 18 | # Habilitar/desabilitar modo mock (padrão: true para dev, false para prod) 19 | ABACATEPAY_API_MOCK_ENABLED=false 20 | ``` 21 | 22 | ### Webhook Configuration 23 | 24 | ```bash 25 | # Secret para validação de assinatura dos webhooks 26 | ABACATEPAY_WEBHOOK_SECRET=seu_webhook_secret_aqui 27 | 28 | # Habilitar validação de assinatura (padrão: false para dev, true para prod) 29 | ABACATEPAY_WEBHOOK_SIGNATURE_ENABLED=true 30 | ``` 31 | 32 | ## Configuração por Ambiente 33 | 34 | ### Desenvolvimento 35 | 36 | ```bash 37 | ABACATEPAY_API_TOKEN=mock-token 38 | ABACATEPAY_API_MOCK_ENABLED=true 39 | ABACATEPAY_WEBHOOK_SIGNATURE_ENABLED=false 40 | ``` 41 | 42 | ### Produção 43 | 44 | ```bash 45 | ABACATEPAY_API_TOKEN=seu_token_de_producao 46 | ABACATEPAY_API_MOCK_ENABLED=false 47 | ABACATEPAY_WEBHOOK_SECRET=seu_webhook_secret_de_producao 48 | ABACATEPAY_WEBHOOK_SIGNATURE_ENABLED=true 49 | ``` 50 | 51 | ## Endpoints da Integração 52 | 53 | ### Webhook 54 | 55 | O endpoint para receber webhooks do Abacatepay é: 56 | 57 | ``` 58 | POST /api/webhooks/abacatepay 59 | ``` 60 | 61 | Configure este endpoint no painel do Abacatepay para receber notificações de pagamento. 62 | 63 | ## Funcionalidades Implementadas 64 | 65 | - ✅ Criação de cobranças (billing) 66 | - ✅ Gerenciamento de clientes 67 | - ✅ Processamento de webhooks 68 | - ✅ Validação de assinatura de webhooks 69 | - ✅ Modo mock para desenvolvimento 70 | - ✅ Configuração via variáveis de ambiente 71 | 72 | ## Segurança 73 | 74 | - Todas as credenciais são obtidas via variáveis de ambiente 75 | - Nenhuma informação sensível está hardcoded no código 76 | - Validação de assinatura HMAC-SHA256 para webhooks 77 | - Logs de segurança para tentativas de webhook inválidas 78 | 79 | ## Como Obter as Credenciais 80 | 81 | 1. Acesse o painel do Abacatepay 82 | 2. Vá para a seção de API/Integrações 83 | 3. Gere um token de API 84 | 4. Configure o webhook secret 85 | 5. Configure o endpoint de webhook: `https://seu-dominio.com/api/webhooks/abacatepay` 86 | 87 | ## Troubleshooting 88 | 89 | ### Erro de Autenticação 90 | 91 | Verifique se a variável `ABACATEPAY_API_TOKEN` está configurada corretamente. 92 | 93 | ### Webhook não Funciona 94 | 95 | 1. Verifique se o endpoint está acessível publicamente 96 | 2. Confirme se o `ABACATEPAY_WEBHOOK_SECRET` está correto 97 | 3. Verifique os logs da aplicação para erros de validação 98 | 99 | ### Modo Mock 100 | 101 | Para testar sem fazer chamadas reais para a API: 102 | 103 | ```bash 104 | ABACATEPAY_API_MOCK_ENABLED=true 105 | ``` -------------------------------------------------------------------------------- /docs/KAFKA_LOCAL_SETUP.md: -------------------------------------------------------------------------------- 1 | # Kafka Local Development Setup 2 | 3 | This guide explains how to set up and run Apache Kafka locally for OrderHub development, ensuring proper integration with the application. 4 | 5 | ## Quick Start 6 | 7 | ### 1. Start Kafka Infrastructure 8 | 9 | The project includes a `docker-compose.yml` file with all necessary Kafka infrastructure. To start only the Kafka services: 10 | 11 | ```bash 12 | # Start Kafka, Zookeeper, and Kafka UI 13 | docker-compose up -d zookeeper kafka kafka-ui 14 | ``` 15 | 16 | This will start: 17 | - **Zookeeper** on port `2181` 18 | - **Kafka** on port `9092` 19 | - **Kafka UI** on port `8090` (http://localhost:8090) 20 | 21 | ### 2. Run the Application with Kafka 22 | 23 | Use the `local-kafka` profile to enable Kafka integration: 24 | 25 | ```bash 26 | # Using Maven 27 | mvn spring-boot:run -Dspring-boot.run.profiles=local-kafka 28 | 29 | # Or using Java directly 30 | java -jar target/orderhub-0.0.1-SNAPSHOT.jar --spring.profiles.active=local-kafka 31 | ``` 32 | 33 | ## Configuration Details 34 | 35 | ### Application Profiles 36 | 37 | The application has different profiles for Kafka: 38 | 39 | - **`local`**: Kafka disabled (default for local development) 40 | - **`local-kafka`**: Kafka enabled with local configuration 41 | - **`prod`**: Kafka enabled for production 42 | - **`docker`**: Kafka enabled for Docker environment 43 | 44 | ### Kafka Configuration (local-kafka profile) 45 | 46 | ```yaml 47 | spring: 48 | kafka: 49 | enabled: true 50 | bootstrap-servers: localhost:9092 51 | consumer: 52 | group-id: orderhub-local-group 53 | auto-offset-reset: earliest 54 | producer: 55 | key-serializer: org.apache.kafka.common.serialization.StringSerializer 56 | value-serializer: org.springframework.kafka.support.serializer.JsonSerializer 57 | ``` 58 | 59 | ## Topics and Events 60 | 61 | The application automatically creates the following topics: 62 | 63 | ### Main Topics 64 | - `orders.created` - Order creation events 65 | - `payments.confirmed` - Payment confirmation events 66 | - `stock.reserved` - Stock reservation events 67 | - `invoice.generated` - Invoice generation events 68 | 69 | ### Dead Letter Topics (DLT) 70 | - `orders.created.dlt` - Failed order creation events 71 | - `payments.confirmed.dlt` - Failed payment confirmation events 72 | - `stock.reserved.dlt` - Failed stock reservation events 73 | - `invoice.generated.dlt` - Failed invoice generation events 74 | 75 | ## Testing the Integration 76 | 77 | ### Quick Test Script 78 | 79 | Use the provided test script to quickly validate the entire setup: 80 | 81 | ```bash 82 | # Make sure Kafka is running and application is started with local-kafka profile 83 | ./scripts/start-kafka-local.sh 84 | mvn spring-boot:run -Dspring-boot.run.profiles=local-kafka 85 | 86 | # In another terminal, run the test 87 | ./scripts/test-kafka-integration.sh 88 | ``` 89 | 90 | This script will: 91 | - Create a test customer and order 92 | - Verify event publishing 93 | - Check application logs 94 | - Provide guidance on monitoring events 95 | 96 | ### Manual Testing 97 | 98 | #### 1. Verify Kafka is Running 99 | 100 | ```bash 101 | # Check if Kafka container is running 102 | docker ps | grep kafka 103 | 104 | # Check Kafka logs 105 | docker logs orderhub-kafka 106 | ``` 107 | 108 | ### 2. Access Kafka UI 109 | 110 | Open http://localhost:8090 in your browser to: 111 | - View topics 112 | - Monitor messages 113 | - Check consumer groups 114 | - Inspect topic configurations 115 | 116 | ### 3. Test Event Publishing 117 | 118 | Create an order through the API to test event publishing: 119 | 120 | ```bash 121 | # Create a customer first 122 | curl -X POST http://localhost:8080/api/customers \ 123 | -H "Content-Type: application/json" \ 124 | -d '{ 125 | "name": "John Doe", 126 | "email": "john@example.com", 127 | "phone": "+1234567890" 128 | }' 129 | 130 | # Create an order (replace {customerId} with actual ID) 131 | curl -X POST http://localhost:8080/api/orders \ 132 | -H "Content-Type: application/json" \ 133 | -d '{ 134 | "customerId": "{customerId}", 135 | "items": [ 136 | { 137 | "productName": "Test Product", 138 | "quantity": 2, 139 | "price": 29.99 140 | } 141 | ] 142 | }' 143 | ``` 144 | 145 | ### 4. Monitor Events 146 | 147 | After creating an order, you should see: 148 | 1. An `OrderCreatedEvent` in the `orders.created` topic 149 | 2. Application logs showing event publishing 150 | 3. Events visible in Kafka UI 151 | 152 | ### 5. Check Application Logs 153 | 154 | Look for these log messages indicating successful Kafka integration: 155 | 156 | ``` 157 | # Event publishing 158 | Publishing OrderCreatedEvent for order: {orderId} 159 | Event published successfully to topic: orders.created 160 | 161 | # Event consumption (if applicable) 162 | Received event from topic: stock.reserved 163 | Processing StockReservedEvent for order: {orderId} 164 | ``` 165 | 166 | ## Troubleshooting 167 | 168 | ### Common Issues 169 | 170 | #### 1. Connection Refused Error 171 | ``` 172 | org.apache.kafka.common.errors.TimeoutException: Failed to update metadata 173 | ``` 174 | 175 | **Solution**: Ensure Kafka is running and accessible on localhost:9092 176 | ```bash 177 | docker-compose up -d kafka 178 | ``` 179 | 180 | #### 2. Topic Creation Issues 181 | ``` 182 | org.apache.kafka.common.errors.TopicExistsException 183 | ``` 184 | 185 | **Solution**: This is normal - topics are created automatically if they don't exist. 186 | 187 | #### 3. Serialization Errors 188 | ``` 189 | com.fasterxml.jackson.core.JsonProcessingException 190 | ``` 191 | 192 | **Solution**: Check that event classes have proper JSON annotations and are in the trusted packages. 193 | 194 | #### 4. Consumer Group Issues 195 | ``` 196 | org.apache.kafka.clients.consumer.CommitFailedException 197 | ``` 198 | 199 | **Solution**: Restart the application to reset the consumer group. 200 | 201 | ### Useful Commands 202 | 203 | ```bash 204 | # Stop all Kafka services 205 | docker-compose down 206 | 207 | # View Kafka logs 208 | docker logs orderhub-kafka -f 209 | 210 | # Access Kafka container 211 | docker exec -it orderhub-kafka bash 212 | 213 | # List topics (from inside Kafka container) 214 | kafka-topics --bootstrap-server localhost:9092 --list 215 | 216 | # Consume messages from a topic (from inside Kafka container) 217 | kafka-console-consumer --bootstrap-server localhost:9092 --topic orders.created --from-beginning 218 | ``` 219 | 220 | ## Development Workflow 221 | 222 | ### Starting Development 223 | 1. Start Kafka infrastructure: `docker-compose up -d zookeeper kafka kafka-ui` 224 | 2. Run application: `mvn spring-boot:run -Dspring-boot.run.profiles=local-kafka` 225 | 3. Open Kafka UI: http://localhost:8090 226 | 4. Test API endpoints and monitor events 227 | 228 | ### Stopping Development 229 | 1. Stop application: `Ctrl+C` 230 | 2. Stop Kafka: `docker-compose down` 231 | 232 | ## Event Classes 233 | 234 | The application uses these event classes for Kafka messaging: 235 | 236 | - `OrderCreatedEvent` - Published when an order is created 237 | - `PaymentConfirmedEvent` - Published when payment is confirmed 238 | - `StockReservedEvent` - Consumed when stock is reserved 239 | - `InvoiceGeneratedEvent` - Consumed when invoice is generated 240 | 241 | All events are located in the `com.kipperdev.orderhub.event` package. 242 | 243 | ## Configuration Reference 244 | 245 | ### Environment Variables (Optional) 246 | 247 | You can override default configurations using environment variables: 248 | 249 | ```bash 250 | # Kafka bootstrap servers 251 | export KAFKA_BOOTSTRAP_SERVERS=localhost:9092 252 | 253 | # Consumer group ID 254 | export KAFKA_CONSUMER_GROUP_ID=orderhub-local-group 255 | 256 | # Enable/disable Kafka 257 | export SPRING_KAFKA_ENABLED=true 258 | ``` 259 | 260 | ### Application Properties Override 261 | 262 | Create `application-local-kafka.properties` for custom local settings: 263 | 264 | ```properties 265 | # Custom Kafka settings 266 | spring.kafka.bootstrap-servers=localhost:9092 267 | spring.kafka.consumer.group-id=my-custom-group 268 | spring.kafka.consumer.auto-offset-reset=latest 269 | 270 | # Enable debug logging 271 | logging.level.org.apache.kafka=DEBUG 272 | logging.level.org.springframework.kafka=DEBUG 273 | ``` 274 | 275 | ## Next Steps 276 | 277 | - Explore the Kafka UI to understand message flow 278 | - Implement custom event handlers 279 | - Add monitoring and alerting for production use 280 | - Consider using Kafka Streams for complex event processing 281 | 282 | For more information about the OrderHub application, see the main [README.md](../README.md). -------------------------------------------------------------------------------- /docs/KAFKA_ORDER_CANCELLED_EVENT.md: -------------------------------------------------------------------------------- 1 | # Implementação do OrderCancelledEvent no Kafka 2 | 3 | ## 📋 Visão Geral 4 | 5 | Este guia mostra como implementar o evento `OrderCancelledEvent` no sistema Kafka do OrderHub, incluindo configuração de tópicos, publicação e consumo do evento. 6 | 7 | ## 🎯 Cenário de Uso 8 | 9 | O `OrderCancelledEvent` é disparado quando: 10 | - Um pedido é cancelado pelo cliente 11 | - Uma saga falha e precisa cancelar o pedido (rollback) 12 | - O sistema cancela automaticamente por timeout ou falha 13 | 14 | ## 🚀 Implementação Passo a Passo 15 | 16 | ### 1. Configurar Tópico Kafka 17 | 18 | Adicione a configuração do tópico no `KafkaConfig.java`: 19 | 20 | ```java 21 | @Bean 22 | public NewTopic orderCancelledTopic() { 23 | return TopicBuilder.name("order.cancelled") 24 | .partitions(3) 25 | .replicas(1) 26 | .build(); 27 | } 28 | 29 | @Bean 30 | public NewTopic orderCancelledDltTopic() { 31 | return TopicBuilder.name("order.cancelled.DLT") 32 | .partitions(3) 33 | .replicas(1) 34 | .build(); 35 | } 36 | ``` 37 | 38 | ### 2. Classe do Evento (Já Implementada) 39 | 40 | O evento `OrderCancelledEvent` já foi criado em: 41 | ``` 42 | src/main/java/com/kipperdev/orderhub/event/OrderCancelledEvent.java 43 | ``` 44 | 45 | **Estrutura do evento:** 46 | ```java 47 | public class OrderCancelledEvent { 48 | private Long orderId; 49 | private String reason; 50 | private String compensationId; 51 | private LocalDateTime cancelledAt; 52 | private String status; 53 | private String cancellationType; 54 | private boolean customerNotified; 55 | private String notes; 56 | } 57 | ``` 58 | 59 | ### 3. Publicar o Evento 60 | 61 | Adicione método no `KafkaProducerService.java`: 62 | 63 | ```java 64 | public void publishOrderCancelledEvent(OrderCancelledEvent event) { 65 | try { 66 | String eventJson = objectMapper.writeValueAsString(event); 67 | 68 | kafkaTemplate.send("order.cancelled", "order-" + event.getOrderId(), eventJson) 69 | .whenComplete((result, ex) -> { 70 | if (ex == null) { 71 | log.info("✅ Evento OrderCancelled publicado com sucesso para pedido: {}", 72 | event.getOrderId()); 73 | } else { 74 | log.error("❌ Erro ao publicar evento OrderCancelled para pedido {}: {}", 75 | event.getOrderId(), ex.getMessage()); 76 | } 77 | }); 78 | 79 | } catch (JsonProcessingException e) { 80 | log.error("❌ Erro na serialização do OrderCancelledEvent para pedido {}: {}", 81 | event.getOrderId(), e.getMessage()); 82 | } 83 | } 84 | ``` 85 | 86 | ### 4. Consumir o Evento 87 | 88 | Adicione listener no `KafkaConsumerService.java`: 89 | 90 | ```java 91 | @KafkaListener( 92 | topics = "order.cancelled", 93 | groupId = "order-service", 94 | containerFactory = "kafkaListenerContainerFactory" 95 | ) 96 | @RetryableTopic( 97 | attempts = "3", 98 | backoff = @Backoff(delay = 1000, multiplier = 2.0), 99 | dltStrategy = DltStrategy.FAIL_ON_ERROR, 100 | include = {Exception.class} 101 | ) 102 | public void handleOrderCancelledEvent( 103 | OrderCancelledEvent event, 104 | Acknowledgment acknowledgment) { 105 | 106 | try { 107 | log.info("📨 Processando evento OrderCancelled para pedido: {} - Motivo: {}", 108 | event.getOrderId(), event.getReason()); 109 | 110 | // Processar cancelamento do pedido 111 | processOrderCancellation(event); 112 | 113 | // Notificar cliente se necessário 114 | if (event.isCustomerNotified()) { 115 | notifyCustomerAboutCancellation(event); 116 | } 117 | 118 | // Confirmar processamento 119 | acknowledgment.acknowledge(); 120 | 121 | log.info("✅ Evento OrderCancelled processado com sucesso para pedido: {}", 122 | event.getOrderId()); 123 | 124 | } catch (Exception e) { 125 | log.error("❌ Erro ao processar evento OrderCancelled para pedido {}: {}", 126 | event.getOrderId(), e.getMessage()); 127 | throw e; // Rejeita a mensagem para retry 128 | } 129 | } 130 | 131 | @KafkaListener( 132 | topics = "order.cancelled.DLT", 133 | groupId = "order-service-dlt" 134 | ) 135 | public void handleOrderCancelledDltEvent( 136 | OrderCancelledEvent event, 137 | Acknowledgment acknowledgment) { 138 | 139 | log.error("💀 Evento OrderCancelled enviado para DLT - Pedido: {} - Motivo: {}", 140 | event.getOrderId(), event.getReason()); 141 | 142 | // Implementar lógica de tratamento de erro crítico 143 | // Ex: alertas, notificações para equipe de suporte 144 | 145 | acknowledgment.acknowledge(); 146 | } 147 | 148 | private void processOrderCancellation(OrderCancelledEvent event) { 149 | // Implementar lógica de cancelamento 150 | // Ex: atualizar status no banco, liberar recursos, etc. 151 | } 152 | 153 | private void notifyCustomerAboutCancellation(OrderCancelledEvent event) { 154 | // Implementar notificação ao cliente 155 | // Ex: email, SMS, push notification 156 | } 157 | ``` 158 | 159 | ### 5. Usar no OrderService 160 | 161 | Adicione método no `OrderService.java`: 162 | 163 | ```java 164 | @Autowired 165 | private KafkaProducerService kafkaProducerService; 166 | 167 | public void cancelOrder(Long orderId, String reason, String cancellationType) { 168 | try { 169 | // Buscar pedido 170 | Order order = orderRepository.findById(orderId) 171 | .orElseThrow(() -> new OrderNotFoundException(orderId)); 172 | 173 | // Atualizar status 174 | order.setStatus(OrderStatus.CANCELLED); 175 | order.setCancellationReason(reason); 176 | order.setCancelledAt(LocalDateTime.now()); 177 | 178 | orderRepository.save(order); 179 | 180 | // Criar e publicar evento 181 | OrderCancelledEvent event = new OrderCancelledEvent(); 182 | event.setOrderId(orderId); 183 | event.setReason(reason); 184 | event.setCancellationType(cancellationType); 185 | event.setCancelledAt(LocalDateTime.now()); 186 | event.setStatus("CANCELLED"); 187 | event.setCustomerNotified(true); 188 | 189 | kafkaProducerService.publishOrderCancelledEvent(event); 190 | 191 | log.info("🚫 Pedido {} cancelado com sucesso - Motivo: {}", orderId, reason); 192 | 193 | } catch (Exception e) { 194 | log.error("❌ Erro ao cancelar pedido {}: {}", orderId, e.getMessage()); 195 | throw new OrderCancellationException("Falha ao cancelar pedido: " + e.getMessage()); 196 | } 197 | } 198 | ``` 199 | 200 | ### 6. Endpoint REST 201 | 202 | Adicione endpoint no `OrderController.java`: 203 | 204 | ```java 205 | @PostMapping("/{orderId}/cancel") 206 | public ResponseEntity> cancelOrder( 207 | @PathVariable Long orderId, 208 | @RequestBody Map request) { 209 | 210 | try { 211 | String reason = request.getOrDefault("reason", "Cancelamento solicitado pelo cliente"); 212 | String cancellationType = request.getOrDefault("type", "CUSTOMER_REQUEST"); 213 | 214 | orderService.cancelOrder(orderId, reason, cancellationType); 215 | 216 | Map response = new HashMap<>(); 217 | response.put("message", "Pedido cancelado com sucesso"); 218 | response.put("orderId", orderId.toString()); 219 | response.put("status", "CANCELLED"); 220 | 221 | return ResponseEntity.ok(response); 222 | 223 | } catch (OrderNotFoundException e) { 224 | return ResponseEntity.notFound().build(); 225 | } catch (Exception e) { 226 | Map error = new HashMap<>(); 227 | error.put("error", "Erro ao cancelar pedido: " + e.getMessage()); 228 | return ResponseEntity.badRequest().body(error); 229 | } 230 | } 231 | ``` 232 | 233 | ## 🧪 Testando a Implementação 234 | 235 | ### 1. Script de Teste 236 | 237 | ```bash 238 | #!/bin/bash 239 | # test-order-cancelled-event.sh 240 | 241 | echo "🧪 Testando OrderCancelledEvent" 242 | echo "==============================" 243 | 244 | # 1. Criar um pedido 245 | echo "📦 Criando pedido..." 246 | ORDER_RESPONSE=$(curl -s -X POST http://localhost:8080/api/orders \ 247 | -H "Content-Type: application/json" \ 248 | -d '{ 249 | "customerId": 1, 250 | "items": [ 251 | { 252 | "productName": "Produto Teste", 253 | "productSku": "TEST-001", 254 | "quantity": 1, 255 | "unitPrice": 29.99 256 | } 257 | ] 258 | }') 259 | 260 | ORDER_ID=$(echo $ORDER_RESPONSE | jq -r '.id') 261 | echo "✅ Pedido criado: $ORDER_ID" 262 | 263 | # 2. Cancelar o pedido 264 | echo "🚫 Cancelando pedido..." 265 | CANCEL_RESPONSE=$(curl -s -X POST "http://localhost:8080/api/orders/$ORDER_ID/cancel" \ 266 | -H "Content-Type: application/json" \ 267 | -d '{ 268 | "reason": "Teste de cancelamento via API", 269 | "type": "CUSTOMER_REQUEST" 270 | }') 271 | 272 | echo "📋 Resposta do cancelamento:" 273 | echo $CANCEL_RESPONSE | jq . 274 | 275 | # 3. Verificar status do pedido 276 | echo "🔍 Verificando status do pedido..." 277 | sleep 2 278 | curl -s "http://localhost:8080/api/orders/$ORDER_ID" | jq '.status' 279 | 280 | echo "✅ Teste concluído!" 281 | ``` 282 | 283 | ### 2. Monitorar Tópico Kafka 284 | 285 | ```bash 286 | # Consumir mensagens do tópico 287 | kafka-console-consumer.sh \ 288 | --bootstrap-server localhost:9092 \ 289 | --topic order.cancelled \ 290 | --from-beginning \ 291 | --property print.key=true 292 | ``` 293 | 294 | ### 3. Verificar Logs 295 | 296 | ```bash 297 | # Acompanhar logs da aplicação 298 | tail -f logs/application.log | grep "OrderCancelled" 299 | ``` 300 | 301 | ## 📊 Monitoramento 302 | 303 | ### Métricas Importantes 304 | 305 | - **Taxa de cancelamento**: Quantos pedidos são cancelados 306 | - **Motivos de cancelamento**: Principais causas 307 | - **Tempo de processamento**: Latência do evento 308 | - **Falhas de processamento**: Mensagens que vão para DLT 309 | 310 | ### Alertas Recomendados 311 | 312 | - Alto volume de cancelamentos em pouco tempo 313 | - Falhas recorrentes no processamento 314 | - Mensagens acumuladas no DLT 315 | - Timeout no processamento de eventos 316 | 317 | ## 🔧 Configurações Adicionais 318 | 319 | ### application.yml 320 | 321 | ```yaml 322 | kafka: 323 | enabled: true 324 | topics: 325 | order-cancelled: 326 | partitions: 3 327 | replication-factor: 1 328 | retention-ms: 604800000 # 7 dias 329 | ``` 330 | 331 | ### Configuração de Retry 332 | 333 | ```java 334 | @RetryableTopic( 335 | attempts = "5", 336 | backoff = @Backoff(delay = 2000, multiplier = 2.0, maxDelay = 30000), 337 | dltStrategy = DltStrategy.FAIL_ON_ERROR, 338 | include = {Exception.class}, 339 | exclude = {OrderNotFoundException.class} 340 | ) 341 | ``` -------------------------------------------------------------------------------- /docs/architecture-diagram.md: -------------------------------------------------------------------------------- 1 | # Arquitetura da Aplicação OrderHub 2 | 3 | ## Diagrama de Arquitetura 4 | 5 | ```mermaid 6 | graph TB 7 | %% External Systems 8 | Client["🧑‍💻 Cliente/Frontend"] 9 | AbacatePay["💳 AbacatePay Gateway"] 10 | 11 | %% OrderHub Application 12 | subgraph "OrderHub Application" 13 | %% API Layer 14 | OrderAPI["📋 Order API\n/api/orders"] 15 | WebhookAPI["🔗 Webhook API\n/webhook/payment"] 16 | 17 | %% Service Layer 18 | OrderService["⚙️ Order Service"] 19 | PaymentService["💰 Payment Service"] 20 | KafkaProducer["📤 Kafka Producer Service"] 21 | KafkaConsumer["📥 Kafka Consumer Service"] 22 | 23 | %% Database 24 | Database[("🗄️ PostgreSQL\nDatabase")] 25 | end 26 | 27 | %% Kafka Infrastructure 28 | subgraph "Apache Kafka" 29 | OrderCreatedTopic["📨 orders.created\nTopic"] 30 | PaymentConfirmedTopic["✅ payments.confirmed\nTopic"] 31 | StockReservedTopic["📦 stock.reserved\nTopic"] 32 | InvoiceGeneratedTopic["🧾 invoice.generated\nTopic"] 33 | end 34 | 35 | %% Other OrderHub Consumers 36 | subgraph "OrderHub Consumers" 37 | StockService["📦 Stock Service"] 38 | InvoiceService["🧾 Invoice Service"] 39 | NotificationService["📧 Notification Service"] 40 | end 41 | 42 | %% Flow 1: Order Creation 43 | Client -->|"1. POST /api/orders\n{customer, items}"| OrderAPI 44 | OrderAPI --> OrderService 45 | OrderService -->|"2. Create Payment Request"| AbacatePay 46 | AbacatePay -->|"3. Payment URL + Transaction ID"| OrderService 47 | OrderService -->|"4. Save Order\n(status: PENDING)"| Database 48 | OrderService --> KafkaProducer 49 | KafkaProducer -->|"5. Publish OrderCreatedEvent"| OrderCreatedTopic 50 | OrderAPI -->|"6. Return Order + Payment URL"| Client 51 | 52 | %% Flow 2: Payment Processing via Webhook 53 | AbacatePay -->|"7. Payment Status Update\nWebhook"| WebhookAPI 54 | WebhookAPI --> PaymentService 55 | PaymentService -->|"8. Update Order Status\n(PAID/FAILED)"| Database 56 | PaymentService --> KafkaProducer 57 | KafkaProducer -->|"9. Publish PaymentConfirmedEvent"| PaymentConfirmedTopic 58 | 59 | %% Flow 3: Event Processing 60 | OrderCreatedTopic --> KafkaConsumer 61 | PaymentConfirmedTopic --> KafkaConsumer 62 | 63 | %% Flow 4: Downstream Services 64 | PaymentConfirmedTopic --> StockService 65 | StockService -->|"Reserve Stock"| StockReservedTopic 66 | 67 | StockReservedTopic --> InvoiceService 68 | InvoiceService -->|"Generate Invoice"| InvoiceGeneratedTopic 69 | 70 | PaymentConfirmedTopic --> NotificationService 71 | InvoiceGeneratedTopic --> NotificationService 72 | 73 | %% Styling 74 | classDef external fill:#e1f5fe,stroke:#01579b,stroke-width:2px 75 | classDef api fill:#f3e5f5,stroke:#4a148c,stroke-width:2px 76 | classDef service fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px 77 | classDef kafka fill:#fff3e0,stroke:#e65100,stroke-width:2px 78 | classDef database fill:#fce4ec,stroke:#880e4f,stroke-width:2px 79 | classDef consumer fill:#f1f8e9,stroke:#33691e,stroke-width:2px 80 | 81 | class Client,AbacatePay external 82 | class OrderAPI,WebhookAPI api 83 | class OrderService,PaymentService,KafkaProducer,KafkaConsumer service 84 | class OrderCreatedTopic,PaymentConfirmedTopic,StockReservedTopic,InvoiceGeneratedTopic kafka 85 | class Database database 86 | class StockService,InvoiceService,NotificationService consumer 87 | ``` 88 | 89 | ## Fluxo Detalhado 90 | 91 | ### 1. Criação de Pedido 92 | 1. **Cliente** envia requisição POST para `/api/orders` com dados do pedido 93 | 2. **Order Service** processa a requisição e cria uma solicitação de pagamento no **AbacatePay** 94 | 3. **AbacatePay** retorna URL de pagamento e Transaction ID 95 | 4. **Order Service** salva o pedido no banco com status `PENDING` 96 | 5. **Kafka Producer** publica evento `OrderCreatedEvent` no tópico `orders.created` 97 | 6. API retorna o pedido criado com URL de pagamento para o cliente 98 | 99 | ### 2. Processamento de Pagamento 100 | 7. **AbacatePay** envia webhook para `/webhook/payment` quando status do pagamento muda 101 | 8. **Payment Service** processa o webhook e atualiza status do pedido no banco 102 | 9. **Kafka Producer** publica evento `PaymentConfirmedEvent` no tópico `payments.confirmed` 103 | 104 | ### 3. Processamento de Eventos 105 | - **Kafka Consumers** da própria aplicação processam eventos internamente 106 | - **Stock Service** escuta `payments.confirmed` e reserva estoque 107 | - **Invoice Service** escuta `stock.reserved` e gera nota fiscal 108 | - **Notification Service** escuta múltiplos eventos para enviar notificações 109 | 110 | ## Características da Arquitetura 111 | 112 | ### ✅ Vantagens 113 | - **Event-Driven**: Desacoplamento entre serviços via Kafka 114 | - **Resiliente**: Dead Letter Topics para tratamento de falhas 115 | - **Escalável**: Particionamento de tópicos Kafka 116 | - **Observável**: Logging detalhado em todos os componentes 117 | - **Idempotente**: Configuração de producers para evitar duplicatas 118 | 119 | ### 🔧 Componentes Técnicos 120 | - **Spring Boot 3.5.4** com Java 21 121 | - **Apache Kafka** para messaging 122 | - **PostgreSQL** como banco principal 123 | - **AbacatePay** como gateway de pagamento 124 | - **Spring Security** para autenticação 125 | - **Docker** para containerização 126 | 127 | ### 📊 Tópicos Kafka 128 | - `orders.created` - Eventos de pedidos criados 129 | - `payments.confirmed` - Confirmações de pagamento 130 | - `stock.reserved` - Reservas de estoque 131 | - `invoice.generated` - Notas fiscais geradas 132 | - Tópicos DLT para cada um dos acima 133 | 134 | ### 🔄 Padrões Implementados 135 | - **Saga Pattern** para transações distribuídas 136 | - **Event Sourcing** para auditoria de eventos 137 | - **CQRS** separação de comandos e consultas 138 | - **Circuit Breaker** para resiliência 139 | - **Webhook Pattern** para integração com gateway de pagamento -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | -- Inicialização do banco de dados OrderHub 2 | -- Este script é executado automaticamente pelo Docker Compose 3 | 4 | -- Criar extensões necessárias 5 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 6 | 7 | -- Criar schema se não existir 8 | CREATE SCHEMA IF NOT EXISTS orderhub; 9 | 10 | -- Definir schema padrão 11 | SET search_path TO orderhub, public; 12 | 13 | -- Comentários das tabelas (serão criadas automaticamente pelo Hibernate) 14 | -- Tabela: customers 15 | -- Armazena informações dos clientes 16 | 17 | -- Tabela: orders 18 | -- Armazena informações dos pedidos 19 | 20 | -- Tabela: order_items 21 | -- Armazena itens dos pedidos 22 | 23 | -- Inserir dados de exemplo (opcional) 24 | -- Estes dados serão inseridos apenas se as tabelas existirem 25 | 26 | -- Função para inserir dados de exemplo 27 | CREATE OR REPLACE FUNCTION insert_sample_data() 28 | RETURNS void AS $$ 29 | BEGIN 30 | -- Verificar se a tabela customers existe 31 | IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'customers') THEN 32 | -- Inserir clientes de exemplo 33 | INSERT INTO customers (id, name, email, phone, created_at, updated_at) VALUES 34 | (1, 'João Silva', 'joao.silva@email.com', '+5511999999999', NOW(), NOW()), 35 | (2, 'Maria Santos', 'maria.santos@email.com', '+5511888888888', NOW(), NOW()), 36 | (3, 'Pedro Oliveira', 'pedro.oliveira@email.com', '+5511777777777', NOW(), NOW()) 37 | ON CONFLICT (email) DO NOTHING; 38 | 39 | RAISE NOTICE 'Dados de exemplo inseridos com sucesso!'; 40 | ELSE 41 | RAISE NOTICE 'Tabelas ainda não foram criadas pelo Hibernate.'; 42 | END IF; 43 | END; 44 | $$ LANGUAGE plpgsql; 45 | 46 | -- Comentário sobre a execução 47 | -- A função insert_sample_data() pode ser chamada manualmente após a aplicação criar as tabelas: 48 | -- SELECT insert_sample_data(); 49 | 50 | -- Configurações de performance para desenvolvimento 51 | ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'; 52 | ALTER SYSTEM SET log_statement = 'all'; 53 | ALTER SYSTEM SET log_min_duration_statement = 1000; 54 | 55 | -- Recarregar configurações 56 | SELECT pg_reload_conf(); 57 | 58 | RAISE NOTICE 'Banco de dados OrderHub inicializado com sucesso!'; 59 | RAISE NOTICE 'Para inserir dados de exemplo, execute: SELECT insert_sample_data();'; 60 | RAISE NOTICE 'Aguardando a aplicação Spring Boot criar as tabelas...'; -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.4 9 | 10 | 11 | com.kipperdev 12 | orderhub 13 | 0.0.1-SNAPSHOT 14 | orderhub 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 21 31 | 2025.0.0 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-data-jpa 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-validation 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-web 45 | 46 | 47 | 48 | org.springframework.cloud 49 | spring-cloud-starter-openfeign 50 | 51 | 52 | org.springframework.kafka 53 | spring-kafka 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-security 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-actuator 62 | 63 | 64 | com.h2database 65 | h2 66 | runtime 67 | 68 | 69 | org.mapstruct 70 | mapstruct 71 | 1.5.5.Final 72 | 73 | 74 | org.mapstruct 75 | mapstruct-processor 76 | 1.5.5.Final 77 | provided 78 | 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-devtools 83 | runtime 84 | true 85 | 86 | 87 | org.postgresql 88 | postgresql 89 | runtime 90 | 91 | 92 | org.projectlombok 93 | lombok 94 | true 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-starter-test 99 | test 100 | 101 | 102 | io.projectreactor 103 | reactor-test 104 | test 105 | 106 | 107 | org.springframework.kafka 108 | spring-kafka-test 109 | test 110 | 111 | 112 | 113 | 114 | 115 | org.springframework.cloud 116 | spring-cloud-dependencies 117 | ${spring-cloud.version} 118 | pom 119 | import 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-compiler-plugin 129 | 130 | 131 | 132 | org.projectlombok 133 | lombok 134 | ${lombok.version} 135 | 136 | 137 | org.mapstruct 138 | mapstruct-processor 139 | 1.5.5.Final 140 | 141 | 142 | org.projectlombok 143 | lombok-mapstruct-binding 144 | 0.2.0 145 | 146 | 147 | 148 | 149 | 150 | org.springframework.boot 151 | spring-boot-maven-plugin 152 | 153 | 154 | 155 | org.projectlombok 156 | lombok 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /scripts/start-kafka-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # OrderHub - Start Kafka Local Development Environment 4 | # This script starts the necessary Kafka infrastructure for local development 5 | 6 | set -e 7 | 8 | echo "🚀 Starting Kafka Local Development Environment for OrderHub" 9 | echo "=================================================" 10 | 11 | # Check if Docker is running 12 | if ! docker info > /dev/null 2>&1; then 13 | echo "❌ Error: Docker is not running. Please start Docker first." 14 | exit 1 15 | fi 16 | 17 | echo "✅ Docker is running" 18 | 19 | # Check if docker-compose.yml exists 20 | if [ ! -f "docker-compose.yml" ]; then 21 | echo "❌ Error: docker-compose.yml not found. Please run this script from the project root." 22 | exit 1 23 | fi 24 | 25 | echo "✅ Found docker-compose.yml" 26 | 27 | # Start Kafka infrastructure 28 | echo "🔄 Starting Kafka infrastructure..." 29 | docker-compose up -d zookeeper kafka kafka-ui 30 | 31 | # Wait for services to be ready 32 | echo "⏳ Waiting for services to start..." 33 | sleep 10 34 | 35 | # Check if services are running 36 | echo "🔍 Checking service status..." 37 | 38 | if docker ps | grep -q "orderhub-zookeeper"; then 39 | echo "✅ Zookeeper is running on port 2181" 40 | else 41 | echo "❌ Zookeeper failed to start" 42 | exit 1 43 | fi 44 | 45 | if docker ps | grep -q "orderhub-kafka"; then 46 | echo "✅ Kafka is running on port 9092" 47 | else 48 | echo "❌ Kafka failed to start" 49 | exit 1 50 | fi 51 | 52 | if docker ps | grep -q "orderhub-kafka-ui"; then 53 | echo "✅ Kafka UI is running on port 8090" 54 | else 55 | echo "❌ Kafka UI failed to start" 56 | exit 1 57 | fi 58 | 59 | echo "" 60 | echo "🎉 Kafka Local Development Environment is ready!" 61 | echo "=================================================" 62 | echo "📊 Kafka UI: http://localhost:8090" 63 | echo "🔌 Kafka Bootstrap Server: localhost:9092" 64 | echo "🐘 Zookeeper: localhost:2181" 65 | echo "" 66 | echo "To start the application with Kafka enabled, run:" 67 | echo "mvn spring-boot:run -Dspring-boot.run.profiles=local-kafka" 68 | echo "" 69 | echo "To stop the services, run:" 70 | echo "docker-compose down" 71 | echo "" 72 | echo "For more information, see: docs/KAFKA_LOCAL_SETUP.md" -------------------------------------------------------------------------------- /scripts/stop-kafka-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # OrderHub - Stop Kafka Local Development Environment 4 | # This script stops the Kafka infrastructure for local development 5 | 6 | set -e 7 | 8 | echo "🛑 Stopping Kafka Local Development Environment for OrderHub" 9 | echo "=================================================" 10 | 11 | # Check if docker-compose.yml exists 12 | if [ ! -f "docker-compose.yml" ]; then 13 | echo "❌ Error: docker-compose.yml not found. Please run this script from the project root." 14 | exit 1 15 | fi 16 | 17 | echo "✅ Found docker-compose.yml" 18 | 19 | # Stop Kafka infrastructure 20 | echo "🔄 Stopping Kafka infrastructure..." 21 | docker-compose down 22 | 23 | echo "" 24 | echo "✅ Kafka Local Development Environment stopped!" 25 | echo "=================================================" 26 | echo "All Kafka services have been stopped and containers removed." 27 | echo "" 28 | echo "To start again, run:" 29 | echo "./scripts/start-kafka-local.sh" 30 | echo "" 31 | echo "To remove all data (topics, messages, etc.), run:" 32 | echo "docker-compose down -v" -------------------------------------------------------------------------------- /scripts/test-kafka-integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # OrderHub - Test Kafka Integration 4 | # This script tests the Kafka integration by creating sample orders and monitoring events 5 | 6 | set -e 7 | 8 | echo "🧪 Testing OrderHub Kafka Integration" 9 | echo "====================================" 10 | 11 | # Check if application is running 12 | if ! curl -s http://localhost:8080/actuator/health > /dev/null; then 13 | echo "❌ Application is not running on port 8080" 14 | echo "Please start the application with: mvn spring-boot:run -Dspring-boot.run.profiles=local-kafka" 15 | exit 1 16 | fi 17 | 18 | echo "✅ Application is running" 19 | 20 | # Check if Kafka UI is accessible 21 | if ! curl -s http://localhost:8090 > /dev/null; then 22 | echo "⚠️ Kafka UI is not accessible on port 8090" 23 | echo "You can still test the integration, but won't be able to see events in the UI" 24 | else 25 | echo "✅ Kafka UI is accessible at http://localhost:8090" 26 | fi 27 | 28 | echo "" 29 | echo "🔄 Testing Order Creation and Event Publishing..." 30 | echo "" 31 | 32 | # Create a customer first 33 | echo "📝 Creating test customer..." 34 | CUSTOMER_RESPONSE=$(curl -s -X POST http://localhost:8080/api/customers \ 35 | -H "Content-Type: application/json" \ 36 | -d '{ 37 | "name": "Test Customer", 38 | "email": "test@example.com", 39 | "phone": "+1234567890" 40 | }') 41 | 42 | CUSTOMER_ID=$(echo $CUSTOMER_RESPONSE | grep -o '"id":[0-9]*' | cut -d':' -f2) 43 | 44 | if [ -z "$CUSTOMER_ID" ]; then 45 | echo "❌ Failed to create customer" 46 | echo "Response: $CUSTOMER_RESPONSE" 47 | exit 1 48 | fi 49 | 50 | echo "✅ Customer created with ID: $CUSTOMER_ID" 51 | 52 | # Create an order 53 | echo "📦 Creating test order..." 54 | ORDER_RESPONSE=$(curl -s -X POST http://localhost:8080/api/orders \ 55 | -H "Content-Type: application/json" \ 56 | -d "{ 57 | \"customerId\": $CUSTOMER_ID, 58 | \"items\": [ 59 | { 60 | \"productName\": \"Test Product\", 61 | \"productSku\": \"TEST-001\", 62 | \"quantity\": 2, 63 | \"unitPrice\": 29.99 64 | } 65 | ], 66 | \"paymentMethod\": \"PIX\" 67 | }") 68 | 69 | ORDER_ID=$(echo $ORDER_RESPONSE | grep -o '"id":[0-9]*' | cut -d':' -f2) 70 | 71 | if [ -z "$ORDER_ID" ]; then 72 | echo "❌ Failed to create order" 73 | echo "Response: $ORDER_RESPONSE" 74 | exit 1 75 | fi 76 | 77 | echo "✅ Order created with ID: $ORDER_ID" 78 | 79 | # Wait a moment for events to be processed 80 | echo "⏳ Waiting for events to be processed..." 81 | sleep 3 82 | 83 | # Check order status 84 | echo "🔍 Checking order status..." 85 | ORDER_STATUS_RESPONSE=$(curl -s http://localhost:8080/api/orders/$ORDER_ID) 86 | echo "Order Status Response: $ORDER_STATUS_RESPONSE" 87 | 88 | echo "" 89 | echo "🎉 Test completed successfully!" 90 | echo "====================================" 91 | echo "📊 Check the following to verify Kafka integration:" 92 | echo "" 93 | echo "1. Application logs should show:" 94 | echo " - 'Evento OrderCreated publicado com sucesso para pedido $ORDER_ID'" 95 | echo " - Kafka connection and topic creation messages" 96 | echo "" 97 | echo "2. Kafka UI (http://localhost:8090):" 98 | echo " - Check 'orders.created' topic for the published event" 99 | echo " - Verify consumer groups are active" 100 | echo "" 101 | echo "3. Order details:" 102 | echo " - Customer ID: $CUSTOMER_ID" 103 | echo " - Order ID: $ORDER_ID" 104 | echo " - Order Status: Check the response above" 105 | echo "" 106 | echo "To monitor events in real-time, you can also use:" 107 | echo "curl -N http://localhost:8080/public/orders/$ORDER_ID/status/stream" 108 | echo "" 109 | echo "For more detailed testing, see: docs/KAFKA_LOCAL_SETUP.md" -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/OrderhubApplication.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.openfeign.EnableFeignClients; 6 | import org.springframework.kafka.annotation.EnableKafka; 7 | 8 | @SpringBootApplication 9 | @EnableFeignClients 10 | @EnableKafka 11 | public class OrderhubApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(OrderhubApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/client/AbacatePayClient.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.client; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | 9 | import com.kipperdev.orderhub.config.AbacatePayFeignConfig; 10 | import com.kipperdev.orderhub.dto.abacate.AbacateChargeRequestDTO; 11 | import com.kipperdev.orderhub.dto.abacate.AbacateChargeResponseDTO; 12 | import com.kipperdev.orderhub.dto.abacate.AbacateCustomerDTO; 13 | import com.kipperdev.orderhub.dto.abacate.AbacateCustomerResponseDTO; 14 | 15 | @FeignClient( 16 | name = "abacate-pay", 17 | url = "${abacate.api.base-url:https://api.abacatepay.com}", 18 | configuration = AbacatePayFeignConfig.class 19 | ) 20 | public interface AbacatePayClient { 21 | 22 | @PostMapping("/v1/customer/create") 23 | AbacateCustomerResponseDTO createCustomer(@RequestBody AbacateCustomerDTO customer); 24 | 25 | @GetMapping("/v1/customers/{id}") 26 | AbacateCustomerResponseDTO getCustomer(@PathVariable("id") String customerId); 27 | 28 | @PostMapping("/v1/billing/create") 29 | AbacateChargeResponseDTO createBilling(@RequestBody AbacateChargeRequestDTO billingRequest); 30 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/config/AbacatePayFeignConfig.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.config; 2 | 3 | import feign.RequestInterceptor; 4 | import feign.RequestTemplate; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.util.StringUtils; 10 | 11 | @Configuration 12 | @Slf4j 13 | public class AbacatePayFeignConfig { 14 | 15 | @Value("${abacate.api.token:}") 16 | private String abacateApiToken; 17 | 18 | @Bean 19 | public RequestInterceptor abacatePayRequestInterceptor() { 20 | return new AbacatePayRequestInterceptor(abacateApiToken); 21 | } 22 | 23 | public static class AbacatePayRequestInterceptor implements RequestInterceptor { 24 | private final String apiToken; 25 | 26 | public AbacatePayRequestInterceptor(String apiToken) { 27 | this.apiToken = apiToken; 28 | } 29 | 30 | @Override 31 | public void apply(RequestTemplate template) { 32 | if (StringUtils.hasText(apiToken) && !"mock-token".equals(apiToken)) { 33 | template.header("Authorization", "Bearer " + apiToken); 34 | log.debug("Adicionado Bearer token para requisição Abacatepay"); 35 | } else { 36 | log.warn("Token da API Abacatepay não configurado ou usando token mock"); 37 | } 38 | 39 | template.header("Content-Type", "application/json"); 40 | template.header("Accept", "application/json"); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/config/FeignConfig.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.config; 2 | 3 | import feign.Logger; 4 | import feign.Request; 5 | import feign.Retryer; 6 | import feign.codec.ErrorDecoder; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Configuration 15 | @Slf4j 16 | public class FeignConfig { 17 | 18 | @Value("${feign.client.connect-timeout:5000}") 19 | private int connectTimeout; 20 | 21 | @Value("${feign.client.read-timeout:10000}") 22 | private int readTimeout; 23 | 24 | @Bean 25 | public Request.Options requestOptions() { 26 | return new Request.Options( 27 | connectTimeout, TimeUnit.MILLISECONDS, 28 | readTimeout, TimeUnit.MILLISECONDS, 29 | true 30 | ); 31 | } 32 | 33 | @Bean 34 | public Retryer retryer() { 35 | return new Retryer.Default( 36 | 100L, // period 37 | 1000L, // maxPeriod 38 | 3 // maxAttempts 39 | ); 40 | } 41 | 42 | @Bean 43 | public Logger.Level feignLoggerLevel() { 44 | return Logger.Level.BASIC; 45 | } 46 | 47 | @Bean 48 | public ErrorDecoder errorDecoder() { 49 | return new CustomErrorDecoder(); 50 | } 51 | 52 | public static class CustomErrorDecoder implements ErrorDecoder { 53 | private final ErrorDecoder defaultErrorDecoder = new Default(); 54 | 55 | @Override 56 | public Exception decode(String methodKey, feign.Response response) { 57 | log.error("Erro na chamada Feign - método: {}, status: {}, motivo: {}", 58 | methodKey, response.status(), response.reason()); 59 | 60 | switch (response.status()) { 61 | case 400: 62 | return new IllegalArgumentException("Requisição inválida: " + response.reason()); 63 | case 401: 64 | return new SecurityException("Não autorizado: " + response.reason()); 65 | case 404: 66 | return new RuntimeException("Recurso não encontrado: " + response.reason()); 67 | case 500: 68 | return new RuntimeException("Erro interno do servidor: " + response.reason()); 69 | case 503: 70 | return new RuntimeException("Serviço indisponível: " + response.reason()); 71 | default: 72 | return defaultErrorDecoder.decode(methodKey, response); 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/config/KafkaConfig.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.config; 2 | 3 | import org.apache.kafka.clients.admin.NewTopic; 4 | import org.apache.kafka.clients.consumer.ConsumerConfig; 5 | import org.apache.kafka.clients.producer.ProducerConfig; 6 | import org.apache.kafka.common.serialization.StringDeserializer; 7 | import org.apache.kafka.common.serialization.StringSerializer; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.kafka.annotation.EnableKafka; 13 | import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; 14 | import org.springframework.kafka.config.TopicBuilder; 15 | import org.springframework.kafka.core.*; 16 | import org.springframework.kafka.listener.ContainerProperties; 17 | import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer; 18 | import org.springframework.kafka.support.serializer.JsonDeserializer; 19 | import org.springframework.kafka.support.serializer.JsonSerializer; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | @Configuration 25 | @EnableKafka 26 | @ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = false) 27 | public class KafkaConfig { 28 | 29 | @Value("${spring.kafka.bootstrap-servers:localhost:9092}") 30 | private String bootstrapServers; 31 | 32 | @Value("${spring.kafka.consumer.group-id:orderhub-group}") 33 | private String groupId; 34 | 35 | // Producer Configuration 36 | @Bean 37 | public ProducerFactory producerFactory() { 38 | Map configProps = new HashMap<>(); 39 | configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 40 | configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 41 | configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); 42 | configProps.put(ProducerConfig.ACKS_CONFIG, "all"); 43 | configProps.put(ProducerConfig.RETRIES_CONFIG, 3); 44 | configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); 45 | configProps.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1); 46 | 47 | return new DefaultKafkaProducerFactory<>(configProps); 48 | } 49 | 50 | @Bean 51 | public KafkaTemplate kafkaTemplate() { 52 | return new KafkaTemplate<>(producerFactory()); 53 | } 54 | 55 | @Bean 56 | public ProducerFactory stringProducerFactory() { 57 | Map configProps = new HashMap<>(); 58 | configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 59 | configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 60 | configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 61 | configProps.put(ProducerConfig.ACKS_CONFIG, "all"); 62 | configProps.put(ProducerConfig.RETRIES_CONFIG, 3); 63 | configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); 64 | configProps.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1); 65 | 66 | return new DefaultKafkaProducerFactory<>(configProps); 67 | } 68 | 69 | @Bean 70 | public KafkaTemplate stringKafkaTemplate() { 71 | return new KafkaTemplate<>(stringProducerFactory()); 72 | } 73 | 74 | // Consumer Configuration 75 | @Bean 76 | public ConsumerFactory consumerFactory() { 77 | Map configProps = new HashMap<>(); 78 | configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 79 | configProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); 80 | configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 81 | configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); 82 | configProps.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class); 83 | configProps.put(JsonDeserializer.TRUSTED_PACKAGES, "com.kipperdev.orderhub.event"); 84 | configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); 85 | configProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); 86 | 87 | return new DefaultKafkaConsumerFactory<>(configProps); 88 | } 89 | 90 | @Bean 91 | public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { 92 | ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); 93 | factory.setConsumerFactory(consumerFactory()); 94 | factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE); 95 | factory.setCommonErrorHandler(new org.springframework.kafka.listener.DefaultErrorHandler()); 96 | 97 | return factory; 98 | } 99 | 100 | // Topic Definitions 101 | @Bean 102 | public NewTopic ordersCreatedTopic() { 103 | return TopicBuilder.name("orders.created") 104 | .partitions(3) 105 | .replicas(1) 106 | .build(); 107 | } 108 | 109 | @Bean 110 | public NewTopic paymentsConfirmedTopic() { 111 | return TopicBuilder.name("payments.confirmed") 112 | .partitions(3) 113 | .replicas(1) 114 | .build(); 115 | } 116 | 117 | @Bean 118 | public NewTopic stockReservedTopic() { 119 | return TopicBuilder.name("stock.reserved") 120 | .partitions(3) 121 | .replicas(1) 122 | .build(); 123 | } 124 | 125 | @Bean 126 | public NewTopic invoiceGeneratedTopic() { 127 | return TopicBuilder.name("invoice.generated") 128 | .partitions(3) 129 | .replicas(1) 130 | .build(); 131 | } 132 | 133 | // Dead Letter Topics 134 | @Bean 135 | public NewTopic ordersCreatedDltTopic() { 136 | return TopicBuilder.name("orders.created.dlt") 137 | .partitions(1) 138 | .replicas(1) 139 | .build(); 140 | } 141 | 142 | @Bean 143 | public NewTopic paymentsConfirmedDltTopic() { 144 | return TopicBuilder.name("payments.confirmed.dlt") 145 | .partitions(1) 146 | .replicas(1) 147 | .build(); 148 | } 149 | 150 | @Bean 151 | public NewTopic stockReservedDltTopic() { 152 | return TopicBuilder.name("stock.reserved.dlt") 153 | .partitions(1) 154 | .replicas(1) 155 | .build(); 156 | } 157 | 158 | @Bean 159 | public NewTopic invoiceGeneratedDltTopic() { 160 | return TopicBuilder.name("invoice.generated.dlt") 161 | .partitions(1) 162 | .replicas(1) 163 | .build(); 164 | } 165 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/config/MapStructConfig.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.config; 2 | 3 | import org.mapstruct.MapperConfig; 4 | import org.mapstruct.ReportingPolicy; 5 | 6 | @MapperConfig( 7 | componentModel = "spring", 8 | unmappedTargetPolicy = ReportingPolicy.IGNORE, 9 | unmappedSourcePolicy = ReportingPolicy.IGNORE 10 | ) 11 | public interface MapStructConfig { 12 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 9 | import org.springframework.security.config.http.SessionCreationPolicy; 10 | import org.springframework.security.core.userdetails.User; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.core.userdetails.UserDetailsService; 13 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 14 | import org.springframework.security.crypto.password.PasswordEncoder; 15 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 16 | import org.springframework.security.web.SecurityFilterChain; 17 | 18 | @Configuration 19 | @EnableWebSecurity 20 | public class SecurityConfig { 21 | 22 | @Value("${admin.username:admin}") 23 | private String adminUsername; 24 | 25 | @Value("${admin.password:admin123}") 26 | private String adminPassword; 27 | 28 | @Bean 29 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 30 | http 31 | .csrf(AbstractHttpConfigurer::disable) 32 | .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 33 | .authorizeHttpRequests(authz -> authz 34 | // Endpoints públicos 35 | .requestMatchers("/public/**").permitAll() 36 | .requestMatchers("/webhook/**").permitAll() 37 | .requestMatchers("/orders").permitAll() 38 | .requestMatchers("/orders/{id}").permitAll() 39 | .requestMatchers("/health").permitAll() 40 | .requestMatchers("/actuator/**").permitAll() 41 | 42 | // Endpoints administrativos protegidos 43 | .requestMatchers("/admin/**").authenticated() 44 | 45 | // Qualquer outra requisição 46 | .anyRequest().permitAll() 47 | ) 48 | .httpBasic(httpBasic -> {}); 49 | 50 | return http.build(); 51 | } 52 | 53 | @Bean 54 | public UserDetailsService userDetailsService() { 55 | UserDetails admin = User.builder() 56 | .username(adminUsername) 57 | .password(passwordEncoder().encode(adminPassword)) 58 | .roles("ADMIN") 59 | .build(); 60 | 61 | return new InMemoryUserDetailsManager(admin); 62 | } 63 | 64 | @Bean 65 | public PasswordEncoder passwordEncoder() { 66 | return new BCryptPasswordEncoder(); 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/controller/AbacatePayWebhookController.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.controller; 2 | 3 | import com.kipperdev.orderhub.dto.abacate.AbacateWebhookDTO; 4 | import com.kipperdev.orderhub.service.AbacatePayService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import javax.crypto.Mac; 12 | import javax.crypto.spec.SecretKeySpec; 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.InvalidKeyException; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.util.HexFormat; 17 | 18 | @RestController 19 | @RequestMapping("/api/webhooks/abacatepay") 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | public class AbacatePayWebhookController { 23 | 24 | private final AbacatePayService abacatePayService; 25 | 26 | @Value("${abacate.webhook.secret:}") 27 | private String webhookSecret; 28 | 29 | @Value("${abacate.webhook.signature.enabled:false}") 30 | private boolean signatureValidationEnabled; 31 | 32 | @PostMapping 33 | public ResponseEntity handleWebhook( 34 | @RequestBody AbacateWebhookDTO webhook, 35 | @RequestHeader(value = "X-Abacate-Signature", required = false) String signature, 36 | @RequestBody String rawBody) { 37 | 38 | try { 39 | log.info("Webhook recebido do Abacatepay: event={}, devMode={}", 40 | webhook.getEvent(), webhook.getDevMode()); 41 | 42 | // Validar assinatura se habilitado 43 | if (signatureValidationEnabled && !validateSignature(rawBody, signature)) { 44 | log.warn("Assinatura do webhook inválida"); 45 | return ResponseEntity.status(401).body("Invalid signature"); 46 | } 47 | 48 | // Processar webhook baseado no evento 49 | if ("billing.paid".equals(webhook.getEvent())) { 50 | String billingId = webhook.getData().getBilling().getId(); 51 | String status = webhook.getData().getBilling().getStatus(); 52 | 53 | abacatePayService.processWebhook(billingId, webhook.getEvent(), status); 54 | 55 | log.info("Webhook de pagamento processado com sucesso: billing={}", billingId); 56 | } else { 57 | log.info("Evento de webhook não processado: {}", webhook.getEvent()); 58 | } 59 | 60 | return ResponseEntity.ok("Webhook processed successfully"); 61 | 62 | } catch (Exception e) { 63 | log.error("Erro ao processar webhook do Abacatepay: {}", e.getMessage(), e); 64 | return ResponseEntity.status(500).body("Internal server error"); 65 | } 66 | } 67 | 68 | private boolean validateSignature(String payload, String signature) { 69 | if (webhookSecret == null || webhookSecret.isEmpty() || signature == null) { 70 | return false; 71 | } 72 | 73 | try { 74 | Mac mac = Mac.getInstance("HmacSHA256"); 75 | SecretKeySpec secretKeySpec = new SecretKeySpec( 76 | webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 77 | mac.init(secretKeySpec); 78 | 79 | byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); 80 | String expectedSignature = HexFormat.of().formatHex(hash); 81 | 82 | return signature.equals(expectedSignature); 83 | 84 | } catch (NoSuchAlgorithmException | InvalidKeyException e) { 85 | log.error("Erro ao validar assinatura do webhook: {}", e.getMessage()); 86 | return false; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/controller/AdminOrderController.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.controller; 2 | 3 | import com.kipperdev.orderhub.dto.OrderResponseDTO; 4 | import com.kipperdev.orderhub.entity.OrderStatus; 5 | import com.kipperdev.orderhub.service.OrderService; 6 | import com.kipperdev.orderhub.specification.OrderSpecification; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.web.PageableDefault; 12 | import org.springframework.format.annotation.DateTimeFormat; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.time.LocalDateTime; 17 | import java.util.Map; 18 | 19 | @RestController 20 | @RequestMapping("/admin/orders") 21 | @RequiredArgsConstructor 22 | @Slf4j 23 | public class AdminOrderController { 24 | 25 | private final OrderService orderService; 26 | 27 | @GetMapping 28 | public ResponseEntity> getAllOrders( 29 | @RequestParam(required = false) OrderStatus status, 30 | @RequestParam(required = false) String customerEmail, 31 | @RequestParam(required = false) String customerName, 32 | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, 33 | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate, 34 | @PageableDefault(size = 20) Pageable pageable) { 35 | 36 | log.info("Consulta administrativa de pedidos - status: {}, email: {}, nome: {}, período: {} a {}", 37 | status, customerEmail, customerName, startDate, endDate); 38 | 39 | var specification = OrderSpecification.withFilters(status, customerEmail, startDate, endDate); 40 | 41 | if (customerName != null && !customerName.trim().isEmpty()) { 42 | specification = specification.and(OrderSpecification.byCustomerName(customerName)); 43 | } 44 | 45 | Page orders = orderService.filterOrders(specification, pageable); 46 | 47 | log.info("Retornando {} pedidos de {} total", orders.getNumberOfElements(), orders.getTotalElements()); 48 | 49 | return ResponseEntity.ok(orders); 50 | } 51 | 52 | @GetMapping("/{id}") 53 | public ResponseEntity getOrderById(@PathVariable Long id) { 54 | log.info("Consulta administrativa do pedido: {}", id); 55 | 56 | return orderService.getOrderById(id) 57 | .map(ResponseEntity::ok) 58 | .orElse(ResponseEntity.notFound().build()); 59 | } 60 | 61 | @PutMapping("/{id}/status") 62 | public ResponseEntity updateOrderStatus( 63 | @PathVariable Long id, 64 | @RequestParam OrderStatus status) { 65 | 66 | log.info("Atualizando status do pedido {} para: {}", id, status); 67 | 68 | try { 69 | OrderResponseDTO updatedOrder = orderService.updateOrderStatus(id, status); 70 | log.info("Status do pedido {} atualizado com sucesso para: {}", id, status); 71 | return ResponseEntity.ok(updatedOrder); 72 | } catch (RuntimeException e) { 73 | log.error("Erro ao atualizar status do pedido {}: {}", id, e.getMessage()); 74 | return ResponseEntity.badRequest().build(); 75 | } 76 | } 77 | 78 | @GetMapping("/stats") 79 | public ResponseEntity> getOrderStats( 80 | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, 81 | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { 82 | 83 | log.info("Consultando estatísticas de pedidos - período: {} a {}", startDate, endDate); 84 | 85 | Map stats = orderService.getOrderStatistics(startDate, endDate); 86 | 87 | return ResponseEntity.ok(stats); 88 | } 89 | 90 | @GetMapping("/export") 91 | public ResponseEntity exportOrders( 92 | @RequestParam(required = false) OrderStatus status, 93 | @RequestParam(required = false) String customerEmail, 94 | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, 95 | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate, 96 | @RequestParam(defaultValue = "csv") String format) { 97 | 98 | log.info("Exportando pedidos - formato: {}, status: {}, email: {}, período: {} a {}", 99 | format, status, customerEmail, startDate, endDate); 100 | 101 | try { 102 | String exportData = orderService.exportOrders(status, customerEmail, startDate, endDate, format); 103 | 104 | return ResponseEntity.ok() 105 | .header("Content-Type", format.equals("csv") ? "text/csv" : "application/json") 106 | .header("Content-Disposition", "attachment; filename=orders." + format) 107 | .body(exportData); 108 | 109 | } catch (Exception e) { 110 | log.error("Erro ao exportar pedidos: {}", e.getMessage()); 111 | return ResponseEntity.internalServerError().body("Erro ao exportar dados"); 112 | } 113 | } 114 | 115 | @PostMapping("/{id}/cancel") 116 | public ResponseEntity cancelOrder(@PathVariable Long id) { 117 | log.info("Cancelando pedido: {}", id); 118 | 119 | try { 120 | OrderResponseDTO cancelledOrder = orderService.updateOrderStatus(id, OrderStatus.CANCELLED); 121 | log.info("Pedido {} cancelado com sucesso", id); 122 | return ResponseEntity.ok(cancelledOrder); 123 | } catch (RuntimeException e) { 124 | log.error("Erro ao cancelar pedido {}: {}", id, e.getMessage()); 125 | return ResponseEntity.badRequest().build(); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.controller; 2 | 3 | import com.kipperdev.orderhub.dto.CreateOrderRequestDTO; 4 | import com.kipperdev.orderhub.dto.OrderResponseDTO; 5 | import com.kipperdev.orderhub.service.OrderService; 6 | import jakarta.validation.Valid; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | @RestController 14 | @RequestMapping("/orders") 15 | @RequiredArgsConstructor 16 | @Slf4j 17 | public class OrderController { 18 | 19 | private final OrderService orderService; 20 | 21 | @PostMapping 22 | public ResponseEntity createOrder(@Valid @RequestBody CreateOrderRequestDTO request) { 23 | try { 24 | log.info("Recebida solicitação de criação de pedido para cliente: {}", request.getCustomer().getEmail()); 25 | 26 | OrderResponseDTO response = orderService.createOrder(request); 27 | 28 | log.info("Pedido criado com sucesso: ID={}, Status={}", response.getId(), response.getStatus()); 29 | 30 | return ResponseEntity.status(HttpStatus.CREATED).body(response); 31 | 32 | } catch (Exception e) { 33 | log.error("Erro ao criar pedido: {}", e.getMessage(), e); 34 | throw new RuntimeException("Falha na criação do pedido: " + e.getMessage()); 35 | } 36 | } 37 | 38 | @GetMapping("/{id}") 39 | public ResponseEntity getOrder(@PathVariable Long id) { 40 | try { 41 | return orderService.getOrderById(id) 42 | .map(order -> ResponseEntity.ok(order)) 43 | .orElse(ResponseEntity.notFound().build()); 44 | } catch (Exception e) { 45 | log.error("Erro ao buscar pedido {}: {}", id, e.getMessage()); 46 | return ResponseEntity.internalServerError().build(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/controller/PublicOrderController.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.controller; 2 | 3 | import com.kipperdev.orderhub.dto.OrderStatusDTO; 4 | import com.kipperdev.orderhub.service.OrderService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @RestController 14 | @RequestMapping("/public/orders") 15 | @RequiredArgsConstructor 16 | @Slf4j 17 | public class PublicOrderController { 18 | 19 | private final OrderService orderService; 20 | 21 | @GetMapping("/{id}/status") 22 | public ResponseEntity getOrderStatus(@PathVariable Long id) { 23 | log.info("Consulta pública de status do pedido: {}", id); 24 | 25 | try { 26 | Optional status = orderService.getOrderStatus(id); 27 | 28 | if (status.isPresent()) { 29 | log.info("Status do pedido {} consultado: {}", id, status.get().getStatus()); 30 | return ResponseEntity.ok(status.get()); 31 | } else { 32 | log.warn("Pedido {} não encontrado", id); 33 | return ResponseEntity.notFound().build(); 34 | } 35 | } catch (Exception error) { 36 | log.error("Erro ao consultar status do pedido {}: {}", id, error.getMessage()); 37 | return ResponseEntity.internalServerError().build(); 38 | } 39 | } 40 | 41 | @GetMapping("/customer/{email}/status") 42 | public ResponseEntity> getOrdersByCustomerEmail(@PathVariable String email) { 43 | log.info("Consulta pública de pedidos por email: {}", email); 44 | 45 | try { 46 | List orders = orderService.getOrdersByCustomerEmail(email); 47 | log.info("Consulta de pedidos por email {} concluída. {} pedidos encontrados", email, orders.size()); 48 | return ResponseEntity.ok(orders); 49 | } catch (Exception error) { 50 | log.error("Erro ao consultar pedidos por email {}: {}", email, error.getMessage()); 51 | return ResponseEntity.internalServerError().build(); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/controller/WebhookController.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.controller; 2 | 3 | import com.kipperdev.orderhub.dto.abacate.AbacateWebhookDTO; 4 | import com.kipperdev.orderhub.service.OrderService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import javax.crypto.Mac; 12 | import javax.crypto.spec.SecretKeySpec; 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.InvalidKeyException; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.util.HexFormat; 17 | 18 | @RestController 19 | @RequestMapping("/webhook") 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | public class WebhookController { 23 | 24 | private final OrderService orderService; 25 | 26 | @Value("${abacate.webhook.secret:default-secret}") 27 | private String webhookSecret; 28 | 29 | @Value("${abacate.webhook.signature.enabled:false}") 30 | private boolean signatureValidationEnabled; 31 | 32 | @PostMapping("/abacate") 33 | public ResponseEntity handleAbacateWebhook( 34 | @RequestBody AbacateWebhookDTO webhook, 35 | @RequestHeader(value = "X-Abacate-Signature", required = false) String signature) { 36 | 37 | try { 38 | String billingId = webhook.getData() != null && webhook.getData().getBilling() != null ? 39 | webhook.getData().getBilling().getId() : "unknown"; 40 | String status = webhook.getData() != null && webhook.getData().getBilling() != null ? 41 | webhook.getData().getBilling().getStatus() : "unknown"; 42 | 43 | log.info("Recebido webhook do Abacate Pay: event={}, billingId={}, status={}", 44 | webhook.getEvent(), billingId, status); 45 | 46 | if (signatureValidationEnabled && !validateSignature(webhook, signature)) { 47 | log.warn("Assinatura inválida no webhook do Abacate Pay"); 48 | return ResponseEntity.badRequest().body("Invalid signature"); 49 | } 50 | 51 | if (webhook.getData() != null && webhook.getData().getBilling() != null) { 52 | billingId = webhook.getData().getBilling().getId(); 53 | 54 | switch (webhook.getEvent().toLowerCase()) { 55 | case "billing.paid": 56 | orderService.updateOrderFromAbacateWebhook(billingId, "PAID"); 57 | break; 58 | 59 | case "billing.failed": 60 | case "billing.cancelled": 61 | orderService.updateOrderFromAbacateWebhook(billingId, "FAILED"); 62 | break; 63 | 64 | case "billing.pending": 65 | orderService.updateOrderFromAbacateWebhook(billingId, "PENDING"); 66 | break; 67 | 68 | default: 69 | log.info("Evento de webhook não processado: {}", webhook.getEvent()); 70 | break; 71 | } 72 | } else { 73 | log.warn("Webhook recebido sem dados de billing válidos"); 74 | } 75 | 76 | log.info("Webhook do Abacate Pay processado com sucesso: billingId={}", billingId); 77 | return ResponseEntity.ok("Webhook processed successfully"); 78 | 79 | } catch (Exception e) { 80 | log.error("Erro ao processar webhook do Abacate Pay: {}", e.getMessage(), e); 81 | return ResponseEntity.internalServerError().body("Error processing webhook"); 82 | } 83 | } 84 | 85 | private boolean validateSignature(AbacateWebhookDTO webhook, String receivedSignature) { 86 | if (receivedSignature == null || receivedSignature.isEmpty()) { 87 | return false; 88 | } 89 | 90 | try { 91 | String billingId = webhook.getData() != null && webhook.getData().getBilling() != null ? 92 | webhook.getData().getBilling().getId() : ""; 93 | String status = webhook.getData() != null && webhook.getData().getBilling() != null ? 94 | webhook.getData().getBilling().getStatus() : ""; 95 | String payload = webhook.getEvent() + billingId + status; 96 | 97 | Mac mac = Mac.getInstance("HmacSHA256"); 98 | SecretKeySpec secretKeySpec = new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 99 | mac.init(secretKeySpec); 100 | 101 | byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); 102 | String calculatedSignature = HexFormat.of().formatHex(hash); 103 | 104 | return calculatedSignature.equals(receivedSignature.toLowerCase()); 105 | 106 | } catch (NoSuchAlgorithmException | InvalidKeyException e) { 107 | log.error("Erro ao validar assinatura do webhook: {}", e.getMessage()); 108 | return false; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/CreateOrderRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto; 2 | 3 | import jakarta.validation.Valid; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotEmpty; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | import java.util.List; 12 | 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class CreateOrderRequestDTO { 17 | 18 | @Valid 19 | @NotNull(message = "Dados do cliente são obrigatórios") 20 | private CustomerDTO customer; 21 | 22 | @Valid 23 | @NotEmpty(message = "Lista de itens não pode estar vazia") 24 | private List items; 25 | 26 | @NotBlank(message = "Método de pagamento é obrigatório") 27 | private String paymentMethod; 28 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/CustomerDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CustomerDTO { 13 | 14 | private Long id; 15 | 16 | @NotBlank(message = "Nome é obrigatório") 17 | private String name; 18 | 19 | @NotBlank(message = "Documento é obrigatório") 20 | private String document; 21 | 22 | @Email(message = "Email deve ter formato válido") 23 | @NotBlank(message = "Email é obrigatório") 24 | private String email; 25 | 26 | @NotBlank(message = "Telefone é obrigatório") 27 | private String phone; 28 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/OrderItemDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto; 2 | 3 | import jakarta.validation.constraints.DecimalMin; 4 | import jakarta.validation.constraints.Min; 5 | import jakarta.validation.constraints.NotBlank; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.math.BigDecimal; 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class OrderItemDTO { 16 | 17 | private Long id; 18 | 19 | @NotBlank(message = "Nome do produto é obrigatório") 20 | private String productName; 21 | 22 | @NotBlank(message = "SKU do produto é obrigatório") 23 | private String productSku; 24 | 25 | @Min(value = 1, message = "Quantidade deve ser maior que zero") 26 | private Integer quantity; 27 | 28 | @DecimalMin(value = "0.01", message = "Preço unitário deve ser maior que zero") 29 | private BigDecimal unitPrice; 30 | 31 | private BigDecimal totalPrice; 32 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/OrderResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto; 2 | 3 | import com.kipperdev.orderhub.entity.OrderStatus; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.math.BigDecimal; 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class OrderResponseDTO { 16 | 17 | private Long id; 18 | private CustomerDTO customer; 19 | private OrderStatus status; 20 | private BigDecimal totalAmount; 21 | private String paymentMethod; 22 | private String paymentLink; 23 | private LocalDateTime createdAt; 24 | private LocalDateTime updatedAt; 25 | private LocalDateTime paidAt; 26 | private List items; 27 | private String statusUrl; 28 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/OrderStatusDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto; 2 | 3 | import com.kipperdev.orderhub.entity.OrderStatus; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.math.BigDecimal; 9 | import java.time.LocalDateTime; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class OrderStatusDTO { 15 | 16 | private Long id; 17 | private OrderStatus status; 18 | private String statusDescription; 19 | private BigDecimal totalAmount; 20 | private LocalDateTime createdAt; 21 | private LocalDateTime updatedAt; 22 | private LocalDateTime paidAt; 23 | private String customerEmail; 24 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/abacate/AbacateChargeRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto.abacate; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class AbacateChargeRequestDTO { 14 | 15 | private String frequency = "ONE_TIME"; 16 | private List methods; 17 | private List products; 18 | 19 | @JsonProperty("returnUrl") 20 | private String returnUrl; 21 | 22 | @JsonProperty("completionUrl") 23 | private String completionUrl; 24 | 25 | private AbacateCustomerDTO customer; 26 | 27 | @JsonProperty("customerId") 28 | private String abacateCustomerId; 29 | 30 | @Data 31 | @NoArgsConstructor 32 | @AllArgsConstructor 33 | public static class AbacateProductDTO { 34 | @JsonProperty("externalId") 35 | private String externalId; 36 | private String name; 37 | private Integer quantity; 38 | private Integer price; 39 | private String description; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/abacate/AbacateChargeResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto.abacate; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class AbacateChargeResponseDTO { 15 | 16 | private String error; 17 | private AbacateChargeDataDTO data; 18 | 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public static class AbacateChargeDataDTO { 23 | private String id; 24 | private Integer amount; 25 | private String status; 26 | private String frequency; 27 | @JsonProperty("devMode") 28 | private Boolean devMode; 29 | private List methods; 30 | @JsonProperty("allowCoupons") 31 | private Boolean allowCoupons; 32 | private List coupons; 33 | @JsonProperty("couponsUsed") 34 | private List couponsUsed; 35 | private String url; 36 | @JsonProperty("createdAt") 37 | private LocalDateTime createdAt; 38 | @JsonProperty("updatedAt") 39 | private LocalDateTime updatedAt; 40 | private List products; 41 | private AbacateChargeMetadataDTO metadata; 42 | private AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO customer; 43 | 44 | @Data 45 | @NoArgsConstructor 46 | @AllArgsConstructor 47 | public static class AbacateProductResponseDTO { 48 | private String id; 49 | private String externalId; 50 | private Integer quantity; 51 | } 52 | 53 | @Data 54 | @NoArgsConstructor 55 | @AllArgsConstructor 56 | public static class AbacateChargeMetadataDTO { 57 | private Integer fee; 58 | private String returnUrl; 59 | private String completionUrl; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/abacate/AbacateCustomerDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto.abacate; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class AbacateCustomerDTO { 12 | private String name; 13 | private String email; 14 | 15 | @JsonProperty("cellphone") 16 | private String cellphone; 17 | 18 | @JsonProperty("taxId") 19 | private String taxId; 20 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/abacate/AbacateCustomerResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto.abacate; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class AbacateCustomerResponseDTO { 12 | 13 | private AbacateCustomerMetadataDTO data; 14 | private String error; 15 | 16 | @Data 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public static class AbacateCustomerMetadataDTO { 20 | private String id; 21 | private AbacateCustomerDTO metadata; 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/dto/abacate/AbacateWebhookDTO.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.dto.abacate; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class AbacateWebhookDTO { 12 | 13 | private String event; 14 | private AbacateWebhookDataDTO data; 15 | 16 | @JsonProperty("devMode") 17 | private Boolean devMode; 18 | 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public static class AbacateWebhookDataDTO { 23 | private AbacatePaymentDTO payment; 24 | private AbacateBillingDTO billing; 25 | 26 | @Data 27 | @NoArgsConstructor 28 | @AllArgsConstructor 29 | public static class AbacatePaymentDTO { 30 | private Integer amount; 31 | private Integer fee; 32 | private String method; 33 | } 34 | 35 | @Data 36 | @NoArgsConstructor 37 | @AllArgsConstructor 38 | public static class AbacateBillingDTO { 39 | private String id; 40 | private Integer amount; 41 | private String status; 42 | private String frequency; 43 | private AbacateCustomerDTO customer; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/entity/Customer.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.entity; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | 13 | @Entity 14 | @Table(name = "customers") 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class Customer { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @NotBlank(message = "Nome é obrigatório") 25 | @Column(nullable = false) 26 | private String name; 27 | 28 | @Email(message = "Email deve ter formato válido") 29 | @NotBlank(message = "Email é obrigatório") 30 | @Column(nullable = false, unique = true) 31 | private String email; 32 | 33 | @NotBlank(message = "Documento é obrigatório") 34 | @Column(nullable = false, unique = true) 35 | private String document; 36 | 37 | @NotBlank(message = "Telefone é obrigatório") 38 | @Column(nullable = false) 39 | private String phone; 40 | 41 | @Column(name = "created_at", nullable = false) 42 | private LocalDateTime createdAt; 43 | 44 | @Column(name = "updated_at") 45 | private LocalDateTime updatedAt; 46 | 47 | @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY) 48 | private List orders; 49 | 50 | @PrePersist 51 | protected void onCreate() { 52 | createdAt = LocalDateTime.now(); 53 | updatedAt = LocalDateTime.now(); 54 | } 55 | 56 | @PreUpdate 57 | protected void onUpdate() { 58 | updatedAt = LocalDateTime.now(); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/entity/Order.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.entity; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.DecimalMin; 5 | import jakarta.validation.constraints.NotNull; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.math.BigDecimal; 11 | import java.time.LocalDateTime; 12 | import java.util.List; 13 | 14 | @Entity 15 | @Table(name = "orders") 16 | @Data 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class Order { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Long id; 24 | 25 | @NotNull(message = "Cliente é obrigatório") 26 | @ManyToOne(fetch = FetchType.LAZY) 27 | @JoinColumn(name = "customer_id", nullable = false) 28 | private Customer customer; 29 | 30 | @Enumerated(EnumType.STRING) 31 | @Column(nullable = false) 32 | private OrderStatus status; 33 | 34 | @DecimalMin(value = "0.01", message = "Valor total deve ser maior que zero") 35 | @Column(name = "total_amount", nullable = false, precision = 10, scale = 2) 36 | private BigDecimal totalAmount; 37 | 38 | @Column(name = "payment_method") 39 | private String paymentMethod; 40 | 41 | @Column(name = "abacate_transaction_id") 42 | private String abacateTransactionId; 43 | 44 | @Column(name = "payment_link") 45 | private String paymentLink; 46 | 47 | @Column(name = "created_at", nullable = false) 48 | private LocalDateTime createdAt; 49 | 50 | @Column(name = "updated_at") 51 | private LocalDateTime updatedAt; 52 | 53 | @Column(name = "paid_at") 54 | private LocalDateTime paidAt; 55 | 56 | @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY) 57 | private List items; 58 | 59 | @PrePersist 60 | protected void onCreate() { 61 | createdAt = LocalDateTime.now(); 62 | updatedAt = LocalDateTime.now(); 63 | if (status == null) { 64 | status = OrderStatus.PENDING_PAYMENT; 65 | } 66 | } 67 | 68 | @PreUpdate 69 | protected void onUpdate() { 70 | updatedAt = LocalDateTime.now(); 71 | if (status == OrderStatus.PAID && paidAt == null) { 72 | paidAt = LocalDateTime.now(); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/entity/OrderItem.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.entity; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.DecimalMin; 5 | import jakarta.validation.constraints.Min; 6 | import jakarta.validation.constraints.NotBlank; 7 | import jakarta.validation.constraints.NotNull; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.math.BigDecimal; 13 | 14 | @Entity 15 | @Table(name = "order_items") 16 | @Data 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class OrderItem { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Long id; 24 | 25 | @NotNull(message = "Pedido é obrigatório") 26 | @ManyToOne(fetch = FetchType.LAZY) 27 | @JoinColumn(name = "order_id", nullable = false) 28 | private Order order; 29 | 30 | @NotBlank(message = "Nome do produto é obrigatório") 31 | @Column(name = "product_name", nullable = false) 32 | private String productName; 33 | 34 | @NotBlank(message = "SKU do produto é obrigatório") 35 | @Column(name = "product_sku", nullable = false) 36 | private String productSku; 37 | 38 | @Min(value = 1, message = "Quantidade deve ser maior que zero") 39 | @Column(nullable = false) 40 | private Integer quantity; 41 | 42 | @DecimalMin(value = "0.01", message = "Preço unitário deve ser maior que zero") 43 | @Column(name = "unit_price", nullable = false, precision = 10, scale = 2) 44 | private BigDecimal unitPrice; 45 | 46 | @DecimalMin(value = "0.01", message = "Preço total deve ser maior que zero") 47 | @Column(name = "total_price", nullable = false, precision = 10, scale = 2) 48 | private BigDecimal totalPrice; 49 | 50 | @PrePersist 51 | @PreUpdate 52 | protected void calculateTotalPrice() { 53 | if (unitPrice != null && quantity != null) { 54 | totalPrice = unitPrice.multiply(BigDecimal.valueOf(quantity)); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/entity/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.entity; 2 | 3 | public enum OrderStatus { 4 | PENDING_PAYMENT("Aguardando Pagamento"), 5 | PAID("Pago"), 6 | FAILED("Falha no Pagamento"), 7 | READY_TO_SHIP("Pronto para Envio"), 8 | SHIPPED("Enviado"), 9 | COMPLETED("Concluído"), 10 | CANCELLED("Cancelado"); 11 | 12 | private final String description; 13 | 14 | OrderStatus(String description) { 15 | this.description = description; 16 | } 17 | 18 | public String getDescription() { 19 | return description; 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/event/InvoiceGeneratedEvent.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.LocalDateTime; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class InvoiceGeneratedEvent { 14 | 15 | private Long orderId; 16 | private String invoiceNumber; 17 | private BigDecimal invoiceAmount; 18 | private String invoiceUrl; 19 | private String status; // GENERATED, FAILED 20 | private LocalDateTime generatedAt; 21 | private String message; 22 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/event/OrderCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class OrderCreatedEvent { 15 | 16 | private Long orderId; 17 | private String customerEmail; 18 | private String customerName; 19 | private BigDecimal totalAmount; 20 | private String paymentMethod; 21 | private List items; 22 | private LocalDateTime createdAt; 23 | 24 | @Data 25 | @NoArgsConstructor 26 | @AllArgsConstructor 27 | public static class OrderItemEvent { 28 | private String productSku; 29 | private String productName; 30 | private Integer quantity; 31 | private BigDecimal unitPrice; 32 | private BigDecimal totalPrice; 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/event/PaymentConfirmedEvent.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.LocalDateTime; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentConfirmedEvent { 14 | 15 | private Long orderId; 16 | private String customerEmail; 17 | private BigDecimal amount; 18 | private String abacateTransactionId; 19 | private String paymentMethod; 20 | private LocalDateTime paidAt; 21 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/event/StockReservedEvent.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class StockReservedEvent { 14 | 15 | private Long orderId; 16 | private String status; // SUCCESS, FAILED, PARTIAL 17 | private List items; 18 | private LocalDateTime reservedAt; 19 | private String message; 20 | 21 | @Data 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | public static class StockItem { 25 | private String productSku; 26 | private Integer requestedQuantity; 27 | private Integer reservedQuantity; 28 | private String status; 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/mapper/OrderMapper.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.mapper; 2 | 3 | import com.kipperdev.orderhub.dto.*; 4 | import com.kipperdev.orderhub.entity.Customer; 5 | import com.kipperdev.orderhub.entity.Order; 6 | import com.kipperdev.orderhub.entity.OrderItem; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.Mapping; 9 | import org.mapstruct.Named; 10 | 11 | import java.util.List; 12 | 13 | @Mapper(componentModel = "spring") 14 | public interface OrderMapper { 15 | 16 | // Customer mappings 17 | CustomerDTO toCustomerDTO(Customer customer); 18 | Customer toCustomerEntity(CustomerDTO customerDTO); 19 | List toCustomerDTOList(List customers); 20 | 21 | // OrderItem mappings 22 | OrderItemDTO toOrderItemDTO(OrderItem orderItem); 23 | OrderItem toOrderItemEntity(OrderItemDTO orderItemDTO); 24 | List toOrderItemDTOList(List orderItems); 25 | 26 | // Order mappings 27 | @Mapping(target = "statusUrl", source = "id", qualifiedByName = "generateStatusUrl") 28 | OrderResponseDTO toOrderResponseDTO(Order order); 29 | 30 | @Mapping(target = "id", ignore = true) 31 | @Mapping(target = "status", ignore = true) 32 | @Mapping(target = "totalAmount", ignore = true) 33 | @Mapping(target = "abacateTransactionId", ignore = true) 34 | @Mapping(target = "paymentLink", ignore = true) 35 | @Mapping(target = "createdAt", ignore = true) 36 | @Mapping(target = "updatedAt", ignore = true) 37 | @Mapping(target = "paidAt", ignore = true) 38 | Order toOrderEntity(CreateOrderRequestDTO createOrderRequestDTO); 39 | 40 | @Mapping(target = "statusDescription", source = "status", qualifiedByName = "getStatusDescription") 41 | @Mapping(target = "customerEmail", source = "customer.email") 42 | OrderStatusDTO toOrderStatusDTO(Order order); 43 | 44 | List toOrderResponseDTOList(List orders); 45 | 46 | @Named("generateStatusUrl") 47 | default String generateStatusUrl(Long orderId) { 48 | return "/public/orders/" + orderId + "/status"; 49 | } 50 | 51 | @Named("getStatusDescription") 52 | default String getStatusDescription(com.kipperdev.orderhub.entity.OrderStatus status) { 53 | return status != null ? status.getDescription() : null; 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.repository; 2 | 3 | import com.kipperdev.orderhub.entity.Customer; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface CustomerRepository extends JpaRepository { 11 | 12 | Optional findByEmail(String email); 13 | 14 | boolean existsByEmail(String email); 15 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/repository/OrderItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.repository; 2 | 3 | import com.kipperdev.orderhub.entity.OrderItem; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface OrderItemRepository extends JpaRepository { 13 | 14 | List findByOrderId(Long orderId); 15 | 16 | @Query("SELECT oi FROM OrderItem oi WHERE oi.productSku = :sku") 17 | List findByProductSku(@Param("sku") String sku); 18 | 19 | @Query("SELECT SUM(oi.quantity) FROM OrderItem oi WHERE oi.productSku = :sku") 20 | Integer getTotalQuantityByProductSku(@Param("sku") String sku); 21 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.repository; 2 | 3 | import com.kipperdev.orderhub.entity.Order; 4 | import com.kipperdev.orderhub.entity.OrderStatus; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.repository.query.Param; 11 | import org.springframework.stereotype.Repository; 12 | 13 | import java.time.LocalDateTime; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | @Repository 18 | public interface OrderRepository extends JpaRepository, JpaSpecificationExecutor { 19 | 20 | Optional findByAbacateTransactionId(String abacateTransactionId); 21 | 22 | List findByCustomerEmail(String email); 23 | 24 | Page findByStatus(OrderStatus status, Pageable pageable); 25 | 26 | @Query("SELECT o FROM Order o WHERE o.customer.email = :email AND o.createdAt BETWEEN :startDate AND :endDate") 27 | List findByCustomerEmailAndDateRange( 28 | @Param("email") String email, 29 | @Param("startDate") LocalDateTime startDate, 30 | @Param("endDate") LocalDateTime endDate 31 | ); 32 | 33 | @Query("SELECT COUNT(o) FROM Order o WHERE o.status = :status") 34 | Long countByStatus(@Param("status") OrderStatus status); 35 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/service/AbacatePayService.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.service; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.stereotype.Service; 12 | 13 | import com.kipperdev.orderhub.client.AbacatePayClient; 14 | import com.kipperdev.orderhub.dto.abacate.AbacateChargeRequestDTO; 15 | import com.kipperdev.orderhub.dto.abacate.AbacateChargeResponseDTO; 16 | import com.kipperdev.orderhub.dto.abacate.AbacateCustomerDTO; 17 | import com.kipperdev.orderhub.dto.abacate.AbacateCustomerResponseDTO; 18 | import com.kipperdev.orderhub.entity.Order; 19 | import com.kipperdev.orderhub.entity.OrderStatus; 20 | import com.kipperdev.orderhub.repository.OrderRepository; 21 | 22 | import lombok.RequiredArgsConstructor; 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | @Service 26 | @RequiredArgsConstructor 27 | @Slf4j 28 | public class AbacatePayService { 29 | 30 | private final AbacatePayClient abacatePayClient; 31 | private final OrderRepository orderRepository; 32 | 33 | @Value("${abacate.api.mock-enabled:true}") 34 | private boolean mockEnabled; 35 | 36 | @Value("${app.base-url:http://localhost:8080}") 37 | private String baseUrl; 38 | 39 | public String createPayment(Order order) { 40 | try { 41 | AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO abacateCustomer = getOrCreateAbacateCustomer(order); 42 | 43 | AbacateChargeRequestDTO billingRequest = getAbacateChargeRequestDTO(order, abacateCustomer); 44 | 45 | AbacateChargeResponseDTO.AbacateChargeDataDTO billingResponse; 46 | 47 | if (mockEnabled) { 48 | billingResponse = createMockBilling(billingRequest); 49 | log.info("Billing mockado criado para pedido {}: {}", order.getId(), billingResponse.getId()); 50 | } else { 51 | billingResponse = abacatePayClient.createBilling(billingRequest).getData(); 52 | log.info("Billing real criado para pedido {}: {}", order.getId(), billingResponse.getId()); 53 | } 54 | 55 | order.setAbacateTransactionId(billingResponse.getId()); 56 | 57 | return billingResponse.getId(); 58 | 59 | } catch (Exception e) { 60 | log.error("Erro ao criar pagamento no Abacate Pay para pedido {}: {}", order.getId(), e.getMessage()); 61 | throw new RuntimeException("Falha na integração com gateway de pagamento", e); 62 | } 63 | } 64 | 65 | private AbacateChargeRequestDTO getAbacateChargeRequestDTO(Order order, AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO abacateCustomer) { 66 | AbacateChargeRequestDTO billingRequest = new AbacateChargeRequestDTO(); 67 | billingRequest.setFrequency("ONE_TIME"); 68 | billingRequest.setMethods(Arrays.asList("PIX")); 69 | 70 | AbacateChargeRequestDTO.AbacateProductDTO product = new AbacateChargeRequestDTO.AbacateProductDTO(); 71 | product.setExternalId(order.getId().toString()); 72 | product.setName("Pedido #" + order.getId()); 73 | product.setQuantity(1); 74 | product.setDescription("Pagamento do pedido #" + order.getId()); 75 | 76 | product.setPrice(order.getTotalAmount().multiply(BigDecimal.valueOf(100)).intValue()); 77 | 78 | billingRequest.setProducts(Collections.singletonList(product)); 79 | billingRequest.setReturnUrl(baseUrl + "/orders/" + order.getId()); 80 | billingRequest.setCompletionUrl(baseUrl + "/orders/" + order.getId() + "/success"); 81 | billingRequest.setCustomer(abacateCustomer.getMetadata()); 82 | billingRequest.setAbacateCustomerId(abacateCustomer.getId()); 83 | return billingRequest; 84 | } 85 | 86 | private AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO getOrCreateAbacateCustomer(Order order) { 87 | if (mockEnabled) { 88 | AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO mockCustomer = new AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO(); 89 | mockCustomer.setId("cust_" + UUID.randomUUID().toString().substring(0, 8)); 90 | 91 | AbacateCustomerDTO metadata = new AbacateCustomerDTO(); 92 | metadata.setName(order.getCustomer().getName()); 93 | metadata.setEmail(order.getCustomer().getEmail()); 94 | metadata.setCellphone(order.getCustomer().getPhone()); 95 | metadata.setTaxId(""); 96 | 97 | mockCustomer.setMetadata(metadata); 98 | return mockCustomer; 99 | } else { 100 | try { 101 | AbacateCustomerDTO customerRequest = new AbacateCustomerDTO(); 102 | customerRequest.setName(order.getCustomer().getName()); 103 | customerRequest.setEmail(order.getCustomer().getEmail()); 104 | customerRequest.setCellphone(order.getCustomer().getPhone()); 105 | customerRequest.setTaxId(order.getCustomer().getDocument()); 106 | 107 | log.info("Criando cliente no Abacate Pay: name={}, email={}, cellphone={}, taxId={}", 108 | customerRequest.getName(), customerRequest.getEmail(), 109 | customerRequest.getCellphone(), customerRequest.getTaxId()); 110 | 111 | return abacatePayClient.createCustomer(customerRequest).getData(); 112 | } catch (Exception e) { 113 | log.error("Erro ao criar/buscar cliente no Abacatepay: {}", e.getMessage()); 114 | throw new RuntimeException("Falha ao processar cliente no gateway de pagamento", e); 115 | } 116 | } 117 | } 118 | 119 | private AbacateChargeResponseDTO.AbacateChargeDataDTO createMockBilling(AbacateChargeRequestDTO request) { 120 | AbacateChargeResponseDTO.AbacateChargeDataDTO response = new AbacateChargeResponseDTO.AbacateChargeDataDTO(); 121 | response.setId("bill_" + UUID.randomUUID().toString().substring(0, 8)); 122 | response.setAmount(request.getProducts().get(0).getPrice()); 123 | response.setStatus("PENDING"); 124 | response.setFrequency(request.getFrequency()); 125 | response.setMethods(request.getMethods()); 126 | response.setCreatedAt(LocalDateTime.now()); 127 | response.setCustomer(new AbacateCustomerResponseDTO.AbacateCustomerMetadataDTO("cust_13455", request.getCustomer())); 128 | response.setProducts(Collections.singletonList( 129 | new AbacateChargeResponseDTO.AbacateChargeDataDTO.AbacateProductResponseDTO( 130 | "prod_" + UUID.randomUUID().toString().substring(0, 8), 131 | request.getProducts().get(0).getExternalId(), 132 | request.getProducts().get(0).getQuantity() 133 | ) 134 | )); 135 | 136 | String paymentLink = baseUrl + "/mock-payment/" + response.getId(); 137 | response.setUrl(paymentLink); 138 | 139 | return response; 140 | } 141 | 142 | // public AbacateChargeResponseDTO getBilling(String billingId) { 143 | // if (mockEnabled) { 144 | // AbacateChargeResponseDTO mockResponse = new AbacateChargeResponseDTO(); 145 | // AbacateChargeResponseDTO.AbacateChargeDataDTO mockData = new AbacateChargeResponseDTO.AbacateChargeDataDTO(); 146 | // mockData.setId(billingId); 147 | // mockData.setStatus("PENDING"); 148 | // mockData.setFrequency("ONE_TIME"); 149 | // mockData.setMethods(Arrays.asList("PIX")); 150 | // mockData.setCreatedAt(LocalDateTime.now()); 151 | // return mockResponse; 152 | // } else { 153 | // AbacateChargeResponseDTO getResponse = abacatePayClient.getBilling(billingId); 154 | // return getResponse; 155 | // } 156 | // } 157 | 158 | public void processWebhook(String billingId, String event, String status) { 159 | try { 160 | log.info("Processando webhook do Abacatepay - Billing: {}, Event: {}, Status: {}", 161 | billingId, event, status); 162 | 163 | Optional orderOpt = orderRepository.findByAbacateTransactionId(billingId); 164 | 165 | if (orderOpt.isPresent()) { 166 | Order order = orderOpt.get(); 167 | OrderStatus newStatus = mapAbacateStatusToOrderStatus(status, event); 168 | 169 | if (!newStatus.equals(order.getStatus())) { 170 | order.setStatus(newStatus); 171 | if (newStatus == OrderStatus.PAID) { 172 | order.setPaidAt(LocalDateTime.now()); 173 | } 174 | orderRepository.save(order); 175 | 176 | log.info("Pedido {} atualizado via webhook do Abacate Pay para status: {}", order.getId(), newStatus); 177 | } 178 | } else { 179 | log.warn("Pedido não encontrado para billing ID: {}", billingId); 180 | } 181 | } catch (Exception e) { 182 | log.error("Erro ao processar webhook do Abacatepay: {}", e.getMessage(), e); 183 | } 184 | } 185 | 186 | private OrderStatus mapAbacateStatusToOrderStatus(String status, String event) { 187 | if ("billing.paid".equals(event) || "PAID".equals(status)) { 188 | return OrderStatus.PAID; 189 | } else if ("billing.failed".equals(event) || "billing.cancelled".equals(event) || "FAILED".equals(status)) { 190 | return OrderStatus.FAILED; 191 | } else if ("PENDING".equals(status)) { 192 | return OrderStatus.PENDING_PAYMENT; 193 | } 194 | return null; 195 | } 196 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/service/CustomerService.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.service; 2 | 3 | import com.kipperdev.orderhub.dto.CustomerDTO; 4 | import com.kipperdev.orderhub.entity.Customer; 5 | import com.kipperdev.orderhub.mapper.OrderMapper; 6 | import com.kipperdev.orderhub.repository.CustomerRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.util.Optional; 13 | 14 | @Service 15 | @RequiredArgsConstructor 16 | @Slf4j 17 | public class CustomerService { 18 | 19 | private final CustomerRepository customerRepository; 20 | private final OrderMapper orderMapper; 21 | 22 | @Transactional 23 | public Customer getOrCreateCustomer(CustomerDTO customerDTO) { 24 | Optional existingCustomer = customerRepository.findByEmail(customerDTO.getEmail()); 25 | 26 | if (existingCustomer.isPresent()) { 27 | Customer customer = existingCustomer.get(); 28 | if (!customer.getName().equals(customerDTO.getName()) || 29 | !customer.getPhone().equals(customerDTO.getPhone()) || 30 | !customer.getDocument().equals(customerDTO.getDocument())) { 31 | customer.setName(customerDTO.getName()); 32 | customer.setPhone(customerDTO.getPhone()); 33 | customer.setDocument(customerDTO.getDocument()); 34 | customer = customerRepository.save(customer); 35 | log.info("Cliente atualizado: {}", customer.getEmail()); 36 | } 37 | return customer; 38 | } else { 39 | Customer newCustomer = orderMapper.toCustomerEntity(customerDTO); 40 | newCustomer = customerRepository.save(newCustomer); 41 | log.info("Novo cliente criado: {}", newCustomer.getEmail()); 42 | return newCustomer; 43 | } 44 | } 45 | 46 | public Optional findByEmail(String email) { 47 | return customerRepository.findByEmail(email) 48 | .map(orderMapper::toCustomerDTO); 49 | } 50 | 51 | public boolean existsByEmail(String email) { 52 | return customerRepository.existsByEmail(email); 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/service/KafkaConsumerService.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.kipperdev.orderhub.entity.OrderStatus; 5 | import com.kipperdev.orderhub.event.InvoiceGeneratedEvent; 6 | import com.kipperdev.orderhub.event.StockReservedEvent; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.kafka.annotation.KafkaListener; 11 | import org.springframework.kafka.annotation.RetryableTopic; 12 | import org.springframework.kafka.support.Acknowledgment; 13 | import org.springframework.kafka.support.KafkaHeaders; 14 | import org.springframework.messaging.handler.annotation.Header; 15 | import org.springframework.messaging.handler.annotation.Payload; 16 | import org.springframework.retry.annotation.Backoff; 17 | import org.springframework.stereotype.Service; 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | @Slf4j 22 | @ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = false) 23 | public class KafkaConsumerService { 24 | 25 | private final OrderService orderService; 26 | private final ObjectMapper objectMapper; 27 | 28 | @RetryableTopic( 29 | attempts = "3", 30 | backoff = @Backoff(delay = 1000, multiplier = 2.0), 31 | dltStrategy = org.springframework.kafka.retrytopic.DltStrategy.FAIL_ON_ERROR, 32 | kafkaTemplate = "stringKafkaTemplate" 33 | ) 34 | @KafkaListener(topics = "stock.reserved", groupId = "orderhub-stock-group") 35 | public void handleStockReservedEvent( 36 | @Payload String message, 37 | @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, 38 | @Header(KafkaHeaders.RECEIVED_PARTITION) int partition, 39 | @Header(KafkaHeaders.OFFSET) long offset, 40 | Acknowledgment acknowledgment) { 41 | 42 | try { 43 | log.info("Recebido evento de estoque reservado: topic={}, partition={}, offset={}", 44 | topic, partition, offset); 45 | 46 | StockReservedEvent event = objectMapper.readValue(message, StockReservedEvent.class); 47 | 48 | // Processar evento de estoque 49 | if ("SUCCESS".equals(event.getStatus())) { 50 | orderService.updateOrderStatus(event.getOrderId(), OrderStatus.READY_TO_SHIP); 51 | log.info("Pedido {} atualizado para READY_TO_SHIP após reserva de estoque", event.getOrderId()); 52 | } else { 53 | log.warn("Falha na reserva de estoque para pedido {}: {}", event.getOrderId(), event.getMessage()); 54 | // Aqui poderia implementar lógica de compensação 55 | } 56 | 57 | acknowledgment.acknowledge(); 58 | 59 | } catch (Exception e) { 60 | log.error("Erro ao processar evento de estoque reservado: {}", e.getMessage(), e); 61 | throw new RuntimeException("Falha no processamento do evento de estoque", e); 62 | } 63 | } 64 | 65 | @RetryableTopic( 66 | attempts = "3", 67 | backoff = @Backoff(delay = 1000, multiplier = 2.0), 68 | dltStrategy = org.springframework.kafka.retrytopic.DltStrategy.FAIL_ON_ERROR, 69 | kafkaTemplate = "stringKafkaTemplate" 70 | ) 71 | @KafkaListener(topics = "invoice.generated", groupId = "orderhub-invoice-group") 72 | public void handleInvoiceGeneratedEvent( 73 | @Payload String message, 74 | @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, 75 | @Header(KafkaHeaders.RECEIVED_PARTITION) int partition, 76 | @Header(KafkaHeaders.OFFSET) long offset, 77 | Acknowledgment acknowledgment) { 78 | 79 | try { 80 | log.info("Recebido evento de nota fiscal gerada: topic={}, partition={}, offset={}", 81 | topic, partition, offset); 82 | 83 | InvoiceGeneratedEvent event = objectMapper.readValue(message, InvoiceGeneratedEvent.class); 84 | 85 | // Processar evento de nota fiscal 86 | if ("GENERATED".equals(event.getStatus())) { 87 | orderService.updateOrderStatus(event.getOrderId(), OrderStatus.COMPLETED); 88 | log.info("Pedido {} finalizado após geração da nota fiscal {}", 89 | event.getOrderId(), event.getInvoiceNumber()); 90 | } else { 91 | log.warn("Falha na geração da nota fiscal para pedido {}: {}", 92 | event.getOrderId(), event.getMessage()); 93 | } 94 | 95 | acknowledgment.acknowledge(); 96 | 97 | } catch (Exception e) { 98 | log.error("Erro ao processar evento de nota fiscal gerada: {}", e.getMessage(), e); 99 | throw new RuntimeException("Falha no processamento do evento de nota fiscal", e); 100 | } 101 | } 102 | 103 | // Listener para Dead Letter Topic (DLT) 104 | @KafkaListener(topics = "stock.reserved.DLT", groupId = "orderhub-stock-dlt-group") 105 | public void handleStockReservedDLT( 106 | @Payload String message, 107 | @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, 108 | Acknowledgment acknowledgment) { 109 | 110 | log.error("Mensagem enviada para DLT de estoque: topic={}, message={}", topic, message); 111 | // Aqui poderia implementar notificação para equipe de suporte 112 | acknowledgment.acknowledge(); 113 | } 114 | 115 | @KafkaListener(topics = "invoice.generated.DLT", groupId = "orderhub-invoice-dlt-group") 116 | public void handleInvoiceGeneratedDLT( 117 | @Payload String message, 118 | @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, 119 | Acknowledgment acknowledgment) { 120 | 121 | log.error("Mensagem enviada para DLT de nota fiscal: topic={}, message={}", topic, message); 122 | // Aqui poderia implementar notificação para equipe de suporte 123 | acknowledgment.acknowledge(); 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/service/KafkaProducerService.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.service; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.kipperdev.orderhub.entity.Order; 6 | import com.kipperdev.orderhub.event.OrderCreatedEvent; 7 | import com.kipperdev.orderhub.event.PaymentConfirmedEvent; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.kafka.core.KafkaTemplate; 12 | import org.springframework.kafka.support.SendResult; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.util.concurrent.CompletableFuture; 16 | 17 | @Service 18 | @Slf4j 19 | @ConditionalOnProperty(name = "spring.kafka.enabled", havingValue = "true", matchIfMissing = false) 20 | public class KafkaProducerService { 21 | 22 | private final KafkaTemplate kafkaTemplate; 23 | private final ObjectMapper objectMapper; 24 | 25 | public KafkaProducerService(KafkaTemplate stringKafkaTemplate, ObjectMapper objectMapper) { 26 | this.kafkaTemplate = stringKafkaTemplate; 27 | this.objectMapper = objectMapper; 28 | } 29 | 30 | private static final String ORDERS_CREATED_TOPIC = "orders.created"; 31 | private static final String PAYMENTS_CONFIRMED_TOPIC = "payments.confirmed"; 32 | 33 | public void publishOrderCreatedEvent(OrderCreatedEvent event) { 34 | try { 35 | String eventJson = objectMapper.writeValueAsString(event); 36 | String key = "order-" + event.getOrderId(); 37 | 38 | CompletableFuture> future = 39 | kafkaTemplate.send(ORDERS_CREATED_TOPIC, key, eventJson); 40 | 41 | future.whenComplete((result, ex) -> { 42 | if (ex == null) { 43 | log.info("Evento OrderCreated publicado com sucesso para pedido {}: offset={}", 44 | event.getOrderId(), result.getRecordMetadata().offset()); 45 | } else { 46 | log.error("Falha ao publicar evento OrderCreated para pedido {}: {}", 47 | event.getOrderId(), ex.getMessage()); 48 | } 49 | }); 50 | 51 | } catch (JsonProcessingException e) { 52 | log.error("Erro ao serializar evento OrderCreated para pedido {}: {}", 53 | event.getOrderId(), e.getMessage()); 54 | } 55 | } 56 | 57 | public void publishPaymentConfirmedEvent(Order order) { 58 | try { 59 | PaymentConfirmedEvent event = new PaymentConfirmedEvent(); 60 | event.setOrderId(order.getId()); 61 | event.setCustomerEmail(order.getCustomer().getEmail()); 62 | event.setAmount(order.getTotalAmount()); 63 | event.setAbacateTransactionId(order.getAbacateTransactionId()); 64 | event.setPaymentMethod(order.getPaymentMethod()); 65 | event.setPaidAt(order.getPaidAt()); 66 | 67 | String eventJson = objectMapper.writeValueAsString(event); 68 | String key = "payment-" + order.getId(); 69 | 70 | CompletableFuture> future = 71 | kafkaTemplate.send(PAYMENTS_CONFIRMED_TOPIC, key, eventJson); 72 | 73 | future.whenComplete((result, ex) -> { 74 | if (ex == null) { 75 | log.info("Evento PaymentConfirmed publicado com sucesso para pedido {}: offset={}", 76 | order.getId(), result.getRecordMetadata().offset()); 77 | } else { 78 | log.error("Falha ao publicar evento PaymentConfirmed para pedido {}: {}", 79 | order.getId(), ex.getMessage()); 80 | } 81 | }); 82 | 83 | } catch (JsonProcessingException e) { 84 | log.error("Erro ao serializar evento PaymentConfirmed para pedido {}: {}", 85 | order.getId(), e.getMessage()); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.service; 2 | 3 | import com.kipperdev.orderhub.dto.*; 4 | import com.kipperdev.orderhub.entity.Customer; 5 | import com.kipperdev.orderhub.entity.Order; 6 | import com.kipperdev.orderhub.entity.OrderItem; 7 | import com.kipperdev.orderhub.entity.OrderStatus; 8 | import com.kipperdev.orderhub.event.OrderCreatedEvent; 9 | import com.kipperdev.orderhub.mapper.OrderMapper; 10 | import com.kipperdev.orderhub.repository.CustomerRepository; 11 | import com.kipperdev.orderhub.repository.OrderRepository; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.Pageable; 16 | import org.springframework.data.jpa.domain.Specification; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import java.math.BigDecimal; 21 | import java.time.LocalDateTime; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Optional; 26 | import java.util.stream.Collectors; 27 | 28 | @Service 29 | @Slf4j 30 | public class OrderService { 31 | 32 | private final OrderRepository orderRepository; 33 | private final CustomerRepository customerRepository; 34 | private final OrderMapper orderMapper; 35 | private final CustomerService customerService; 36 | private final AbacatePayService abacatePayService; 37 | private final KafkaProducerService kafkaProducerService; 38 | 39 | public OrderService(OrderRepository orderRepository, 40 | CustomerRepository customerRepository, 41 | OrderMapper orderMapper, 42 | CustomerService customerService, 43 | AbacatePayService abacatePayService, 44 | @Autowired(required = false) KafkaProducerService kafkaProducerService) { 45 | this.orderRepository = orderRepository; 46 | this.customerRepository = customerRepository; 47 | this.orderMapper = orderMapper; 48 | this.customerService = customerService; 49 | this.abacatePayService = abacatePayService; 50 | this.kafkaProducerService = kafkaProducerService; 51 | } 52 | 53 | @Transactional 54 | public OrderResponseDTO createOrder(CreateOrderRequestDTO request) { 55 | log.info("Criando novo pedido para cliente: {}", request.getCustomer().getEmail()); 56 | 57 | Customer customer = customerService.getOrCreateCustomer(request.getCustomer()); 58 | 59 | Order order = new Order(); 60 | order.setCustomer(customer); 61 | order.setPaymentMethod(request.getPaymentMethod()); 62 | order.setStatus(OrderStatus.PENDING_PAYMENT); 63 | 64 | List orderItems = request.getItems().stream() 65 | .map(itemDTO -> { 66 | OrderItem item = orderMapper.toOrderItemEntity(itemDTO); 67 | item.setOrder(order); 68 | // Calcular totalPrice manualmente antes de usar 69 | if (item.getUnitPrice() != null && item.getQuantity() != null) { 70 | item.setTotalPrice(item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); 71 | } 72 | return item; 73 | }) 74 | .collect(Collectors.toList()); 75 | 76 | order.setItems(orderItems); 77 | 78 | BigDecimal totalAmount = orderItems.stream() 79 | .map(OrderItem::getTotalPrice) 80 | .reduce(BigDecimal.ZERO, BigDecimal::add); 81 | order.setTotalAmount(totalAmount); 82 | 83 | Order savedOrder = orderRepository.save(order); 84 | log.info("Pedido criado com ID: {}", savedOrder.getId()); 85 | 86 | try { 87 | String paymentLink = abacatePayService.createPayment(savedOrder); 88 | savedOrder.setPaymentLink(paymentLink); 89 | savedOrder = orderRepository.save(savedOrder); 90 | } catch (Exception e) { 91 | log.error("Erro ao criar pagamento no Abacate Pay para pedido {}: {}", savedOrder.getId(), e.getMessage()); 92 | } 93 | 94 | publishOrderCreatedEvent(savedOrder); 95 | 96 | return orderMapper.toOrderResponseDTO(savedOrder); 97 | } 98 | 99 | public Optional getOrderStatus(Long orderId) { 100 | return orderRepository.findById(orderId) 101 | .map(orderMapper::toOrderStatusDTO); 102 | } 103 | 104 | public List getOrdersByCustomerEmail(String email) { 105 | List orders = orderRepository.findByCustomerEmail(email); 106 | return orders.stream() 107 | .map(orderMapper::toOrderStatusDTO) 108 | .collect(Collectors.toList()); 109 | } 110 | 111 | public Page filterOrders(Specification spec, Pageable pageable) { 112 | Page orders = orderRepository.findAll(spec, pageable); 113 | return orders.map(orderMapper::toOrderResponseDTO); 114 | } 115 | 116 | public Optional getOrderById(Long orderId) { 117 | return orderRepository.findById(orderId) 118 | .map(orderMapper::toOrderResponseDTO); 119 | } 120 | 121 | public Map getOrderStatistics(LocalDateTime startDate, LocalDateTime endDate) { 122 | Map stats = new HashMap<>(); 123 | 124 | long totalOrders = orderRepository.count(); 125 | stats.put("totalOrders", totalOrders); 126 | 127 | for (OrderStatus status : OrderStatus.values()) { 128 | long count = orderRepository.countByStatus(status); 129 | stats.put("orders" + status.name(), count); 130 | } 131 | 132 | List paidOrders = orderRepository.findByStatus(OrderStatus.PAID, Pageable.unpaged()).getContent(); 133 | BigDecimal totalRevenue = paidOrders.stream() 134 | .map(Order::getTotalAmount) 135 | .reduce(BigDecimal.ZERO, BigDecimal::add); 136 | stats.put("totalRevenue", totalRevenue); 137 | 138 | return stats; 139 | } 140 | 141 | public String exportOrders(OrderStatus status, String customerEmail, 142 | LocalDateTime startDate, LocalDateTime endDate, String format) { 143 | List orders = orderRepository.findAll(); 144 | 145 | if ("csv".equalsIgnoreCase(format)) { 146 | StringBuilder csv = new StringBuilder(); 147 | csv.append("ID,Cliente,Email,Status,Valor Total,Data Criação\n"); 148 | 149 | for (Order order : orders) { 150 | csv.append(order.getId()).append(",") 151 | .append(order.getCustomer().getName()).append(",") 152 | .append(order.getCustomer().getEmail()).append(",") 153 | .append(order.getStatus()).append(",") 154 | .append(order.getTotalAmount()).append(",") 155 | .append(order.getCreatedAt()).append("\n"); 156 | } 157 | 158 | return csv.toString(); 159 | } else { 160 | return orders.stream() 161 | .map(orderMapper::toOrderResponseDTO) 162 | .collect(Collectors.toList()) 163 | .toString(); 164 | } 165 | } 166 | 167 | @Transactional 168 | public OrderResponseDTO updateOrderStatus(Long orderId, OrderStatus newStatus) { 169 | Order order = orderRepository.findById(orderId) 170 | .orElseThrow(() -> new RuntimeException("Pedido não encontrado: " + orderId)); 171 | 172 | OrderStatus oldStatus = order.getStatus(); 173 | order.setStatus(newStatus); 174 | 175 | Order updatedOrder = orderRepository.save(order); 176 | log.info("Status do pedido {} alterado de {} para {}", orderId, oldStatus, newStatus); 177 | 178 | return orderMapper.toOrderResponseDTO(updatedOrder); 179 | } 180 | 181 | @Transactional 182 | public void updateOrderFromAbacateWebhook(String abacateTransactionId, String status) { 183 | Optional orderOpt = orderRepository.findByAbacateTransactionId(abacateTransactionId); 184 | 185 | if (orderOpt.isPresent()) { 186 | Order order = orderOpt.get(); 187 | OrderStatus newStatus = mapAbacateStatusToOrderStatus(status); 188 | 189 | if (newStatus != null && !newStatus.equals(order.getStatus())) { 190 | order.setStatus(newStatus); 191 | if (newStatus == OrderStatus.PAID) { 192 | order.setPaidAt(LocalDateTime.now()); 193 | } 194 | orderRepository.save(order); 195 | 196 | log.info("Pedido {} atualizado via webhook do Abacate Pay para status: {}", order.getId(), newStatus); 197 | 198 | if (newStatus == OrderStatus.PAID && kafkaProducerService != null) { 199 | kafkaProducerService.publishPaymentConfirmedEvent(order); 200 | } 201 | } 202 | } else { 203 | log.warn("Pedido não encontrado para transação Abacate: {}", abacateTransactionId); 204 | } 205 | } 206 | 207 | private void publishOrderCreatedEvent(Order order) { 208 | if (kafkaProducerService == null) { 209 | log.debug("KafkaProducerService não disponível, pulando publicação de evento"); 210 | return; 211 | } 212 | 213 | try { 214 | OrderCreatedEvent event = new OrderCreatedEvent(); 215 | event.setOrderId(order.getId()); 216 | event.setCustomerEmail(order.getCustomer().getEmail()); 217 | event.setCustomerName(order.getCustomer().getName()); 218 | event.setTotalAmount(order.getTotalAmount()); 219 | event.setPaymentMethod(order.getPaymentMethod()); 220 | event.setCreatedAt(order.getCreatedAt()); 221 | 222 | List itemEvents = order.getItems().stream() 223 | .map(item -> new OrderCreatedEvent.OrderItemEvent( 224 | item.getProductSku(), 225 | item.getProductName(), 226 | item.getQuantity(), 227 | item.getUnitPrice(), 228 | item.getTotalPrice() 229 | )) 230 | .collect(Collectors.toList()); 231 | event.setItems(itemEvents); 232 | 233 | kafkaProducerService.publishOrderCreatedEvent(event); 234 | } catch (Exception e) { 235 | log.error("Erro ao publicar evento de pedido criado: {}", e.getMessage()); 236 | } 237 | } 238 | 239 | private OrderStatus mapAbacateStatusToOrderStatus(String abacateStatus) { 240 | return switch (abacateStatus.toUpperCase()) { 241 | case "PAID", "COMPLETED" -> OrderStatus.PAID; 242 | case "FAILED", "CANCELLED" -> OrderStatus.FAILED; 243 | case "PENDING" -> OrderStatus.PENDING_PAYMENT; 244 | default -> null; 245 | }; 246 | } 247 | } -------------------------------------------------------------------------------- /src/main/java/com/kipperdev/orderhub/specification/OrderSpecification.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub.specification; 2 | 3 | import com.kipperdev.orderhub.entity.Order; 4 | import com.kipperdev.orderhub.entity.OrderStatus; 5 | import jakarta.persistence.criteria.Predicate; 6 | import org.springframework.data.jpa.domain.Specification; 7 | 8 | import java.time.LocalDateTime; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class OrderSpecification { 13 | 14 | public static Specification withFilters( 15 | OrderStatus status, 16 | String customerEmail, 17 | LocalDateTime startDate, 18 | LocalDateTime endDate) { 19 | 20 | return (root, query, criteriaBuilder) -> { 21 | List predicates = new ArrayList<>(); 22 | 23 | if (status != null) { 24 | predicates.add(criteriaBuilder.equal(root.get("status"), status)); 25 | } 26 | 27 | if (customerEmail != null && !customerEmail.trim().isEmpty()) { 28 | predicates.add(criteriaBuilder.like( 29 | criteriaBuilder.lower(root.get("customer").get("email")), 30 | "%" + customerEmail.toLowerCase() + "%" 31 | )); 32 | } 33 | 34 | if (startDate != null) { 35 | predicates.add(criteriaBuilder.greaterThanOrEqualTo( 36 | root.get("createdAt"), startDate 37 | )); 38 | } 39 | 40 | if (endDate != null) { 41 | predicates.add(criteriaBuilder.lessThanOrEqualTo( 42 | root.get("createdAt"), endDate 43 | )); 44 | } 45 | 46 | return criteriaBuilder.and(predicates.toArray(new Predicate[0])); 47 | }; 48 | } 49 | 50 | public static Specification byStatus(OrderStatus status) { 51 | return (root, query, criteriaBuilder) -> 52 | criteriaBuilder.equal(root.get("status"), status); 53 | } 54 | 55 | public static Specification byCustomerEmail(String email) { 56 | return (root, query, criteriaBuilder) -> 57 | criteriaBuilder.like( 58 | criteriaBuilder.lower(root.get("customer").get("email")), 59 | "%" + email.toLowerCase() + "%" 60 | ); 61 | } 62 | 63 | public static Specification byDateRange(LocalDateTime startDate, LocalDateTime endDate) { 64 | return (root, query, criteriaBuilder) -> { 65 | List predicates = new ArrayList<>(); 66 | 67 | if (startDate != null) { 68 | predicates.add(criteriaBuilder.greaterThanOrEqualTo( 69 | root.get("createdAt"), startDate 70 | )); 71 | } 72 | 73 | if (endDate != null) { 74 | predicates.add(criteriaBuilder.lessThanOrEqualTo( 75 | root.get("createdAt"), endDate 76 | )); 77 | } 78 | 79 | return criteriaBuilder.and(predicates.toArray(new Predicate[0])); 80 | }; 81 | } 82 | 83 | public static Specification byCustomerName(String customerName) { 84 | return (root, query, criteriaBuilder) -> 85 | criteriaBuilder.like( 86 | criteriaBuilder.lower(root.get("customer").get("name")), 87 | "%" + customerName.toLowerCase() + "%" 88 | ); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | # Local Profile Configuration 2 | # AbacatePay enabled, local Kafka, database via query strings 3 | spring: 4 | application: 5 | name: orderhub 6 | 7 | # Database Configuration - using environment variables 8 | datasource: 9 | url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/orderhub_local?useSSL=false&serverTimezone=UTC} 10 | driver-class-name: org.postgresql.Driver 11 | username: ${DATABASE_USERNAME:fernandakipper} 12 | password: ${DATABASE_PASSWORD:} 13 | 14 | # JPA Configuration 15 | jpa: 16 | hibernate: 17 | ddl-auto: update 18 | show-sql: true 19 | properties: 20 | hibernate: 21 | format_sql: true 22 | dialect: org.hibernate.dialect.PostgreSQLDialect 23 | 24 | # Kafka Configuration - ENABLED for local development 25 | kafka: 26 | enabled: true 27 | bootstrap-servers: localhost:9092 28 | consumer: 29 | group-id: orderhub-local-group 30 | auto-offset-reset: earliest 31 | key-deserializer: org.apache.kafka.common.serialization.StringDeserializer 32 | value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer 33 | properties: 34 | spring.json.trusted.packages: "com.kipperdev.orderhub.event" 35 | producer: 36 | key-serializer: org.apache.kafka.common.serialization.StringSerializer 37 | value-serializer: org.springframework.kafka.support.serializer.JsonSerializer 38 | 39 | # WebFlux Configuration 40 | webflux: 41 | base-path: / 42 | 43 | # Server Configuration 44 | server: 45 | port: 8080 46 | 47 | # Management/Actuator Configuration 48 | management: 49 | endpoints: 50 | web: 51 | exposure: 52 | include: health,info,metrics,kafka 53 | endpoint: 54 | health: 55 | show-details: always 56 | 57 | # Feign Client Configuration 58 | feign: 59 | client: 60 | config: 61 | default: 62 | connect-timeout: 5000 63 | read-timeout: 10000 64 | logger-level: full 65 | 66 | # Abacate Pay Configuration - ENABLED 67 | abacate: 68 | api: 69 | base-url: ${ABACATE_API_BASE_URL:https://api.abacatepay.com} 70 | token: ${ABACATE_API_TOKEN} 71 | mock-enabled: ${ABACATE_API_MOCK_ENABLED:false} 72 | webhook: 73 | secret: ${ABACATE_WEBHOOK_SECRET:abacate_webhook_secret} 74 | signature: 75 | enabled: ${ABACATE_WEBHOOK_SIGNATURE_ENABLED:true} 76 | 77 | # Admin Configuration 78 | admin: 79 | username: admin 80 | password: admin123 81 | 82 | # Logging Configuration 83 | logging: 84 | level: 85 | com.kipperdev.orderhub: DEBUG 86 | org.springframework.kafka: DEBUG 87 | org.springframework.web: INFO 88 | feign: DEBUG 89 | root: WARN 90 | pattern: 91 | console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" 92 | file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" 93 | 94 | # Application Configuration 95 | app: 96 | base-url: http://localhost:8080 97 | order: 98 | status-url-template: "${app.base-url}/public/orders/{orderId}/status" -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | # Production Profile Configuration 2 | # All connections via environment variables 3 | spring: 4 | application: 5 | name: orderhub 6 | 7 | # Database Configuration - using environment variables 8 | datasource: 9 | url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/orderhub} 10 | driver-class-name: org.postgresql.Driver 11 | username: ${DATABASE_USERNAME:orderhub} 12 | password: ${DATABASE_PASSWORD:password} 13 | hikari: 14 | maximum-pool-size: ${DB_POOL_SIZE:20} 15 | minimum-idle: ${DB_POOL_MIN_IDLE:5} 16 | connection-timeout: ${DB_CONNECTION_TIMEOUT:30000} 17 | idle-timeout: ${DB_IDLE_TIMEOUT:600000} 18 | max-lifetime: ${DB_MAX_LIFETIME:1800000} 19 | 20 | # JPA Configuration 21 | jpa: 22 | hibernate: 23 | ddl-auto: ${JPA_DDL_AUTO:validate} 24 | show-sql: ${JPA_SHOW_SQL:false} 25 | properties: 26 | hibernate: 27 | format_sql: false 28 | dialect: org.hibernate.dialect.PostgreSQLDialect 29 | jdbc: 30 | batch_size: 20 31 | order_inserts: true 32 | order_updates: true 33 | 34 | # Kafka Configuration - using environment variables 35 | kafka: 36 | enabled: ${KAFKA_ENABLED:true} 37 | bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} 38 | consumer: 39 | group-id: ${KAFKA_CONSUMER_GROUP_ID:orderhub-prod-group} 40 | auto-offset-reset: ${KAFKA_AUTO_OFFSET_RESET:earliest} 41 | key-deserializer: org.apache.kafka.common.serialization.StringDeserializer 42 | value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer 43 | properties: 44 | spring.json.trusted.packages: "com.kipperdev.orderhub.event" 45 | security.protocol: ${KAFKA_SECURITY_PROTOCOL:PLAINTEXT} 46 | sasl.mechanism: ${KAFKA_SASL_MECHANISM:PLAIN} 47 | sasl.jaas.config: ${KAFKA_SASL_JAAS_CONFIG:} 48 | producer: 49 | key-serializer: org.apache.kafka.common.serialization.StringSerializer 50 | value-serializer: org.springframework.kafka.support.serializer.JsonSerializer 51 | properties: 52 | security.protocol: ${KAFKA_SECURITY_PROTOCOL:PLAINTEXT} 53 | sasl.mechanism: ${KAFKA_SASL_MECHANISM:PLAIN} 54 | sasl.jaas.config: ${KAFKA_SASL_JAAS_CONFIG:} 55 | acks: ${KAFKA_PRODUCER_ACKS:all} 56 | retries: ${KAFKA_PRODUCER_RETRIES:3} 57 | batch.size: ${KAFKA_PRODUCER_BATCH_SIZE:16384} 58 | linger.ms: ${KAFKA_PRODUCER_LINGER_MS:5} 59 | 60 | # WebFlux Configuration 61 | webflux: 62 | base-path: / 63 | 64 | # Server Configuration 65 | server: 66 | port: ${SERVER_PORT:8080} 67 | servlet: 68 | context-path: ${SERVER_CONTEXT_PATH:/} 69 | compression: 70 | enabled: true 71 | mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json 72 | min-response-size: 1024 73 | 74 | # Management/Actuator Configuration 75 | management: 76 | endpoints: 77 | web: 78 | exposure: 79 | include: ${ACTUATOR_ENDPOINTS:health,info,metrics} 80 | base-path: ${ACTUATOR_BASE_PATH:/actuator} 81 | endpoint: 82 | health: 83 | show-details: ${ACTUATOR_HEALTH_SHOW_DETAILS:when-authorized} 84 | metrics: 85 | export: 86 | prometheus: 87 | enabled: ${PROMETHEUS_ENABLED:false} 88 | 89 | # Feign Client Configuration 90 | feign: 91 | client: 92 | config: 93 | default: 94 | connect-timeout: ${FEIGN_CONNECT_TIMEOUT:5000} 95 | read-timeout: ${FEIGN_READ_TIMEOUT:10000} 96 | logger-level: ${FEIGN_LOG_LEVEL:basic} 97 | 98 | # Abacate Pay Configuration - using environment variables 99 | abacate: 100 | api: 101 | base-url: ${ABACATE_API_BASE_URL:https://api.abacatepay.com} 102 | token: ${ABACATE_API_TOKEN} 103 | mock-enabled: ${ABACATE_API_MOCK_ENABLED:false} 104 | webhook: 105 | secret: ${ABACATE_WEBHOOK_SECRET} 106 | signature: 107 | enabled: ${ABACATE_WEBHOOK_SIGNATURE_ENABLED:true} 108 | 109 | # Admin Configuration 110 | admin: 111 | username: ${ADMIN_USERNAME:admin} 112 | password: ${ADMIN_PASSWORD} 113 | 114 | # Logging Configuration 115 | logging: 116 | level: 117 | com.kipperdev.orderhub: ${LOG_LEVEL_APP:INFO} 118 | org.springframework.kafka: ${LOG_LEVEL_KAFKA:WARN} 119 | org.springframework.web: ${LOG_LEVEL_WEB:WARN} 120 | feign: ${LOG_LEVEL_FEIGN:WARN} 121 | root: ${LOG_LEVEL_ROOT:WARN} 122 | pattern: 123 | console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" 124 | file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" 125 | file: 126 | name: ${LOG_FILE_NAME:logs/orderhub.log} 127 | max-size: ${LOG_FILE_MAX_SIZE:10MB} 128 | max-history: ${LOG_FILE_MAX_HISTORY:30} 129 | 130 | # Application Configuration 131 | app: 132 | base-url: ${APP_BASE_URL:http://localhost:8080} 133 | order: 134 | status-url-template: "${app.base-url}/public/orders/{orderId}/status" 135 | 136 | # Security Configuration 137 | security: 138 | require-ssl: ${SECURITY_REQUIRE_SSL:false} 139 | cors: 140 | allowed-origins: ${CORS_ALLOWED_ORIGINS:*} 141 | allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS} 142 | allowed-headers: ${CORS_ALLOWED_HEADERS:*} 143 | allow-credentials: ${CORS_ALLOW_CREDENTIALS:true} -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=orderhub 2 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # Mock Profile Configuration 2 | # Everything disabled, only mocks, H2 database 3 | spring: 4 | application: 5 | name: orderhub 6 | 7 | # H2 Database Configuration 8 | datasource: 9 | url: jdbc:h2:mem:orderhub_mock 10 | driver-class-name: org.h2.Driver 11 | username: sa 12 | password: 13 | 14 | h2: 15 | console: 16 | enabled: true 17 | path: /h2-console 18 | 19 | # JPA Configuration 20 | jpa: 21 | hibernate: 22 | ddl-auto: create-drop 23 | show-sql: true 24 | properties: 25 | hibernate: 26 | format_sql: true 27 | 28 | # Kafka Configuration - DISABLED 29 | kafka: 30 | enabled: false 31 | 32 | # Disable Kafka Auto Configuration 33 | autoconfigure: 34 | exclude: 35 | - org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration 36 | 37 | # WebFlux Configuration 38 | webflux: 39 | base-path: / 40 | 41 | # Server Configuration 42 | server: 43 | port: 8080 44 | 45 | # Management/Actuator Configuration 46 | management: 47 | endpoints: 48 | web: 49 | exposure: 50 | include: health,info,metrics 51 | endpoint: 52 | health: 53 | show-details: always 54 | 55 | # Feign Client Configuration 56 | feign: 57 | client: 58 | config: 59 | default: 60 | connect-timeout: 5000 61 | read-timeout: 10000 62 | logger-level: basic 63 | 64 | # Abacate Pay Configuration - MOCK ENABLED 65 | abacate: 66 | api: 67 | base-url: https://api.abacatepay.com 68 | token: mock-token 69 | mock-enabled: true 70 | webhook: 71 | secret: mock-secret 72 | signature: 73 | enabled: false 74 | 75 | # Admin Configuration 76 | admin: 77 | username: admin 78 | password: admin123 79 | 80 | # Logging Configuration 81 | logging: 82 | level: 83 | com.kipperdev.orderhub: DEBUG 84 | org.springframework.kafka: WARN 85 | org.springframework.web: INFO 86 | feign: DEBUG 87 | root: WARN 88 | pattern: 89 | console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" 90 | file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" 91 | 92 | # Application Configuration 93 | app: 94 | base-url: http://localhost:8080 95 | order: 96 | status-url-template: "${app.base-url}/public/orders/{orderId}/status" -------------------------------------------------------------------------------- /src/test/java/com/kipperdev/orderhub/OrderhubApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.kipperdev.orderhub; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class OrderhubApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------