├── .dockerignore ├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── Ecommerce-API.postman_collection.json ├── README.md ├── compose.yaml ├── docker-config ├── grafana │ ├── dashboards │ │ └── dashboard-spring.json │ ├── grafana-dashboards.yml │ └── grafana-datasources.yml ├── prometheus │ └── prometheus.yml └── tempo │ └── tempo.yml ├── k8s-deployment.yaml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── david │ │ └── ecommerceapi │ │ ├── EcommerceApiApplication.java │ │ ├── assistant │ │ ├── application │ │ │ └── AssistantServiceImpl.java │ │ ├── domain │ │ │ ├── AssistantService.java │ │ │ ├── AssistantSuggestion.java │ │ │ └── PromptType.java │ │ └── infraestructure │ │ │ ├── AssistantController.java │ │ │ ├── AssistantControllerImpl.java │ │ │ ├── dto │ │ │ └── ProductSuggestionDTO.java │ │ │ ├── mapper │ │ │ └── SuggestionMapper.java │ │ │ ├── rest │ │ │ └── OpenAiAssistantImpl.java │ │ │ └── utility │ │ │ └── PromptLoader.java │ │ ├── auth │ │ ├── application │ │ │ └── AuthenticationService.java │ │ └── infrastructure │ │ │ ├── AuthenticationController.java │ │ │ ├── AuthenticationRequest.java │ │ │ ├── AuthenticationResponse.java │ │ │ └── RegisterRequest.java │ │ ├── config │ │ ├── application │ │ │ └── JwtService.java │ │ └── infrastructure │ │ │ ├── ApplicationConfig.java │ │ │ ├── AuthenticationConfig.java │ │ │ ├── OpenAPIConfig.java │ │ │ ├── SecurityConfig.java │ │ │ └── filter │ │ │ └── JwtAuthFilter.java │ │ ├── exception │ │ ├── domain │ │ │ ├── BadRequestException.java │ │ │ ├── ErrorMessage.java │ │ │ ├── InvalidTokenException.java │ │ │ ├── MalformedHeaderException.java │ │ │ └── NotFoundException.java │ │ └── infrastructure │ │ │ └── ApiExceptionHandler.java │ │ ├── payment │ │ ├── application │ │ │ ├── PaymentCard.java │ │ │ ├── PaymentPaypal.java │ │ │ └── PaymentService.java │ │ ├── domain │ │ │ └── PaymentInterface.java │ │ └── infrastructure │ │ │ └── PaymentController.java │ │ ├── product │ │ ├── application │ │ │ └── ProductService.java │ │ ├── domain │ │ │ ├── Category.java │ │ │ ├── Product.java │ │ │ └── ProductRepository.java │ │ └── infrastructure │ │ │ ├── ProductController.java │ │ │ ├── ProductControllerImpl.java │ │ │ ├── entity │ │ │ ├── ProductCacheEntity.java │ │ │ └── ProductEntity.java │ │ │ ├── mapper │ │ │ └── ProductMapper.java │ │ │ └── repository │ │ │ ├── CacheProductRepository.java │ │ │ ├── QueryProductRepository.java │ │ │ └── implementation │ │ │ ├── PostgresProductRepositoryImpl.java │ │ │ └── RedisProductRepositoryImpl.java │ │ ├── productShoppingCart │ │ ├── application │ │ │ └── ProductShoppingCartService.java │ │ ├── domain │ │ │ └── ProductShoppingCart.java │ │ └── infrastructure │ │ │ ├── ProductShoppingCartDTO.java │ │ │ ├── ProductShoppingCartDTOMapper.java │ │ │ ├── ProductShoppingCartRepository.java │ │ │ └── ProductShoppingController.java │ │ ├── scheduling │ │ └── ScheduledTasks.java │ │ ├── shoppingCart │ │ ├── application │ │ │ └── ShoppingCartService.java │ │ ├── domain │ │ │ ├── ShoppingCart.java │ │ │ └── ShoppingCartRepository.java │ │ └── infrastructure │ │ │ ├── MySqlShoppingCartRepository.java │ │ │ ├── ShoppingCartController.java │ │ │ └── SpringShoppingCartRepository.java │ │ ├── user │ │ ├── application │ │ │ └── UserServiceImpl.java │ │ ├── domain │ │ │ ├── Role.java │ │ │ ├── User.java │ │ │ ├── UserRepository.java │ │ │ └── UserService.java │ │ └── infrastructure │ │ │ ├── UserController.java │ │ │ ├── UserControllerImpl.java │ │ │ ├── annotation │ │ │ ├── MaskData.java │ │ │ └── ProtectDataSerializer.java │ │ │ ├── dto │ │ │ └── UserDTO.java │ │ │ ├── entity │ │ │ └── UserEntity.java │ │ │ ├── mapper │ │ │ └── UserMapper.java │ │ │ └── repository │ │ │ ├── MySqlUserRepository.java │ │ │ └── SpringUserRepository.java │ │ └── util │ │ └── FileUploadUtil.java └── resources │ ├── application-dev.yml │ ├── application-test.yml │ ├── application.yml │ ├── logback-spring.xml │ └── prompts │ └── productSuggestionPrompt.txt └── test └── java └── com └── david └── ecommerceapi ├── EcommerceApiApplicationTests.java ├── config └── TestConfiguration.java ├── product ├── application │ └── ProductServiceTest.java ├── infrastructure │ ├── ProductControllerMvcTest.java │ ├── ProductControllerRestTest.java │ └── ProductControllerTest.java └── repository │ └── ProductRepositoryTest.java └── user ├── application └── UserServiceImplTest.java └── infrastructure └── UserEntityControllerTest.java /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.DS_Store 8 | **/__pycache__ 9 | **/.venv 10 | **/.classpath 11 | **/.dockerignore 12 | **/.env 13 | **/.git 14 | **/.gitignore 15 | **/.project 16 | **/.settings 17 | **/.toolstarget 18 | **/.vs 19 | **/.vscode 20 | **/*.*proj.user 21 | **/*.dbmdl 22 | **/*.jfm 23 | **/bin 24 | **/charts 25 | **/docker-compose* 26 | **/compose* 27 | **/Dockerfile* 28 | **/node_modules 29 | **/npm-debug.log 30 | **/obj 31 | **/secrets.dev.yaml 32 | **/values.dev.yaml 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | services: 23 | database: 24 | image: mysql:latest 25 | ports: 26 | - 3306:3306 27 | options: >- 28 | --health-cmd "mysqladmin ping" 29 | --health-interval 20s 30 | --health-timeout 10s 31 | --health-retries 5 32 | env: 33 | MYSQL_DATABASE: "spring" 34 | MYSQL_PASSWORD: "password" 35 | MYSQL_ROOT_PASSWORD: "password" 36 | 37 | env: 38 | SPRING_PROFILES_ACTIVE: test 39 | DB_HOST: localhost 40 | DB_PORT: 3306 41 | 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v3 45 | 46 | - name: Set up JDK 17 47 | uses: actions/setup-java@v3 48 | with: 49 | java-version: '17' 50 | distribution: 'temurin' 51 | cache: maven 52 | 53 | - name: Build with Maven 54 | run: mvn clean install 55 | 56 | - name: Run tests 57 | run: mvn test 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### CUSTOM ### 36 | src/main/resources/uploads/** 37 | src/main/resources/application.yml 38 | src/main/resources/application-dev.yml 39 | src/main/resources/application-prod.yml -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-DAM/ecommerce-api/14b741bd6bdce891d705cbcb6390110fa8c5a66f/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.5-openjdk-17 2 | 3 | WORKDIR /ecommerce-api 4 | COPY . . 5 | RUN mvn clean install 6 | 7 | CMD mvn spring-boot:run -------------------------------------------------------------------------------- /Ecommerce-API.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "ae1e2a1f-8dc8-4769-872c-fed9331d841a", 4 | "name": "Ecommerce-API", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "23080126" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Usuarios", 11 | "item": [ 12 | { 13 | "name": "Obtener todos los usuarios", 14 | "event": [ 15 | { 16 | "listen": "test", 17 | "script": { 18 | "exec": [ 19 | "pm.test(\"Estado de respuesta\",function(){\r", 20 | " pm.response.to.be.ok();\r", 21 | "});\r", 22 | "\r", 23 | "pm.test(\"Primer valor es alguno\",function(){\r", 24 | " let response = pm.response.toJSON();\r", 25 | " pm.expect(response[0].firstname).to.eq(\"David\")\r", 26 | "});\r", 27 | "\r", 28 | "pm.test(\"Latencia\",function(){\r", 29 | " pm.expect(pm.response.responseTime).to.be.below(50);\r", 30 | "});" 31 | ], 32 | "type": "text/javascript", 33 | "packages": {} 34 | } 35 | } 36 | ], 37 | "request": { 38 | "auth": { 39 | "type": "bearer", 40 | "bearer": [ 41 | { 42 | "key": "token", 43 | "value": "{{TOKEN}}", 44 | "type": "string" 45 | } 46 | ] 47 | }, 48 | "method": "GET", 49 | "header": [], 50 | "url": { 51 | "raw": "{{URL}}/users", 52 | "host": [ 53 | "{{URL}}" 54 | ], 55 | "path": [ 56 | "users" 57 | ] 58 | } 59 | }, 60 | "response": [] 61 | }, 62 | { 63 | "name": "Crear usuario", 64 | "request": { 65 | "method": "POST", 66 | "header": [], 67 | "body": { 68 | "mode": "raw", 69 | "raw": "{\r\n \"firstname\": \"David\",\r\n \"lastname\":\"Jimenez\",\r\n \"email\":\"david11jv@gmail.com\",\r\n \"password\":\"123456789\"\r\n}", 70 | "options": { 71 | "raw": { 72 | "language": "json" 73 | } 74 | } 75 | }, 76 | "url": { 77 | "raw": "{{URL}}/auth/register", 78 | "host": [ 79 | "{{URL}}" 80 | ], 81 | "path": [ 82 | "auth", 83 | "register" 84 | ] 85 | } 86 | }, 87 | "response": [] 88 | }, 89 | { 90 | "name": "Autenticarse", 91 | "event": [ 92 | { 93 | "listen": "test", 94 | "script": { 95 | "exec": [ 96 | "\r", 97 | "\r", 98 | "pm.test(\"Authentication correctly\", function () {\r", 99 | "\r", 100 | " let body = pm.response.json();\r", 101 | "\r", 102 | " pm.expect(body.token).to.not.be.empty;\r", 103 | "});\r", 104 | "\r", 105 | "if(pm.response.json().token){\r", 106 | "\r", 107 | " pm.collectionVariables.set(\"TOKEN\", pm.response.json().token);\r", 108 | "\r", 109 | "}" 110 | ], 111 | "type": "text/javascript", 112 | "packages": {} 113 | } 114 | } 115 | ], 116 | "request": { 117 | "method": "POST", 118 | "header": [], 119 | "body": { 120 | "mode": "raw", 121 | "raw": "{\r\n \"email\":\"test@gmail.com\",\r\n \"password\":\"123456789\"\r\n}", 122 | "options": { 123 | "raw": { 124 | "language": "json" 125 | } 126 | } 127 | }, 128 | "url": { 129 | "raw": "{{URL}}/auth/authenticate", 130 | "host": [ 131 | "{{URL}}" 132 | ], 133 | "path": [ 134 | "auth", 135 | "authenticate" 136 | ] 137 | } 138 | }, 139 | "response": [] 140 | } 141 | ] 142 | }, 143 | { 144 | "name": "Productos", 145 | "item": [ 146 | { 147 | "name": "Crear producto", 148 | "request": { 149 | "auth": { 150 | "type": "bearer", 151 | "bearer": [ 152 | { 153 | "key": "token", 154 | "value": "{{TOKEN}}", 155 | "type": "string" 156 | } 157 | ] 158 | }, 159 | "method": "POST", 160 | "header": [], 161 | "body": { 162 | "mode": "formdata", 163 | "formdata": [ 164 | { 165 | "key": "name", 166 | "value": "Ordenador ASUS", 167 | "type": "text" 168 | }, 169 | { 170 | "key": "description", 171 | "value": "El más potente de todos", 172 | "type": "text" 173 | }, 174 | { 175 | "key": "category", 176 | "value": "COMPUTER", 177 | "type": "text" 178 | }, 179 | { 180 | "key": "photo", 181 | "type": "file", 182 | "src": "_uT-ks6ws/perfil.png" 183 | } 184 | ] 185 | }, 186 | "url": { 187 | "raw": "{{URL}}/products", 188 | "host": [ 189 | "{{URL}}" 190 | ], 191 | "path": [ 192 | "products" 193 | ] 194 | } 195 | }, 196 | "response": [] 197 | }, 198 | { 199 | "name": "Editar producto", 200 | "request": { 201 | "auth": { 202 | "type": "bearer", 203 | "bearer": [ 204 | { 205 | "key": "token", 206 | "value": "{{TOKEN}}", 207 | "type": "string" 208 | } 209 | ] 210 | }, 211 | "method": "PUT", 212 | "header": [], 213 | "body": { 214 | "mode": "formdata", 215 | "formdata": [ 216 | { 217 | "key": "id", 218 | "value": "3", 219 | "type": "text" 220 | }, 221 | { 222 | "key": "name", 223 | "value": "Ordenador ASUS ROGE X2", 224 | "type": "text" 225 | }, 226 | { 227 | "key": "description", 228 | "value": "El más potente de todos los que hay", 229 | "type": "text" 230 | }, 231 | { 232 | "key": "category", 233 | "value": "COMPUTER", 234 | "type": "text" 235 | }, 236 | { 237 | "key": "photo", 238 | "type": "file", 239 | "src": "aS5UmI5CK/perfil.png", 240 | "disabled": true 241 | } 242 | ] 243 | }, 244 | "url": { 245 | "raw": "{{URL}}/products", 246 | "host": [ 247 | "{{URL}}" 248 | ], 249 | "path": [ 250 | "products" 251 | ] 252 | } 253 | }, 254 | "response": [] 255 | }, 256 | { 257 | "name": "Obtener todos los productos", 258 | "request": { 259 | "auth": { 260 | "type": "bearer", 261 | "bearer": [ 262 | { 263 | "key": "token", 264 | "value": "{{TOKEN}}", 265 | "type": "string" 266 | } 267 | ] 268 | }, 269 | "method": "GET", 270 | "header": [], 271 | "url": { 272 | "raw": "{{URL}}/products", 273 | "host": [ 274 | "{{URL}}" 275 | ], 276 | "path": [ 277 | "products" 278 | ] 279 | } 280 | }, 281 | "response": [] 282 | }, 283 | { 284 | "name": "Obtener un producto", 285 | "request": { 286 | "auth": { 287 | "type": "bearer", 288 | "bearer": [ 289 | { 290 | "key": "token", 291 | "value": "{{TOKEN}}", 292 | "type": "string" 293 | } 294 | ] 295 | }, 296 | "method": "GET", 297 | "header": [], 298 | "url": { 299 | "raw": "{{URL}}/products/:id", 300 | "host": [ 301 | "{{URL}}" 302 | ], 303 | "path": [ 304 | "products", 305 | ":id" 306 | ], 307 | "variable": [ 308 | { 309 | "key": "id", 310 | "value": "1" 311 | } 312 | ] 313 | } 314 | }, 315 | "response": [] 316 | }, 317 | { 318 | "name": "BATCH Importar productos", 319 | "request": { 320 | "method": "GET", 321 | "header": [], 322 | "url": { 323 | "raw": "{{URL}}/products/import", 324 | "host": [ 325 | "{{URL}}" 326 | ], 327 | "path": [ 328 | "products", 329 | "import" 330 | ] 331 | } 332 | }, 333 | "response": [] 334 | } 335 | ] 336 | }, 337 | { 338 | "name": "ProductosCarrito", 339 | "item": [ 340 | { 341 | "name": "Añadir productos carrito", 342 | "request": { 343 | "auth": { 344 | "type": "bearer", 345 | "bearer": [ 346 | { 347 | "key": "token", 348 | "value": "{{TOKEN}}", 349 | "type": "string" 350 | } 351 | ] 352 | }, 353 | "method": "POST", 354 | "header": [], 355 | "body": { 356 | "mode": "raw", 357 | "raw": "\r\n[\r\n {\r\n \"quantity\":1,\r\n \"product_id\":3\r\n }\r\n]", 358 | "options": { 359 | "raw": { 360 | "language": "json" 361 | } 362 | } 363 | }, 364 | "url": { 365 | "raw": "{{URL}}/shopping_cart/1/products", 366 | "host": [ 367 | "{{URL}}" 368 | ], 369 | "path": [ 370 | "shopping_cart", 371 | "1", 372 | "products" 373 | ] 374 | } 375 | }, 376 | "response": [] 377 | }, 378 | { 379 | "name": "Obtener productos carrito", 380 | "protocolProfileBehavior": { 381 | "disableBodyPruning": true 382 | }, 383 | "request": { 384 | "auth": { 385 | "type": "bearer", 386 | "bearer": [ 387 | { 388 | "key": "token", 389 | "value": "{{TOKEN}}", 390 | "type": "string" 391 | } 392 | ] 393 | }, 394 | "method": "GET", 395 | "header": [], 396 | "body": { 397 | "mode": "raw", 398 | "raw": "", 399 | "options": { 400 | "raw": { 401 | "language": "json" 402 | } 403 | } 404 | }, 405 | "url": { 406 | "raw": "{{URL}}/shopping_cart/1/products", 407 | "host": [ 408 | "{{URL}}" 409 | ], 410 | "path": [ 411 | "shopping_cart", 412 | "1", 413 | "products" 414 | ] 415 | } 416 | }, 417 | "response": [] 418 | } 419 | ] 420 | } 421 | ], 422 | "event": [ 423 | { 424 | "listen": "prerequest", 425 | "script": { 426 | "type": "text/javascript", 427 | "exec": [ 428 | "" 429 | ] 430 | } 431 | }, 432 | { 433 | "listen": "test", 434 | "script": { 435 | "type": "text/javascript", 436 | "exec": [ 437 | "" 438 | ] 439 | } 440 | } 441 | ], 442 | "variable": [ 443 | { 444 | "key": "URL", 445 | "value": "http://localhost:8080/api", 446 | "type": "string" 447 | }, 448 | { 449 | "key": "TOKEN", 450 | "value": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXZpZDExanZAZ21haWwuY29tIiwiaWF0IjoxNjc2ODAxMjE0LCJleHAiOjE2NzY4MTIwMTR9.5GXPx80xb9gbPH_keekuJNTJCXxyw_dU4AityT_r4pU", 451 | "type": "string" 452 | } 453 | ] 454 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # E-commerce API 2 | 3 | Este repositorio contiene una API para gestionar un sistema de comercio electrónico. La aplicación está desarrollada en 4 | **Java** utilizando **Spring Boot**, con un enfoque en las buenas prácticas de desarrollo, incluyendo principios de 5 | arquitectura limpia y patrones de diseño. 6 | 7 | ## Características 8 | 9 | - Gestión de usuarios y autenticación. 10 | - Catálogo de productos con soporte para búsqueda y filtrado. 11 | - Carrito de compras y procesamiento de pedidos. 12 | - Integración con bases de datos para persistencia de datos. 13 | - Documentación de la API con **OpenAPI (Swagger)**. 14 | - Trazas, logs y métricas implementadas con el **Grafana Stack** (Grafana, Loki y Prometheus). 15 | - Arquitectura hexagonal para una mejor separación de responsabilidades. 16 | - Autenticación basada en **JWT** (JSON Web Tokens). 17 | - Pipeline de integración continua configurado con **GitHub Actions**. 18 | 19 | ## Tecnologías utilizadas 20 | 21 | - **Java 17** 22 | - **Spring Boot 3** 23 | - **Spring Security** para la autenticación y autorización. 24 | - **JPA/Hibernate** para la interacción con la base de datos. 25 | - **MySQL** como base de datos relacional. 26 | - **OpenAPI** para la documentación de la API. 27 | - **JUnit** y **Mockito** para pruebas unitarias. 28 | - **Docker** para la contenedorización de la aplicación. 29 | - **Grafana Stack** (Grafana, Loki, Prometheus) para monitoreo y observabilidad. 30 | - **GitHub Actions** para integración continua. 31 | 32 | ## Requisitos previos 33 | 34 | Antes de ejecutar el proyecto, asegúrate de tener instalado: 35 | 36 | - **Java 17** o superior. 37 | - **Maven** para la gestión de dependencias. 38 | - **Docker** (opcional, para despliegue con contenedores). 39 | - **MySQL** (si no usas Docker para la base de datos). 40 | 41 | ## Instalación y configuración 42 | 43 | 1. Clona este repositorio: 44 | 45 | ```bash 46 | git clone https://github.com/David-DAM/ecommerce-api.git 47 | cd ecommerce-api 48 | ``` 49 | 50 | 2. Configura las variables de entorno en el archivo `application.yml` o `application.properties` para conectar la base 51 | de datos: 52 | 53 | ```yaml 54 | spring: 55 | datasource: 56 | url: jdbc:mysql://localhost:3306/spring 57 | username: root 58 | password: password 59 | jpa: 60 | hibernate: 61 | ddl-auto: update 62 | ``` 63 | 64 | 3. Configura el stack de observabilidad (Grafana, Loki, Prometheus) utilizando Docker Compose o la herramienta de tu 65 | preferencia. Asegúrate de enlazar los servicios correctamente. 66 | 67 | 4. Compila el proyecto con Maven: 68 | 69 | ```bash 70 | mvn clean install 71 | ``` 72 | 73 | 5. Ejecuta la aplicación: 74 | 75 | ```bash 76 | mvn spring-boot:run 77 | ``` 78 | 79 | 6. Accede a la documentación de la API 80 | en [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html). 81 | 82 | ## Uso 83 | 84 | ### Endpoints principales 85 | 86 | - **Usuarios**: 87 | - Registro de usuarios: `POST /api/v1/users/register` 88 | - Login: `POST /api/v1/users/login` 89 | 90 | - **Productos**: 91 | - Obtener lista de productos: `GET /api/v1/products` 92 | - Detalle de un producto: `GET /api/v1/products/{id}` 93 | 94 | - **Carrito de compras**: 95 | - Agregar un producto: `POST /api/v1/cart` 96 | - Ver el carrito: `GET /api/v1/cart` 97 | 98 | - **Pedidos**: 99 | - Crear un pedido: `POST /api/v1/orders` 100 | - Obtener pedidos: `GET /api/v1/orders` 101 | 102 | ## Pruebas 103 | 104 | Para ejecutar las pruebas unitarias: 105 | 106 | ```bash 107 | mvn test 108 | ``` 109 | 110 | ## Despliegue con Docker 111 | 112 | 1. Construye la imagen de Docker: 113 | 114 | ```bash 115 | docker build -t ecommerce-api . 116 | ``` 117 | 118 | 2. Ejecuta el contenedor: 119 | 120 | ```bash 121 | docker run -p 8080:8080 ecommerce-api 122 | ``` 123 | 124 | 3. Asegúrate de tener un contenedor o instancia de MySQL corriendo con la base de datos configurada. 125 | 126 | 4. Despliega el stack de observabilidad con Grafana, Loki y Prometheus para habilitar monitoreo y trazabilidad. 127 | 128 | ## Licencia 129 | 130 | Este proyecto está bajo la licencia MIT. Consulta el archivo [LICENSE](LICENSE) para más información. 131 | 132 | --- 133 | 134 | **Desarrollado por [David-DAM](https://github.com/David-DAM).** 135 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | # ecommerce-api: 5 | # build: ./ 6 | # ports: 7 | # - "8080:8080" 8 | # healthcheck: 9 | # test: [ "CMD", "curl", "-f", "http://localhost:8080/actuator/health" ] 10 | # interval: 30s 11 | # timeout: 10s 12 | # retries: 5 13 | # depends_on: 14 | # db: 15 | # condition: service_healthy 16 | 17 | prometheus: 18 | container_name: prometheus 19 | image: prom/prometheus 20 | command: 21 | - --config.file=/etc/prometheus/prometheus.yml 22 | volumes: 23 | - ./docker-config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 24 | ports: 25 | - "9090:9090" 26 | healthcheck: 27 | test: [ "CMD", "curl", "-f", "http://localhost:9090/-/healthy" ] 28 | interval: 10s 29 | timeout: 10s 30 | retries: 5 31 | 32 | loki: 33 | image: grafana/loki:latest 34 | command: -config.file=/etc/loki/local-config.yaml 35 | ports: 36 | - "3100:3100" 37 | healthcheck: 38 | test: [ "CMD", "curl", "-f", "http://localhost:3100/ready" ] 39 | interval: 10s 40 | timeout: 10s 41 | retries: 5 42 | 43 | tempo: 44 | image: grafana/tempo:latest 45 | command: [ "-config.file=/etc/tempo.yml" ] 46 | volumes: 47 | - ./docker-config/tempo/tempo.yml:/etc/tempo.yml 48 | ports: 49 | - "3200:3200" # tempo 50 | - "9411:9411" # zipkin 51 | healthcheck: 52 | test: [ "CMD", "curl", "-f", "http://localhost:3200/metrics" ] 53 | interval: 10s 54 | timeout: 10s 55 | retries: 5 56 | 57 | grafana: 58 | container_name: grafana 59 | image: grafana/grafana 60 | volumes: 61 | - ./docker-config/grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml 62 | - ./docker-config/grafana/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yml 63 | - ./docker-config/grafana/dashboards:/etc/grafana/provisioning/dashboards 64 | ports: 65 | - "3000:3000" 66 | healthcheck: 67 | test: [ "CMD", "curl", "-f", "http://localhost:3000/healthz" ] 68 | interval: 10s 69 | timeout: 10s 70 | retries: 5 71 | 72 | redis: 73 | image: redis:latest 74 | container_name: redis 75 | ports: 76 | - "6379:6379" 77 | volumes: 78 | - redis_data:/data 79 | command: [ "redis-server", "--save", "''", "--appendonly", "no" ] 80 | healthcheck: 81 | test: [ "CMD", "redis-cli", "ping" ] 82 | interval: 10s 83 | timeout: 10s 84 | retries: 5 85 | 86 | postgres: 87 | image: pgvector/pgvector:pg17 88 | container_name: postgres_pgvector 89 | environment: 90 | POSTGRES_USER: root 91 | POSTGRES_PASSWORD: password 92 | POSTGRES_DB: ecommerce 93 | ports: 94 | - "5432:5432" 95 | volumes: 96 | - postgres_data:/var/lib/postgresql/data 97 | healthcheck: 98 | test: [ "CMD-SHELL", "pg_isready -U root -d ecommerce" ] 99 | interval: 10s 100 | timeout: 10s 101 | retries: 3 102 | 103 | volumes: 104 | redis_data: 105 | postgres_data: 106 | -------------------------------------------------------------------------------- /docker-config/grafana/grafana-dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'default' 4 | orgId: 1 5 | folder: '' 6 | type: file 7 | disableDeletion: false 8 | updateIntervalSeconds: 10 9 | options: 10 | path: /etc/grafana/provisioning/dashboards 11 | -------------------------------------------------------------------------------- /docker-config/grafana/grafana-datasources.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | uid: prometheus 7 | access: proxy 8 | orgId: 1 9 | url: http://prometheus:9090 10 | basicAuth: false 11 | isDefault: false 12 | version: 1 13 | editable: false 14 | jsonData: 15 | httpMethod: POST 16 | exemplarTraceIdDestinations: 17 | - name: trace_id 18 | datasourceUid: tempo 19 | - name: Loki 20 | type: loki 21 | uid: loki 22 | access: proxy 23 | orgId: 1 24 | url: http://loki:3100 25 | basicAuth: false 26 | isDefault: false 27 | version: 1 28 | editable: false 29 | jsonData: 30 | derivedFields: 31 | - datasourceUid: tempo 32 | matcherRegex: \[.+,(.+?), 33 | name: TraceID 34 | url: $${__value.raw} 35 | - name: Tempo 36 | type: tempo 37 | access: proxy 38 | orgId: 1 39 | url: http://tempo:3200 40 | basicAuth: false 41 | isDefault: true 42 | version: 1 43 | editable: false 44 | apiVersion: 1 45 | uid: tempo 46 | jsonData: 47 | httpMethod: GET 48 | tracesToLogs: 49 | datasourceUid: 'loki' 50 | nodeGraph: 51 | enabled: true 52 | tracesToLogsV2: 53 | datasourceUid: loki 54 | spanStartTimeShift: '-1h' 55 | spanEndTimeShift: '1h' 56 | filterByTraceID: true 57 | filterBySpanID: true 58 | tags: [ { key: 'service.name', value: 'job' } ] -------------------------------------------------------------------------------- /docker-config/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: 'ecommerce-service' 7 | metrics_path: '/actuator/prometheus' 8 | scrape_interval: 5s 9 | static_configs: 10 | - targets: [ 'host.docker.internal:8080' ] 11 | labels: 12 | application: 'Ecommerce Service Application' -------------------------------------------------------------------------------- /docker-config/tempo/tempo.yml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 3200 3 | 4 | distributor: 5 | receivers: 6 | zipkin: 7 | 8 | storage: 9 | trace: 10 | backend: local 11 | local: 12 | path: /tmp/tempo/blocks -------------------------------------------------------------------------------- /k8s-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mysql 5 | labels: 6 | app: mysql 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: mysql 12 | template: 13 | metadata: 14 | labels: 15 | app: mysql 16 | spec: 17 | containers: 18 | - name: mysql 19 | image: mysql:8.0 20 | env: 21 | - name: MYSQL_ROOT_PASSWORD 22 | value: "password" 23 | - name: MYSQL_DATABASE 24 | value: "spring" 25 | - name: MYSQL_PASSWORD 26 | value: "password" 27 | ports: 28 | - containerPort: 3306 29 | volumeMounts: 30 | - name: mysql-persistent-storage 31 | mountPath: /var/lib/mysql 32 | volumes: 33 | - name: mysql-persistent-storage 34 | persistentVolumeClaim: 35 | claimName: mysql-pv-claim 36 | 37 | --- 38 | apiVersion: v1 39 | kind: Service 40 | metadata: 41 | name: mysql 42 | spec: 43 | ports: 44 | - port: 3306 45 | targetPort: 3306 46 | nodePort: 32000 47 | selector: 48 | app: mysql 49 | type: NodePort 50 | 51 | --- 52 | apiVersion: v1 53 | kind: PersistentVolume 54 | metadata: 55 | name: mysql-pv 56 | spec: 57 | capacity: 58 | storage: 2Gi 59 | accessModes: 60 | - ReadWriteOnce 61 | hostPath: 62 | path: "/mnt/data" 63 | 64 | --- 65 | apiVersion: v1 66 | kind: PersistentVolumeClaim 67 | metadata: 68 | name: mysql-pv-claim 69 | spec: 70 | accessModes: 71 | - ReadWriteOnce 72 | resources: 73 | requests: 74 | storage: 2Gi 75 | 76 | -------------------------------------------------------------------------------- /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 | # https://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 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.2.1 9 | 10 | 11 | com.example 12 | ecommerce-api 13 | 0.0.1-SNAPSHOT 14 | ecommerce-api 15 | Ecommcerce API 16 | 17 | 17 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jpa 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-security 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-web 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-devtools 38 | runtime 39 | true 40 | 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | true 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | test 52 | 53 | 54 | 55 | org.springframework.security 56 | spring-security-test 57 | test 58 | 59 | 60 | 61 | org.springdoc 62 | springdoc-openapi-starter-webmvc-ui 63 | 2.0.2 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-validation 69 | 70 | 71 | 72 | io.jsonwebtoken 73 | jjwt-api 74 | 0.11.5 75 | 76 | 77 | 78 | io.jsonwebtoken 79 | jjwt-impl 80 | 0.11.5 81 | 82 | 83 | 84 | io.jsonwebtoken 85 | jjwt-jackson 86 | 0.11.5 87 | 88 | 89 | 90 | org.mapstruct 91 | mapstruct 92 | 1.6.2 93 | 94 | 95 | 96 | org.mapstruct 97 | mapstruct-processor 98 | 1.6.2 99 | provided 100 | 101 | 102 | 103 | io.micrometer 104 | micrometer-tracing-bridge-brave 105 | 106 | 107 | 108 | io.zipkin.reporter2 109 | zipkin-reporter-brave 110 | 111 | 112 | 113 | io.micrometer 114 | micrometer-registry-prometheus 115 | runtime 116 | 117 | 118 | 119 | com.github.loki4j 120 | loki-logback-appender 121 | 1.5.2 122 | 123 | 124 | 125 | org.springframework.boot 126 | spring-boot-starter-actuator 127 | 128 | 129 | 130 | org.springframework.boot 131 | spring-boot-starter-data-redis 132 | 133 | 134 | 135 | org.springframework.ai 136 | spring-ai-openai-spring-boot-starter 137 | 138 | 139 | 140 | org.springframework.ai 141 | spring-ai-pgvector-store-spring-boot-starter 142 | 1.0.0-M6 143 | 144 | 145 | 146 | org.postgresql 147 | postgresql 148 | runtime 149 | 150 | 151 | 152 | 153 | 154 | 155 | org.springframework.ai 156 | spring-ai-bom 157 | 1.0.0-M6 158 | pom 159 | import 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | org.springframework.boot 169 | spring-boot-maven-plugin 170 | 171 | 172 | 173 | org.projectlombok 174 | lombok 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/EcommerceApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class EcommerceApiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(EcommerceApiApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/application/AssistantServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.application; 2 | 3 | import com.david.ecommerceapi.assistant.domain.AssistantService; 4 | import com.david.ecommerceapi.assistant.domain.AssistantSuggestion; 5 | import com.david.ecommerceapi.product.domain.Product; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Optional; 10 | 11 | @Service 12 | @RequiredArgsConstructor 13 | public class AssistantServiceImpl implements AssistantService { 14 | 15 | private final AssistantSuggestion assistantSuggestion; 16 | 17 | public Optional getProductSuggestion(String message) { 18 | return assistantSuggestion.getProductSuggestion(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/domain/AssistantService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.domain; 2 | 3 | import com.david.ecommerceapi.product.domain.Product; 4 | 5 | import java.util.Optional; 6 | 7 | public interface AssistantService { 8 | 9 | Optional getProductSuggestion(String message); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/domain/AssistantSuggestion.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.domain; 2 | 3 | import com.david.ecommerceapi.product.domain.Product; 4 | 5 | import java.util.Optional; 6 | 7 | public interface AssistantSuggestion { 8 | 9 | Optional getProductSuggestion(String message); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/domain/PromptType.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum PromptType { 9 | 10 | PRODUCT_SUGGESTION("productSuggestionPrompt.txt"); 11 | 12 | private final String value; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/infraestructure/AssistantController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.infraestructure; 2 | 3 | import com.david.ecommerceapi.assistant.infraestructure.dto.ProductSuggestionDTO; 4 | import org.springframework.http.ResponseEntity; 5 | 6 | public interface AssistantController { 7 | 8 | ResponseEntity getProductSuggestions(String message); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/infraestructure/AssistantControllerImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.infraestructure; 2 | 3 | import com.david.ecommerceapi.assistant.domain.AssistantService; 4 | import com.david.ecommerceapi.assistant.infraestructure.dto.ProductSuggestionDTO; 5 | import com.david.ecommerceapi.assistant.infraestructure.mapper.SuggestionMapper; 6 | import com.david.ecommerceapi.product.domain.Product; 7 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.Optional; 16 | 17 | @RestController 18 | @RequestMapping("api/assistant") 19 | @SecurityRequirement(name = "Bearer Authentication") 20 | @RequiredArgsConstructor 21 | public class AssistantControllerImpl implements AssistantController { 22 | 23 | private final AssistantService assistantService; 24 | private final SuggestionMapper suggestionMapper; 25 | 26 | @GetMapping("/products") 27 | public ResponseEntity getProductSuggestions(@RequestParam String message) { 28 | Optional productSuggestion = assistantService.getProductSuggestion(message); 29 | 30 | if (productSuggestion.isEmpty()) { 31 | return ResponseEntity.notFound().build(); 32 | } 33 | 34 | ProductSuggestionDTO productSuggestionDTO = suggestionMapper.productToProductSuggestionDTO(productSuggestion.get()); 35 | 36 | return ResponseEntity.ok(productSuggestionDTO); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/infraestructure/dto/ProductSuggestionDTO.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.infraestructure.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @JsonInclude(JsonInclude.Include.NON_NULL) 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class ProductSuggestionDTO { 14 | 15 | @JsonProperty(required = true) 16 | private String id; 17 | @JsonProperty(required = true) 18 | private String name; 19 | @JsonProperty(required = true) 20 | private String description; 21 | @JsonProperty(required = true) 22 | private double price; 23 | @JsonProperty(required = true) 24 | private String imageUrl; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/infraestructure/mapper/SuggestionMapper.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.infraestructure.mapper; 2 | 3 | import com.david.ecommerceapi.assistant.infraestructure.dto.ProductSuggestionDTO; 4 | import com.david.ecommerceapi.product.domain.Product; 5 | import org.mapstruct.Mapper; 6 | import org.mapstruct.MappingConstants; 7 | 8 | @Mapper(componentModel = MappingConstants.ComponentModel.SPRING) 9 | public interface SuggestionMapper { 10 | 11 | ProductSuggestionDTO productToProductSuggestionDTO(Product product); 12 | 13 | Product productSuggestionDTOToProduct(ProductSuggestionDTO productSuggestionDTO); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/infraestructure/rest/OpenAiAssistantImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.infraestructure.rest; 2 | 3 | import com.david.ecommerceapi.assistant.domain.AssistantSuggestion; 4 | import com.david.ecommerceapi.assistant.domain.PromptType; 5 | import com.david.ecommerceapi.assistant.infraestructure.dto.ProductSuggestionDTO; 6 | import com.david.ecommerceapi.assistant.infraestructure.mapper.SuggestionMapper; 7 | import com.david.ecommerceapi.assistant.infraestructure.utility.PromptLoader; 8 | import com.david.ecommerceapi.product.domain.Product; 9 | import com.david.ecommerceapi.product.infrastructure.repository.implementation.PostgresProductRepositoryImpl; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.ai.chat.messages.Message; 13 | import org.springframework.ai.chat.model.ChatResponse; 14 | import org.springframework.ai.chat.prompt.Prompt; 15 | import org.springframework.ai.chat.prompt.PromptTemplate; 16 | import org.springframework.ai.converter.BeanOutputConverter; 17 | import org.springframework.ai.document.Document; 18 | import org.springframework.ai.openai.OpenAiChatModel; 19 | import org.springframework.ai.openai.OpenAiChatOptions; 20 | import org.springframework.ai.openai.api.ResponseFormat; 21 | import org.springframework.ai.vectorstore.SearchRequest; 22 | import org.springframework.ai.vectorstore.VectorStore; 23 | import org.springframework.core.io.Resource; 24 | import org.springframework.stereotype.Component; 25 | 26 | import java.util.List; 27 | import java.util.Optional; 28 | 29 | @Slf4j 30 | @RequiredArgsConstructor 31 | @Component 32 | public class OpenAiAssistantImpl implements AssistantSuggestion { 33 | 34 | private final OpenAiChatModel openAiChatModel; 35 | private final PromptLoader promptLoader; 36 | private final SuggestionMapper suggestionMapper; 37 | private final VectorStore vectorStore; 38 | private final PostgresProductRepositoryImpl productRepository; 39 | 40 | @Override 41 | public Optional getProductSuggestion(String request) { 42 | 43 | SearchRequest searchRequest = SearchRequest.builder() 44 | .query(request) 45 | .topK(3) 46 | .build(); 47 | 48 | List documents = vectorStore.similaritySearch(searchRequest); 49 | 50 | if (documents == null || documents.isEmpty()) { 51 | return Optional.empty(); 52 | } 53 | 54 | List productIds = documents.stream().map(document -> Long.valueOf(document.getMetadata().get("productId").toString())).toList(); 55 | 56 | List products = productRepository.findAllByIdIn(productIds); 57 | 58 | if (products.isEmpty()) { 59 | return Optional.empty(); 60 | } 61 | 62 | Resource promptResource = promptLoader.getPromptResource(PromptType.PRODUCT_SUGGESTION); 63 | 64 | PromptTemplate promptTemplate = new PromptTemplate(promptResource); 65 | promptTemplate.add("request", request); 66 | promptTemplate.add("products", products); 67 | 68 | Message message = promptTemplate.createMessage(); 69 | 70 | BeanOutputConverter outputConverter = new BeanOutputConverter<>(ProductSuggestionDTO.class); 71 | 72 | String jsonSchema = outputConverter.getJsonSchema(); 73 | 74 | Prompt prompt = new Prompt(message, OpenAiChatOptions.builder() 75 | .responseFormat(new ResponseFormat(ResponseFormat.Type.JSON_SCHEMA, jsonSchema)) 76 | .build() 77 | ); 78 | 79 | ChatResponse response = openAiChatModel.call(prompt); 80 | 81 | String content = response.getResult().getOutput().getText(); 82 | 83 | ProductSuggestionDTO productSuggestionDTO = outputConverter.convert(content); 84 | 85 | return Optional.ofNullable(suggestionMapper.productSuggestionDTOToProduct(productSuggestionDTO)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/assistant/infraestructure/utility/PromptLoader.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.assistant.infraestructure.utility; 2 | 3 | import com.david.ecommerceapi.assistant.domain.PromptType; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.core.io.Resource; 6 | import org.springframework.core.io.ResourceLoader; 7 | import org.springframework.stereotype.Component; 8 | 9 | @RequiredArgsConstructor 10 | @Component 11 | public class PromptLoader { 12 | 13 | private final ResourceLoader resourceLoader; 14 | 15 | public Resource getPromptResource(PromptType promptType) { 16 | 17 | return switch (promptType) { 18 | case PRODUCT_SUGGESTION -> resourceLoader.getResource("classpath:prompts/" + promptType.getValue()); 19 | default -> throw new IllegalStateException("Unexpected value: " + promptType); 20 | }; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/auth/application/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.auth.application; 2 | 3 | import com.david.ecommerceapi.auth.infrastructure.AuthenticationRequest; 4 | import com.david.ecommerceapi.auth.infrastructure.AuthenticationResponse; 5 | import com.david.ecommerceapi.config.application.JwtService; 6 | import com.david.ecommerceapi.exception.domain.NotFoundException; 7 | import com.david.ecommerceapi.user.domain.Role; 8 | import com.david.ecommerceapi.user.domain.User; 9 | import com.david.ecommerceapi.user.domain.UserRepository; 10 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 11 | import com.david.ecommerceapi.user.infrastructure.mapper.UserMapper; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.security.authentication.AuthenticationManager; 14 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import org.springframework.stereotype.Service; 17 | 18 | @Service 19 | @RequiredArgsConstructor 20 | public class AuthenticationService { 21 | 22 | private final UserRepository userRepository; 23 | private final PasswordEncoder passwordEncoder; 24 | private final JwtService jwtService; 25 | private final AuthenticationManager authenticationManager; 26 | private final UserMapper userMapper; 27 | 28 | public AuthenticationResponse register(User user) { 29 | 30 | if (userRepository.findByEmail(user.getEmail()).isPresent()) throw new NotFoundException("error"); 31 | 32 | user.setPassword(passwordEncoder.encode(user.getPassword())); 33 | user.setRole(Role.USER); 34 | 35 | User saved = userRepository.save(user); 36 | UserEntity userEntity = userMapper.userToUserEntity(saved); 37 | 38 | String jwtToken = jwtService.generateToken(userEntity); 39 | 40 | return AuthenticationResponse.builder() 41 | .token(jwtToken) 42 | .build(); 43 | } 44 | 45 | public AuthenticationResponse authenticate(AuthenticationRequest request) { 46 | 47 | authenticationManager.authenticate( 48 | new UsernamePasswordAuthenticationToken( 49 | request.getEmail(), 50 | request.getPassword() 51 | ) 52 | ); 53 | 54 | User user = userRepository.findByEmail(request.getEmail()) 55 | .orElseThrow(() -> new NotFoundException("User not found")); 56 | 57 | UserEntity userEntity = userMapper.userToUserEntity(user); 58 | 59 | String jwtToken = jwtService.generateToken(userEntity); 60 | 61 | return AuthenticationResponse.builder() 62 | .token(jwtToken) 63 | .build(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/auth/infrastructure/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.auth.infrastructure; 2 | 3 | import com.david.ecommerceapi.auth.application.AuthenticationService; 4 | import com.david.ecommerceapi.user.domain.User; 5 | import com.david.ecommerceapi.user.infrastructure.mapper.UserMapper; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RestController 14 | @RequestMapping("/api/auth") 15 | @RequiredArgsConstructor 16 | public class AuthenticationController { 17 | 18 | private final AuthenticationService service; 19 | private final UserMapper userMapper; 20 | 21 | @PostMapping("/register") 22 | public ResponseEntity register(@RequestBody RegisterRequest request) {//@Validated 23 | 24 | User user = userMapper.registerRequestToUser(request); 25 | 26 | return ResponseEntity.ok(service.register(user)); 27 | } 28 | 29 | @PostMapping("/authenticate") 30 | public ResponseEntity authenticate(@RequestBody AuthenticationRequest request) { 31 | 32 | return ResponseEntity.ok(service.authenticate(request)); 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/auth/infrastructure/AuthenticationRequest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.auth.infrastructure; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotNull; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import org.hibernate.validator.constraints.Length; 10 | 11 | @Data 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class AuthenticationRequest { 16 | @NotNull(message = "The email is mandatory") 17 | @Email(message = "The given email does not match the pattern") 18 | private String email; 19 | @NotNull(message = "The password is mandatory") 20 | @Length(min = 5, message = "The password should be at least of 5 characters of length") 21 | private String password; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/auth/infrastructure/AuthenticationResponse.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.auth.infrastructure; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @Builder 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class AuthenticationResponse { 13 | 14 | private String token; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/auth/infrastructure/RegisterRequest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.auth.infrastructure; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import org.hibernate.validator.constraints.Length; 11 | import org.hibernate.validator.constraints.UniqueElements; 12 | 13 | @Data 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class RegisterRequest { 18 | @NotNull(message = "The firstname is mandatory") 19 | @NotBlank(message = "The firstname is mandatory") 20 | private String firstname; 21 | @NotNull(message = "The lastname is mandatory") 22 | @NotBlank(message = "The lastname is mandatory") 23 | private String lastname; 24 | @NotNull(message = "The email is mandatory") 25 | @NotBlank(message = "The email is mandatory") 26 | @Email(message = "The given email does not match the pattern") 27 | @UniqueElements() 28 | private String email; 29 | @NotNull(message = "The password is mandatory") 30 | @NotBlank(message = "The password is mandatory") 31 | @Length(min = 5, message = "The password should be at least of 5 characters of length") 32 | private String password; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/config/application/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config.application; 2 | 3 | import io.jsonwebtoken.*; 4 | import io.jsonwebtoken.io.Decoders; 5 | import io.jsonwebtoken.security.Keys; 6 | import io.jsonwebtoken.security.SignatureException; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.security.Key; 11 | import java.util.Date; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.function.Function; 15 | 16 | @Service 17 | public class JwtService { 18 | 19 | private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970"; 20 | private static final long TOKEN_EXPIRATION = 1000 * 60 * 60 * 24; 21 | private static final long REFRESH_WINDOW = 1000 * 60 * 60 * 24 * 7; 22 | 23 | public String extractUsername(String token) { 24 | return extractClaim(token, Claims::getSubject); 25 | } 26 | 27 | public T extractClaim(String token, Function claimsResolver) { 28 | final Claims claims = extractAllClaims(token); 29 | return claimsResolver.apply(claims); 30 | } 31 | 32 | public String generateToken(UserDetails userDetails) { 33 | return generateToken(new HashMap<>(), userDetails); 34 | } 35 | 36 | public String generateToken( 37 | Map extraClaims, 38 | UserDetails userDetails 39 | ) { 40 | return Jwts 41 | .builder() 42 | .setClaims(extraClaims) 43 | .setSubject(userDetails.getUsername()) 44 | .setIssuedAt(new Date(System.currentTimeMillis())) 45 | .setExpiration(new Date(System.currentTimeMillis() + TOKEN_EXPIRATION)) 46 | .signWith(getSignInKey(), SignatureAlgorithm.HS256) 47 | .compact(); 48 | } 49 | 50 | public boolean isTokenValid(String token, UserDetails userDetails) { 51 | final String username = extractUsername(token); 52 | return (username.equals(userDetails.getUsername())); 53 | } 54 | 55 | public boolean isTokenExpired(String token) { 56 | return extractExpiration(token).before(new Date()); 57 | } 58 | 59 | private Date extractExpiration(String token) { 60 | return extractClaim(token, Claims::getExpiration); 61 | } 62 | 63 | private Claims extractAllClaims(String token) { 64 | try { 65 | return Jwts 66 | .parserBuilder() 67 | .setSigningKey(getSignInKey()) 68 | .build() 69 | .parseClaimsJws(token) 70 | .getBody(); 71 | 72 | } catch (ExpiredJwtException e) { 73 | return e.getClaims(); 74 | } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) { 75 | throw new RuntimeException("Invalid JWT token or mal formed", e); 76 | } 77 | } 78 | 79 | private Key getSignInKey() { 80 | byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); 81 | return Keys.hmacShaKeyFor(keyBytes); 82 | } 83 | 84 | public boolean canTokenBeRenewed(String token) { 85 | try { 86 | Claims claims = extractAllClaims(token); 87 | Date expiration = claims.getExpiration(); 88 | long currentTime = System.currentTimeMillis(); 89 | return expiration.before(new Date(currentTime)) && 90 | expiration.getTime() + REFRESH_WINDOW > currentTime; 91 | } catch (Exception e) { 92 | return false; 93 | } 94 | } 95 | 96 | public String renewToken(String token, UserDetails userDetails) { 97 | if (!canTokenBeRenewed(token)) { 98 | throw new IllegalArgumentException("The JWT couldn't be renewed"); 99 | } 100 | return generateToken(userDetails); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/config/infrastructure/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config.infrastructure; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; 5 | 6 | @Configuration 7 | @EnableRedisRepositories 8 | //@EnableScheduling 9 | //@EnableTransactionManagement 10 | public class ApplicationConfig { 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/config/infrastructure/AuthenticationConfig.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config.infrastructure; 2 | 3 | import com.david.ecommerceapi.user.infrastructure.repository.SpringUserRepository; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.authentication.AuthenticationProvider; 9 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 10 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 14 | import org.springframework.security.crypto.password.PasswordEncoder; 15 | 16 | @Configuration 17 | @RequiredArgsConstructor 18 | public class AuthenticationConfig { 19 | 20 | private final SpringUserRepository userRepository; 21 | 22 | @Bean 23 | public UserDetailsService userDetailsService() { 24 | return username -> userRepository.findByEmail(username) 25 | .orElseThrow(() -> new UsernameNotFoundException("User not found")); 26 | } 27 | 28 | @Bean 29 | public AuthenticationProvider authenticationProvider() { 30 | DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 31 | authProvider.setUserDetailsService(userDetailsService()); 32 | authProvider.setPasswordEncoder(passwordEncoder()); 33 | return authProvider; 34 | } 35 | 36 | @Bean 37 | public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { 38 | return config.getAuthenticationManager(); 39 | } 40 | 41 | @Bean 42 | public PasswordEncoder passwordEncoder() { 43 | return new BCryptPasswordEncoder(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/config/infrastructure/OpenAPIConfig.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config.infrastructure; 2 | 3 | import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; 4 | import io.swagger.v3.oas.annotations.security.SecurityScheme; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | //@OpenAPIDefinition( 9 | // info =@Info( 10 | // title = "User API", 11 | // version = "${api.version}", 12 | // contact = @Contact( 13 | // name = "Baeldung", email = "user-apis@baeldung.com", url = "https://www.baeldung.com" 14 | // ), 15 | // license = @License( 16 | // name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0" 17 | // ), 18 | // termsOfService = "${tos.uri}", 19 | // description = "${api.description}" 20 | // ), 21 | // servers = @Server( 22 | // url = "${api.server.url}", 23 | // description = "Production" 24 | // ) 25 | //) 26 | @SecurityScheme( 27 | name = "Bearer Authentication", 28 | type = SecuritySchemeType.HTTP, 29 | bearerFormat = "JWT", 30 | scheme = "bearer" 31 | ) 32 | public class OpenAPIConfig { 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/config/infrastructure/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config.infrastructure; 2 | 3 | import com.david.ecommerceapi.config.infrastructure.filter.JwtAuthFilter; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.authentication.AuthenticationProvider; 9 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 13 | import org.springframework.security.config.http.SessionCreationPolicy; 14 | import org.springframework.security.web.SecurityFilterChain; 15 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 16 | import org.springframework.web.servlet.HandlerExceptionResolver; 17 | 18 | 19 | @Configuration 20 | @EnableWebSecurity 21 | @EnableMethodSecurity 22 | public class SecurityConfig { 23 | @Autowired 24 | private AuthenticationProvider authenticationProvider; 25 | @Autowired 26 | @Qualifier("handlerExceptionResolver") 27 | private HandlerExceptionResolver handlerExceptionResolver; 28 | 29 | @Bean 30 | public JwtAuthFilter jwtAuthFilter() { 31 | return new JwtAuthFilter(handlerExceptionResolver); 32 | } 33 | 34 | @Bean 35 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 36 | return http 37 | .csrf(AbstractHttpConfigurer::disable) 38 | .authorizeHttpRequests(auth -> auth 39 | .requestMatchers( 40 | "/api/auth/**", 41 | "/v3/api-docs/**", 42 | "/swagger-ui/**", 43 | "/swagger-ui.html", 44 | "/proxy/**", 45 | "/actuator/**" 46 | ).permitAll() 47 | //.hasAuthority("ADMIN") 48 | .anyRequest().authenticated() 49 | ) 50 | .sessionManagement(session -> 51 | session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) 52 | ) 53 | .authenticationProvider(authenticationProvider) 54 | .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) 55 | .build(); 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/config/infrastructure/filter/JwtAuthFilter.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config.infrastructure.filter; 2 | 3 | import com.david.ecommerceapi.config.application.JwtService; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.lang.NonNull; 11 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.core.userdetails.UserDetailsService; 15 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | import org.springframework.web.servlet.HandlerExceptionResolver; 18 | 19 | import java.io.IOException; 20 | 21 | @Slf4j 22 | public class JwtAuthFilter extends OncePerRequestFilter { 23 | @Autowired 24 | private JwtService jwtService; 25 | @Autowired 26 | private UserDetailsService userDetailsService; 27 | private HandlerExceptionResolver handlerExceptionResolver; 28 | 29 | @Autowired 30 | public JwtAuthFilter(HandlerExceptionResolver handlerExceptionResolver) { 31 | this.handlerExceptionResolver = handlerExceptionResolver; 32 | } 33 | 34 | @Override 35 | protected void doFilterInternal( 36 | @NonNull HttpServletRequest request, 37 | @NonNull HttpServletResponse response, 38 | @NonNull FilterChain filterChain 39 | ) throws ServletException, IOException { 40 | 41 | final String authHeader = request.getHeader("Authorization"); 42 | final String jwt; 43 | final String userEmail; 44 | 45 | if (authHeader == null || !authHeader.startsWith("Bearer ")) { 46 | filterChain.doFilter(request, response); 47 | return; 48 | } 49 | 50 | try { 51 | 52 | jwt = authHeader.substring(7); 53 | 54 | userEmail = jwtService.extractUsername(jwt); 55 | 56 | if (userEmail == null || SecurityContextHolder.getContext().getAuthentication() != null) { 57 | log.debug("The JWT doesn't contains a username"); 58 | filterChain.doFilter(request, response); 59 | return; 60 | } 61 | 62 | UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); 63 | 64 | boolean isTokenValid = jwtService.isTokenValid(jwt, userDetails); 65 | boolean isTokenExpired = jwtService.isTokenExpired(jwt); 66 | boolean canBeRenewed = jwtService.canTokenBeRenewed(jwt); 67 | 68 | if (!isTokenValid || (isTokenExpired && !canBeRenewed)) { 69 | log.debug("The JWT is not valid"); 70 | SecurityContextHolder.clearContext(); 71 | filterChain.doFilter(request, response); 72 | return; 73 | } 74 | 75 | if (isTokenExpired) { 76 | log.debug("The JWT is expired and is going to be renewed"); 77 | String newToken = jwtService.renewToken(jwt, userDetails); 78 | response.setHeader("Authorization", "Bearer " + newToken); 79 | } 80 | 81 | UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( 82 | userDetails, 83 | null, 84 | userDetails.getAuthorities() 85 | ); 86 | 87 | authToken.setDetails( 88 | new WebAuthenticationDetailsSource().buildDetails(request) 89 | ); 90 | 91 | SecurityContextHolder.getContext().setAuthentication(authToken); 92 | 93 | } catch (Exception e) { 94 | log.error("Error processing JWT: {}", e.getMessage()); 95 | handlerExceptionResolver.resolveException(request, response, null, e); 96 | } 97 | 98 | filterChain.doFilter(request, response); 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/exception/domain/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.exception.domain; 2 | 3 | public class BadRequestException extends RuntimeException{ 4 | 5 | private static final String DESCRIPTION = "Bad Request Exception (400)"; 6 | 7 | public BadRequestException(String detail){ 8 | super(DESCRIPTION + ". " + detail); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/exception/domain/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.exception.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.util.Map; 7 | 8 | @Data 9 | public class ErrorMessage { 10 | private String exception; 11 | private String message; 12 | private String path; 13 | //private Map errors; 14 | 15 | public ErrorMessage(Exception exception, String path){ 16 | this.exception = exception.getClass().getSimpleName(); 17 | this.message = exception.getMessage(); 18 | this.path = path; 19 | } 20 | 21 | public ErrorMessage(Exception exception, String path, Map errors){ 22 | this.exception = exception.getClass().getSimpleName(); 23 | this.message = exception.getMessage(); 24 | //this.errors = errors; 25 | this.path = path; 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/exception/domain/InvalidTokenException.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.exception.domain; 2 | 3 | public class InvalidTokenException extends BadRequestException{ 4 | 5 | private static final String DESCRIPTION = "Token expired"; 6 | 7 | public InvalidTokenException(String detail){ 8 | super(DESCRIPTION + ". " + detail); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/exception/domain/MalformedHeaderException.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.exception.domain; 2 | 3 | public class MalformedHeaderException extends BadRequestException{ 4 | 5 | private static final String DESCRIPTION = "Token with wrong format"; 6 | 7 | public MalformedHeaderException(String detail){ 8 | super(DESCRIPTION + ". " + detail); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/exception/domain/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.exception.domain; 2 | 3 | public class NotFoundException extends RuntimeException{ 4 | 5 | private static final String DESCRIPTION = "Not found Exception "; 6 | 7 | public NotFoundException(String detail){ 8 | super(DESCRIPTION + ". " + detail); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/exception/infrastructure/ApiExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.exception.infrastructure; 2 | 3 | import com.david.ecommerceapi.exception.domain.BadRequestException; 4 | import com.david.ecommerceapi.exception.domain.ErrorMessage; 5 | import com.david.ecommerceapi.exception.domain.InvalidTokenException; 6 | import com.david.ecommerceapi.exception.domain.NotFoundException; 7 | import io.jsonwebtoken.ExpiredJwtException; 8 | import io.jsonwebtoken.MalformedJwtException; 9 | import jakarta.persistence.EntityNotFoundException; 10 | import jakarta.servlet.http.HttpServletRequest; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.converter.HttpMessageNotReadableException; 13 | import org.springframework.web.HttpRequestMethodNotSupportedException; 14 | import org.springframework.web.bind.MethodArgumentNotValidException; 15 | import org.springframework.web.bind.MissingRequestHeaderException; 16 | import org.springframework.web.bind.MissingServletRequestParameterException; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | import org.springframework.web.bind.annotation.ResponseBody; 19 | import org.springframework.web.bind.annotation.ResponseStatus; 20 | import org.springframework.web.bind.annotation.RestControllerAdvice; 21 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 22 | 23 | import java.security.SignatureException; 24 | 25 | 26 | @RestControllerAdvice 27 | public class ApiExceptionHandler { 28 | @ResponseStatus(HttpStatus.NOT_FOUND) 29 | @ExceptionHandler({ 30 | NotFoundException.class, 31 | TypeNotPresentException.class, 32 | EntityNotFoundException.class, 33 | }) 34 | @ResponseBody 35 | public ErrorMessage notFound(HttpServletRequest request, Exception exception) { 36 | return new ErrorMessage(exception, request.getRequestURI()); 37 | } 38 | 39 | @ResponseStatus(HttpStatus.BAD_REQUEST) 40 | @ExceptionHandler({ 41 | BadRequestException.class, 42 | InvalidTokenException.class, 43 | HttpRequestMethodNotSupportedException.class, 44 | MethodArgumentNotValidException.class, 45 | MissingRequestHeaderException.class, 46 | MissingServletRequestParameterException.class, 47 | MethodArgumentTypeMismatchException.class, 48 | HttpMessageNotReadableException.class, 49 | //MethodArgumentNotValidException.class 50 | }) 51 | @ResponseBody 52 | public ErrorMessage badRequest(HttpServletRequest request, Exception exception) {//, MethodArgumentNotValidException ex 53 | //Map errors = new HashMap<>(); 54 | // ex.getBindingResult().getFieldErrors().forEach((error) -> { 55 | // errors.put(error.getField(), error.getDefaultMessage()); 56 | // }); 57 | 58 | //Handle business exceptions in other methods for the specific cases 59 | return new ErrorMessage(exception, request.getRequestURI());//, errors 60 | } 61 | 62 | @ResponseStatus(HttpStatus.UNAUTHORIZED) 63 | @ExceptionHandler({ 64 | ExpiredJwtException.class, 65 | MalformedJwtException.class, 66 | SignatureException.class 67 | }) 68 | @ResponseBody 69 | public ErrorMessage unauthorized(HttpServletRequest request, Exception exception) { 70 | return new ErrorMessage(exception, request.getRequestURI()); 71 | } 72 | 73 | // public Map businnesException(BusinessException e){ 74 | // Map errors= new HashMap<>(); 75 | // errors.put("message","exeption"); 76 | // 77 | // return errors; 78 | // } 79 | 80 | // @ResponseStatus(HttpStatus.FORBIDDEN) 81 | // @ExceptionHandler({ForbidenException.class}) 82 | // @ResponseBody 83 | // public ErrorMessage forbiddenRequest(HttpServletRequest request, Exception exception){ 84 | // return new ErrorMessage(exception, request.getRequestURI()); 85 | // } 86 | 87 | // @ResponseStatus(HttpStatus.CONFLICT) 88 | // @ExceptionHandler({ConflictException.class}) 89 | // @ResponseBody 90 | // public ErrorMessage conflict(HttpServletRequest request, Exception exception){ 91 | // return new ErrorMessage(exception, request.getRequestURI()); 92 | // } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/payment/application/PaymentCard.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.payment.application; 2 | 3 | import com.david.ecommerceapi.payment.domain.PaymentInterface; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Date; 8 | 9 | @Qualifier("card") 10 | @Component 11 | public class PaymentCard implements PaymentInterface { 12 | private String number; 13 | private Date expirationDate; 14 | private String ccv; 15 | 16 | @Override 17 | public String makePayment() { 18 | return "Credit card"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/payment/application/PaymentPaypal.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.payment.application; 2 | 3 | import com.david.ecommerceapi.payment.domain.PaymentInterface; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Qualifier("paypal") 8 | @Component 9 | public class PaymentPaypal implements PaymentInterface { 10 | 11 | private String username; 12 | private String password; 13 | 14 | @Override 15 | public String makePayment() { 16 | 17 | return "Paypal"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/payment/application/PaymentService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.payment.application; 2 | 3 | import com.david.ecommerceapi.payment.domain.PaymentInterface; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.stream.Collectors; 11 | 12 | @Service 13 | public class PaymentService { 14 | 15 | private final Map paymentInterfaces; 16 | 17 | public PaymentService(List paymentInterfaceList) { 18 | 19 | paymentInterfaces = paymentInterfaceList.stream() 20 | .collect(Collectors.toMap(x -> x.getClass().getAnnotation(Qualifier.class).value(), 21 | Function.identity()) 22 | ); 23 | } 24 | 25 | 26 | public String makePayment(String paymentType) { 27 | 28 | return paymentInterfaces.get(paymentType).makePayment(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/payment/domain/PaymentInterface.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.payment.domain; 2 | 3 | public interface PaymentInterface { 4 | 5 | public String makePayment(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/payment/infrastructure/PaymentController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.payment.infrastructure; 2 | 3 | import com.david.ecommerceapi.payment.application.PaymentService; 4 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @AllArgsConstructor 13 | @SecurityRequirement(name = "Bearer Authentication") 14 | @RequestMapping("/api/payments") 15 | public class PaymentController { 16 | 17 | private final PaymentService paymentService; 18 | 19 | @GetMapping("/pay/{paymentType}") 20 | public String pay(@PathVariable String paymentType) { 21 | 22 | return paymentService.makePayment(paymentType); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/application/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.application; 2 | 3 | import com.david.ecommerceapi.exception.domain.NotFoundException; 4 | import com.david.ecommerceapi.product.domain.Product; 5 | import com.david.ecommerceapi.product.infrastructure.repository.implementation.PostgresProductRepositoryImpl; 6 | import com.david.ecommerceapi.product.infrastructure.repository.implementation.RedisProductRepositoryImpl; 7 | import com.david.ecommerceapi.util.FileUploadUtil; 8 | import lombok.AllArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.ai.document.Document; 11 | import org.springframework.ai.vectorstore.VectorStore; 12 | import org.springframework.beans.BeanUtils; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | import org.springframework.util.StringUtils; 16 | import org.springframework.web.multipart.MultipartFile; 17 | 18 | import java.io.IOException; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Optional; 22 | import java.util.UUID; 23 | 24 | @Slf4j 25 | @Service 26 | @AllArgsConstructor 27 | @Transactional 28 | public class ProductService { 29 | 30 | private final PostgresProductRepositoryImpl postgresProductRepository; 31 | private final RedisProductRepositoryImpl redisProductRepository; 32 | private final FileUploadUtil fileUploadUtil; 33 | private final VectorStore vectorStore; 34 | 35 | public Product save(Product product, MultipartFile file) throws IOException { 36 | 37 | if (file != null && !file.isEmpty()) { 38 | 39 | String fileName = StringUtils.cleanPath(file.getOriginalFilename()); 40 | 41 | String fileCode = fileUploadUtil.saveFile(fileName, file); 42 | product.setImage(fileCode + "-" + file.getOriginalFilename()); 43 | } 44 | 45 | Product saved = postgresProductRepository.save(product); 46 | redisProductRepository.save(saved); 47 | saveVectorProduct(saved); 48 | 49 | return saved; 50 | } 51 | 52 | private void saveVectorProduct(Product product) { 53 | Document document = Document.builder() 54 | .id(UUID.randomUUID().toString()) 55 | .text(product.getDescription()) 56 | .metadata(Map.of( 57 | "productId", product.getId().toString(), 58 | "name", product.getName(), 59 | "category", product.getCategory().name(), 60 | "price", product.getPrice() 61 | )) 62 | .build(); 63 | vectorStore.add(List.of(document)); 64 | } 65 | 66 | public List findAll() {//Integer page, Integer pageSize, String sortBy 67 | 68 | // Pageable paging = PageRequest.of(page, pageSize, Sort.by(sortBy)); 69 | 70 | // Page pagedResult = productRepository.findAll(paging); 71 | // 72 | // if(pagedResult.hasContent()) { 73 | // return pagedResult.getContent(); 74 | // } else { 75 | // return new ArrayList(); 76 | // } 77 | List products = redisProductRepository.findAll(); 78 | 79 | if (!products.isEmpty()) { 80 | log.info("Found {} products in redis", products.size()); 81 | return products; 82 | } 83 | 84 | products = postgresProductRepository.findAll(); 85 | 86 | if (!products.isEmpty()) { 87 | redisProductRepository.saveAll(products); 88 | } 89 | log.info("Found {} products in database", products.size()); 90 | 91 | return products; 92 | } 93 | 94 | public Product findById(Long id) { 95 | Optional optionalProduct = redisProductRepository.findById(id); 96 | 97 | if (optionalProduct.isPresent()) { 98 | log.info("Found product in redis with id {}", id); 99 | return optionalProduct.get(); 100 | } 101 | 102 | Product product = postgresProductRepository.findById(id).orElseThrow(() -> new NotFoundException("Product with Id:" + id + " not found")); 103 | 104 | redisProductRepository.save(product); 105 | log.info("Found product in database with id {}", id); 106 | 107 | return product; 108 | } 109 | 110 | public Product update(Product product, Optional multipartFile) throws IOException { 111 | 112 | Product productDb = postgresProductRepository.findById(product.getId()).orElseThrow(() -> new NotFoundException("El producto no fue encontrado")); 113 | 114 | BeanUtils.copyProperties(product, productDb, "image"); 115 | 116 | if (multipartFile.isPresent() && multipartFile.get().getOriginalFilename() != null) { 117 | 118 | String fileName = StringUtils.cleanPath(multipartFile.get().getOriginalFilename()); 119 | 120 | String filecode = fileUploadUtil.saveFile(fileName, multipartFile.get()); 121 | 122 | productDb.setImage(filecode + "-" + multipartFile.get().getOriginalFilename()); 123 | 124 | } 125 | 126 | postgresProductRepository.save(productDb); 127 | redisProductRepository.save(productDb); 128 | 129 | return productDb; 130 | } 131 | 132 | public Product delete(Long id) { 133 | Product product = postgresProductRepository.findById(id) 134 | .orElseThrow(() -> new NotFoundException("El producto no fue encontrado")); 135 | 136 | postgresProductRepository.deleteById(product.getId()); 137 | redisProductRepository.deleteById(product.getId()); 138 | vectorStore.delete(List.of(product.getId().toString())); 139 | 140 | return product; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/domain/Category.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.domain; 2 | 3 | public enum Category { 4 | COMPUTER, 5 | PHONE, 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/domain/Product.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.domain; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @Builder 11 | public class Product { 12 | 13 | private Long id; 14 | 15 | private String name; 16 | 17 | private String description; 18 | 19 | private double price; 20 | 21 | private String image; 22 | 23 | private Category category; 24 | 25 | private List productShoppingCarts; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/domain/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface ProductRepository { 7 | 8 | Product save(Product product); 9 | 10 | Optional findById(Long id); 11 | 12 | List findAll(); 13 | 14 | List findAllByIdIn(List ids); 15 | 16 | void deleteById(Long id); 17 | 18 | void saveAll(List products); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure; 2 | 3 | import com.david.ecommerceapi.product.domain.Product; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import java.io.IOException; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public interface ProductController { 12 | 13 | ResponseEntity save(Product product, MultipartFile photo) throws IOException; 14 | 15 | ResponseEntity> findAll(); 16 | 17 | ResponseEntity findById(Long id); 18 | 19 | ResponseEntity deletedById(Long id); 20 | 21 | ResponseEntity update(Product product, Optional photo) throws IOException; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/ProductControllerImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure; 2 | 3 | import com.david.ecommerceapi.product.application.ProductService; 4 | import com.david.ecommerceapi.product.domain.Product; 5 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import java.io.IOException; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | @RestController 16 | @AllArgsConstructor 17 | @SecurityRequirement(name = "Bearer Authentication") 18 | @RequestMapping("/api/products") 19 | public class ProductControllerImpl implements ProductController { 20 | 21 | private final ProductService productService; 22 | 23 | @PostMapping() 24 | public ResponseEntity save(@ModelAttribute Product product, @RequestParam(required = false) MultipartFile photo) throws IOException { 25 | 26 | return ResponseEntity.ok(productService.save(product, photo)); 27 | } 28 | 29 | @GetMapping() 30 | public ResponseEntity> findAll( 31 | // @RequestParam(defaultValue = "0") Integer page, 32 | // @RequestParam(defaultValue = "10") Integer pageSize, 33 | // @RequestParam(defaultValue = "id") String sortBy 34 | ) { 35 | 36 | return ResponseEntity.ok(productService.findAll());//page, pageSize, sortBy 37 | } 38 | 39 | @GetMapping("/{id}") 40 | public ResponseEntity findById(@PathVariable Long id) { 41 | return ResponseEntity.ok(productService.findById(id)); 42 | } 43 | 44 | @DeleteMapping("/{id}") 45 | public ResponseEntity deletedById(@PathVariable Long id) { 46 | return ResponseEntity.ok(productService.delete(id)); 47 | } 48 | 49 | @PutMapping() 50 | public ResponseEntity update(@ModelAttribute Product product, @RequestParam Optional photo) throws IOException { 51 | return ResponseEntity.ok(productService.update(product, photo)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/entity/ProductCacheEntity.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.entity; 2 | 3 | import com.david.ecommerceapi.product.domain.Category; 4 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 5 | import jakarta.persistence.Id; 6 | import lombok.Data; 7 | import org.springframework.data.redis.core.RedisHash; 8 | 9 | import java.io.Serializable; 10 | import java.util.List; 11 | 12 | @RedisHash("Product") 13 | @Data 14 | public class ProductCacheEntity implements Serializable { 15 | 16 | @Id 17 | private Long id; 18 | 19 | private String name; 20 | 21 | private String description; 22 | 23 | private double price; 24 | 25 | private String image; 26 | 27 | private Category category; 28 | 29 | private List productShoppingCarts; 30 | } 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/entity/ProductEntity.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.entity; 2 | 3 | import com.david.ecommerceapi.product.domain.Category; 4 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 5 | import com.fasterxml.jackson.annotation.JsonManagedReference; 6 | import jakarta.persistence.*; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.util.List; 13 | 14 | @Data 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | @Builder 18 | @Entity 19 | @Table(name = "products") 20 | public class ProductEntity { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | private String name; 27 | private String description; 28 | private double price; 29 | private String image; 30 | @Enumerated(EnumType.STRING) 31 | private Category category; 32 | @JsonManagedReference 33 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "product") 34 | private List productShoppingCarts; 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/mapper/ProductMapper.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.mapper; 2 | 3 | import com.david.ecommerceapi.product.domain.Product; 4 | import com.david.ecommerceapi.product.infrastructure.entity.ProductCacheEntity; 5 | import com.david.ecommerceapi.product.infrastructure.entity.ProductEntity; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.MappingConstants; 8 | 9 | @Mapper(componentModel = MappingConstants.ComponentModel.SPRING) 10 | public interface ProductMapper { 11 | 12 | ProductEntity productToProductEntity(Product product); 13 | 14 | Product productEntityToProduct(ProductEntity product); 15 | 16 | ProductCacheEntity productToProductCacheEntity(Product product); 17 | 18 | Product productCacheEntityToProduct(ProductCacheEntity product); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/repository/CacheProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.repository; 2 | 3 | import com.david.ecommerceapi.product.infrastructure.entity.ProductCacheEntity; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CacheProductRepository extends CrudRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/repository/QueryProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.repository; 2 | 3 | import com.david.ecommerceapi.product.infrastructure.entity.ProductEntity; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface QueryProductRepository extends JpaRepository { 9 | //Page findAll(Pageable pageable); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/repository/implementation/PostgresProductRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.repository.implementation; 2 | 3 | import com.david.ecommerceapi.product.domain.Product; 4 | import com.david.ecommerceapi.product.domain.ProductRepository; 5 | import com.david.ecommerceapi.product.infrastructure.entity.ProductEntity; 6 | import com.david.ecommerceapi.product.infrastructure.mapper.ProductMapper; 7 | import com.david.ecommerceapi.product.infrastructure.repository.QueryProductRepository; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | @Repository 15 | @RequiredArgsConstructor 16 | public class PostgresProductRepositoryImpl implements ProductRepository { 17 | 18 | private final QueryProductRepository queryProductRepository; 19 | private final ProductMapper productMapper; 20 | 21 | @Override 22 | public Product save(Product product) { 23 | ProductEntity saved = queryProductRepository.save(productMapper.productToProductEntity(product)); 24 | return productMapper.productEntityToProduct(saved); 25 | } 26 | 27 | @Override 28 | public Optional findById(Long id) { 29 | return queryProductRepository.findById(id).map(productMapper::productEntityToProduct); 30 | } 31 | 32 | @Override 33 | public List findAll() { 34 | return queryProductRepository.findAll().stream().map(productMapper::productEntityToProduct).toList(); 35 | } 36 | 37 | @Override 38 | public List findAllByIdIn(List ids) { 39 | return queryProductRepository.findAllById(ids).stream().map(productMapper::productEntityToProduct).toList(); 40 | } 41 | 42 | @Override 43 | public void deleteById(Long id) { 44 | queryProductRepository.deleteById(id); 45 | } 46 | 47 | @Override 48 | public void saveAll(List products) { 49 | queryProductRepository.saveAll(products.stream().map(productMapper::productToProductEntity).toList()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/product/infrastructure/repository/implementation/RedisProductRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure.repository.implementation; 2 | 3 | import com.david.ecommerceapi.product.domain.Product; 4 | import com.david.ecommerceapi.product.domain.ProductRepository; 5 | import com.david.ecommerceapi.product.infrastructure.entity.ProductCacheEntity; 6 | import com.david.ecommerceapi.product.infrastructure.mapper.ProductMapper; 7 | import com.david.ecommerceapi.product.infrastructure.repository.CacheProductRepository; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.stream.StreamSupport; 14 | 15 | @RequiredArgsConstructor 16 | @Repository 17 | public class RedisProductRepositoryImpl implements ProductRepository { 18 | 19 | private final CacheProductRepository cacheProductRepository; 20 | private final ProductMapper productMapper; 21 | 22 | @Override 23 | public Product save(Product product) { 24 | ProductCacheEntity saved = cacheProductRepository.save(productMapper.productToProductCacheEntity(product)); 25 | return productMapper.productCacheEntityToProduct(saved); 26 | } 27 | 28 | @Override 29 | public Optional findById(Long id) { 30 | return cacheProductRepository.findById(id).map(productMapper::productCacheEntityToProduct); 31 | } 32 | 33 | @Override 34 | public List findAll() { 35 | Iterable all = cacheProductRepository.findAll(); 36 | return StreamSupport.stream(all.spliterator(), false).map(productMapper::productCacheEntityToProduct).toList(); 37 | } 38 | 39 | @Override 40 | public List findAllByIdIn(List ids) { 41 | Iterable all = cacheProductRepository.findAllById(ids); 42 | return StreamSupport.stream(all.spliterator(), false).map(productMapper::productCacheEntityToProduct).toList(); 43 | } 44 | 45 | @Override 46 | public void deleteById(Long id) { 47 | cacheProductRepository.deleteById(id); 48 | } 49 | 50 | @Override 51 | public void saveAll(List products) { 52 | cacheProductRepository.saveAll(products.stream().map(productMapper::productToProductCacheEntity).toList()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/productShoppingCart/application/ProductShoppingCartService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.productShoppingCart.application; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.infrastructure.ProductShoppingCartRepository; 4 | import com.david.ecommerceapi.shoppingCart.infrastructure.SpringShoppingCartRepository; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | @AllArgsConstructor 10 | public class ProductShoppingCartService { 11 | 12 | private final ProductShoppingCartRepository productShoppingCartRepository; 13 | private final SpringShoppingCartRepository shoppingCartRepository; 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/productShoppingCart/domain/ProductShoppingCart.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.productShoppingCart.domain; 2 | 3 | import com.david.ecommerceapi.product.infrastructure.entity.ProductEntity; 4 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 5 | import com.fasterxml.jackson.annotation.JsonBackReference; 6 | import jakarta.persistence.*; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Entity 13 | @Table(name = "products_shopping_cart") 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | @Data 17 | @Builder 18 | public class ProductShoppingCart { 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Long id; 22 | private Integer quantity; 23 | @JsonBackReference 24 | @ManyToOne(fetch = FetchType.LAZY) 25 | @JoinColumn() 26 | private ProductEntity product; 27 | @JsonBackReference 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | @JoinColumn() 30 | private ShoppingCart shoppingCart; 31 | 32 | public static class ProductShoppingCartBuilder { 33 | public ProductShoppingCartBuilder() { 34 | } 35 | 36 | // Lombok will fill in the fields and methods 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/productShoppingCart/infrastructure/ProductShoppingCartDTO.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.productShoppingCart.infrastructure; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @Builder 12 | public class ProductShoppingCartDTO { 13 | 14 | private Long product_id; 15 | private Integer quantity; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/productShoppingCart/infrastructure/ProductShoppingCartDTOMapper.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.productShoppingCart.infrastructure; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.function.Function; 7 | 8 | @Service 9 | public class ProductShoppingCartDTOMapper implements Function { 10 | @Override 11 | public ProductShoppingCartDTO apply(ProductShoppingCart productShoppingCart) { 12 | return new ProductShoppingCartDTO.ProductShoppingCartDTOBuilder() 13 | .product_id(productShoppingCart.getProduct().getId()) 14 | .quantity(productShoppingCart.getQuantity()) 15 | .build(); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/productShoppingCart/infrastructure/ProductShoppingCartRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.productShoppingCart.infrastructure; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface ProductShoppingCartRepository extends JpaRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/productShoppingCart/infrastructure/ProductShoppingController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.productShoppingCart.infrastructure; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.application.ProductShoppingCartService; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | @AllArgsConstructor 10 | @RequestMapping("api/products/shoppingCart") 11 | public class ProductShoppingController { 12 | 13 | private final ProductShoppingCartService productShoppingCartService; 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/scheduling/ScheduledTasks.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.scheduling; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @RequiredArgsConstructor 10 | @Slf4j 11 | public class ScheduledTasks { 12 | 13 | @Scheduled(fixedRate = 30000) 14 | public void sendReportOrders() { 15 | 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/shoppingCart/application/ShoppingCartService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.shoppingCart.application; 2 | 3 | import com.david.ecommerceapi.exception.domain.BadRequestException; 4 | import com.david.ecommerceapi.exception.domain.NotFoundException; 5 | import com.david.ecommerceapi.product.infrastructure.repository.QueryProductRepository; 6 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 7 | import com.david.ecommerceapi.productShoppingCart.infrastructure.ProductShoppingCartDTO; 8 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 9 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCartRepository; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | 15 | @Service 16 | @AllArgsConstructor 17 | public class ShoppingCartService { 18 | 19 | private final ShoppingCartRepository shoppingCartRepository; 20 | private final QueryProductRepository productRepository; 21 | 22 | public List findAllProductsByShoppingCart(Long id) { 23 | 24 | ShoppingCart shoppingCart = shoppingCartRepository.findById(id).orElseThrow(() -> new NotFoundException("Shopping cart not found")); 25 | //TODO add .stream().collect(Collectors.groupingBy(w -> w.stud_location)); for repeated objects 26 | return shoppingCart.getProducts(); 27 | } 28 | 29 | //TODO add transactional 30 | public ShoppingCart saveProducts(List productShoppingCartDTOList, Long id) { 31 | ShoppingCart shoppingCart = shoppingCartRepository.findById(id).orElseThrow(() -> new NotFoundException("Shopping cart not found")); 32 | 33 | List productShoppingCartListFromDTO = getProductShoppingCartListFromDTO(productShoppingCartDTOList, shoppingCart); 34 | 35 | isValidShoppingCartList(productShoppingCartListFromDTO); 36 | 37 | List productShoppingCartList = shoppingCart.getProducts(); 38 | 39 | productShoppingCartList.addAll(productShoppingCartListFromDTO); 40 | 41 | shoppingCart.setTotal( 42 | shoppingCart.getTotal() + productShoppingCartList 43 | .stream() 44 | .mapToDouble(x -> x.getProduct().getPrice() * x.getQuantity()) 45 | .sum() 46 | ); 47 | 48 | return shoppingCartRepository.save(shoppingCart); 49 | } 50 | 51 | public ShoppingCart editProducts(List productShoppingCartDTOList, Long id) { 52 | ShoppingCart shoppingCart = shoppingCartRepository.findById(id).orElseThrow(() -> new NotFoundException("Shopping cart not found")); 53 | 54 | List productShoppingCartListFromDTO = getProductShoppingCartListFromDTO(productShoppingCartDTOList, shoppingCart); 55 | 56 | List productShoppingCartList = shoppingCart.getProducts(); 57 | 58 | productShoppingCartList.clear(); 59 | 60 | productShoppingCartList.addAll(productShoppingCartListFromDTO); 61 | 62 | shoppingCart.setTotal( 63 | productShoppingCartList 64 | .stream() 65 | .mapToDouble(x -> x.getProduct().getPrice() * x.getQuantity()) 66 | .sum() 67 | ); 68 | 69 | return shoppingCartRepository.save(shoppingCart); 70 | } 71 | 72 | public ShoppingCart deleteAllProducts(Long id) { 73 | ShoppingCart shoppingCart = shoppingCartRepository.findById(id).orElseThrow(() -> new NotFoundException("Shopping cart not found")); 74 | 75 | List productShoppingCartList = shoppingCart.getProducts(); 76 | 77 | productShoppingCartList.clear(); 78 | 79 | shoppingCart.setTotal(0); 80 | 81 | return shoppingCartRepository.save(shoppingCart); 82 | } 83 | 84 | public ShoppingCart deleteProduct(ProductShoppingCart productShoppingCart, Long id) { 85 | ShoppingCart shoppingCart = shoppingCartRepository.findById(id).orElseThrow(() -> new NotFoundException("Shopping cart not found")); 86 | 87 | List productShoppingCartList = shoppingCart.getProducts(); 88 | 89 | productShoppingCartList.remove(productShoppingCart); 90 | 91 | shoppingCart.setTotal( 92 | productShoppingCartList 93 | .stream() 94 | .mapToDouble(x -> x.getProduct().getPrice() * x.getQuantity()) 95 | .sum() 96 | ); 97 | 98 | return shoppingCartRepository.save(shoppingCart); 99 | } 100 | 101 | public List getProductShoppingCartListFromDTO(List productShoppingCartDTOList, ShoppingCart shoppingCart) { 102 | 103 | return productShoppingCartDTOList 104 | .stream() 105 | .map( 106 | x -> new ProductShoppingCart.ProductShoppingCartBuilder() 107 | .quantity(x.getQuantity()) 108 | .product( 109 | productRepository.findById(x.getProduct_id()).orElseThrow(() -> new NotFoundException("Shopping cart not found")) 110 | ) 111 | .shoppingCart(shoppingCart).build() 112 | ) 113 | .toList(); 114 | } 115 | 116 | //TODO replace with better solution for each errror message 117 | public void isValidShoppingCartList(List shoppingCartList) { 118 | shoppingCartList.stream() 119 | .filter(x -> x.getQuantity() > 0) 120 | .filter(x -> x.getProduct() != null) 121 | .findAny() 122 | .ifPresent(a -> { 123 | throw new BadRequestException("Product list no valid"); 124 | }); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/shoppingCart/domain/ShoppingCart.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.shoppingCart.domain; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 4 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 5 | import com.fasterxml.jackson.annotation.JsonBackReference; 6 | import com.fasterxml.jackson.annotation.JsonManagedReference; 7 | import jakarta.persistence.*; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | 13 | import java.util.List; 14 | 15 | @Entity 16 | @Table(name = "shopping_carts") 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | @Builder 21 | public class ShoppingCart { 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | private double total; 26 | private boolean payed; 27 | @JsonBackReference 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | @JoinColumn() 30 | private UserEntity user; 31 | @JsonManagedReference 32 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "shoppingCart") 33 | private List products; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/shoppingCart/domain/ShoppingCartRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.shoppingCart.domain; 2 | 3 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public interface ShoppingCartRepository { 8 | 9 | Optional findByUserAndPayed(UserEntity userEntity, boolean payed); 10 | 11 | ShoppingCart save(ShoppingCart shoppingCart); 12 | 13 | Optional findById(Long id); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/shoppingCart/infrastructure/MySqlShoppingCartRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.shoppingCart.infrastructure; 2 | 3 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 4 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCartRepository; 5 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.Optional; 10 | 11 | @Repository 12 | @RequiredArgsConstructor 13 | public class MySqlShoppingCartRepository implements ShoppingCartRepository { 14 | 15 | private final SpringShoppingCartRepository springShoppingCartRepository; 16 | 17 | @Override 18 | public Optional findByUserAndPayed(UserEntity userEntity, boolean payed) { 19 | return springShoppingCartRepository.findByUserAndPayed(userEntity, payed); 20 | } 21 | 22 | @Override 23 | public ShoppingCart save(ShoppingCart shoppingCart) { 24 | return springShoppingCartRepository.save(shoppingCart); 25 | } 26 | 27 | @Override 28 | public Optional findById(Long id) { 29 | return springShoppingCartRepository.findById(id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/shoppingCart/infrastructure/ShoppingCartController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.shoppingCart.infrastructure; 2 | 3 | import com.david.ecommerceapi.productShoppingCart.domain.ProductShoppingCart; 4 | import com.david.ecommerceapi.productShoppingCart.infrastructure.ProductShoppingCartDTO; 5 | import com.david.ecommerceapi.shoppingCart.application.ShoppingCartService; 6 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 7 | import lombok.AllArgsConstructor; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | @RestController 14 | @AllArgsConstructor 15 | @RequestMapping("api/shopping_cart") 16 | public class ShoppingCartController { 17 | 18 | private final ShoppingCartService shoppingCartService; 19 | 20 | @GetMapping("/{id}/products") 21 | public ResponseEntity> findAllProductsByShoppingCart(@PathVariable Long id) { 22 | return ResponseEntity.ok(shoppingCartService.findAllProductsByShoppingCart(id)); 23 | } 24 | 25 | @PostMapping("/{id}/products") 26 | public ResponseEntity saveProducts(@RequestBody List productShoppingCartList, @PathVariable Long id) { 27 | return ResponseEntity.ok(this.shoppingCartService.saveProducts(productShoppingCartList, id)); 28 | } 29 | 30 | @PutMapping("/{id}/products") 31 | public ResponseEntity editProducts(@RequestBody List productShoppingCartList, @PathVariable Long id) { 32 | return ResponseEntity.ok(this.shoppingCartService.editProducts(productShoppingCartList, id)); 33 | } 34 | 35 | @DeleteMapping("/{id}/products") 36 | public ResponseEntity deleteAllProducts(@RequestBody List productShoppingCartList, @PathVariable Long id) { 37 | return ResponseEntity.ok(this.shoppingCartService.deleteAllProducts(id)); 38 | } 39 | 40 | //TODO add controllers for specific product of shopping cart 41 | @DeleteMapping("/{id}/products/{idProduct}") 42 | public ResponseEntity editProducts(@RequestBody ProductShoppingCart productShoppingCart, @PathVariable Long id, @PathVariable Long idProduct) { 43 | return ResponseEntity.ok(this.shoppingCartService.deleteProduct(productShoppingCart, id)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/shoppingCart/infrastructure/SpringShoppingCartRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.shoppingCart.infrastructure; 2 | 3 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 4 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface SpringShoppingCartRepository extends JpaRepository { 12 | Optional findByUserAndPayed(UserEntity userEntity, boolean payed); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/application/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.application; 2 | 3 | import com.david.ecommerceapi.exception.domain.NotFoundException; 4 | import com.david.ecommerceapi.user.domain.User; 5 | import com.david.ecommerceapi.user.domain.UserRepository; 6 | import com.david.ecommerceapi.user.domain.UserService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.beans.BeanUtils; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class UserServiceImpl implements UserService { 16 | 17 | private final UserRepository userRepository; 18 | 19 | public User update(User updateUser) { 20 | 21 | User user = userRepository.findById(updateUser.getId()).orElseThrow(() -> new NotFoundException("User not found")); 22 | 23 | BeanUtils.copyProperties(updateUser, user, "role"); 24 | 25 | return userRepository.save(user); 26 | } 27 | 28 | public List findAll() { 29 | return userRepository.findAll(); 30 | } 31 | 32 | public User findById(Long id) throws NotFoundException { 33 | 34 | return userRepository.findById(id).orElseThrow(() -> new NotFoundException("User not found")); 35 | } 36 | 37 | public void delete(Long id) { 38 | userRepository.deleteById(id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/domain/Role.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.domain; 2 | 3 | public enum Role { 4 | USER, 5 | ADMIN 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.domain; 2 | 3 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @Builder 11 | public class User { 12 | 13 | private Long id; 14 | private String firstname; 15 | private String lastname; 16 | private String email; 17 | private String password; 18 | private Role role; 19 | private List shoppingCarts; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/domain/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface UserRepository { 7 | 8 | User save(User userEntity); 9 | 10 | Optional findById(Long id); 11 | 12 | List findAll(); 13 | 14 | Optional findByEmail(String email); 15 | 16 | Boolean existsByEmail(String email); 17 | 18 | void deleteById(Long id); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/domain/UserService.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.domain; 2 | 3 | import java.util.List; 4 | 5 | public interface UserService { 6 | 7 | User update(User user); 8 | 9 | List findAll(); 10 | 11 | User findById(Long id); 12 | 13 | void delete(Long id); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/UserController.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure; 2 | 3 | import com.david.ecommerceapi.user.infrastructure.dto.UserDTO; 4 | import org.springframework.http.ResponseEntity; 5 | 6 | import java.util.List; 7 | 8 | public interface UserController { 9 | 10 | ResponseEntity findById(Long id); 11 | 12 | ResponseEntity> findAll(); 13 | 14 | ResponseEntity update(UserDTO userDTO); 15 | 16 | ResponseEntity delete(Long id); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/UserControllerImpl.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure; 2 | 3 | import com.david.ecommerceapi.exception.domain.NotFoundException; 4 | import com.david.ecommerceapi.user.domain.User; 5 | import com.david.ecommerceapi.user.domain.UserService; 6 | import com.david.ecommerceapi.user.infrastructure.dto.UserDTO; 7 | import com.david.ecommerceapi.user.infrastructure.mapper.UserMapper; 8 | import io.swagger.v3.oas.annotations.Operation; 9 | import io.swagger.v3.oas.annotations.security.SecurityRequirement; 10 | import io.swagger.v3.oas.annotations.tags.Tag; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.security.access.prepost.PreAuthorize; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.List; 17 | 18 | @RestController 19 | @RequestMapping("/api/users") 20 | @SecurityRequirement(name = "Bearer Authentication") 21 | @Tag(name = "User", description = "The User API. Contains all the operations that can be performed on a user.") 22 | @RequiredArgsConstructor 23 | public class UserControllerImpl implements UserController { 24 | 25 | private final UserService userService; 26 | private final UserMapper userMapper; 27 | 28 | @GetMapping("/{id}") 29 | public ResponseEntity findById(@PathVariable Long id) throws NotFoundException { 30 | 31 | User user = userService.findById(id); 32 | 33 | UserDTO userDTO = userMapper.userToUserDTO(user); 34 | 35 | return ResponseEntity.ok(userDTO); 36 | } 37 | 38 | @Operation(summary = "List all users", description = "List all users") 39 | @GetMapping() 40 | @PreAuthorize("hasAuthority('ADMIN')") 41 | public ResponseEntity> findAll() { 42 | 43 | List userDTOS = userService.findAll().stream().map(userMapper::userToUserDTO).toList(); 44 | 45 | return ResponseEntity.ok(userDTOS); 46 | } 47 | 48 | @PutMapping() 49 | public ResponseEntity update(@RequestBody UserDTO userDTO) { 50 | 51 | User user = userMapper.userDTOToUser(userDTO); 52 | 53 | User updated = userService.update(user); 54 | 55 | UserDTO updatedDTO = userMapper.userToUserDTO(updated); 56 | 57 | return ResponseEntity.ok(updatedDTO); 58 | } 59 | 60 | @DeleteMapping("/{id}") 61 | public ResponseEntity delete(@PathVariable Long id) { 62 | 63 | userService.delete(id); 64 | 65 | return ResponseEntity.noContent().build(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/annotation/MaskData.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.annotation; 2 | 3 | import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @JacksonAnnotationsInside 12 | @JsonSerialize(using = ProtectDataSerializer.class) 13 | @Target(ElementType.FIELD) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface MaskData { 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/annotation/ProtectDataSerializer.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.annotation; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.databind.JsonSerializer; 5 | import com.fasterxml.jackson.databind.SerializerProvider; 6 | 7 | import java.io.IOException; 8 | 9 | public class ProtectDataSerializer extends JsonSerializer { 10 | 11 | @Override 12 | public void serialize(String data, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { 13 | String masked = data.replaceAll("\\w(?=\\w{4})", "x"); 14 | 15 | jsonGenerator.writeString(masked); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.dto; 2 | 3 | import com.david.ecommerceapi.user.domain.Role; 4 | import com.david.ecommerceapi.user.infrastructure.annotation.MaskData; 5 | import jakarta.persistence.EnumType; 6 | import jakarta.persistence.Enumerated; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @Builder 16 | public class UserDTO { 17 | 18 | private Long id; 19 | private String firstname; 20 | private String lastname; 21 | @MaskData 22 | private String email; 23 | @Enumerated(EnumType.STRING) 24 | private Role role; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.entity; 2 | 3 | import com.david.ecommerceapi.shoppingCart.domain.ShoppingCart; 4 | import com.david.ecommerceapi.user.domain.Role; 5 | import com.fasterxml.jackson.annotation.JsonManagedReference; 6 | import jakarta.persistence.*; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import org.springframework.security.core.GrantedAuthority; 12 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | 15 | import java.util.Collection; 16 | import java.util.List; 17 | 18 | @Entity 19 | @Data 20 | @Builder 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @Table(name = "users") 24 | public class UserEntity implements UserDetails { 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | private Long id; 28 | private String firstname; 29 | private String lastname; 30 | @Column(unique = true) 31 | private String email; 32 | private String password; 33 | @Enumerated(EnumType.STRING) 34 | private Role role; 35 | @JsonManagedReference 36 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "user") 37 | private List shoppingCarts; 38 | 39 | @Override 40 | public Collection getAuthorities() { 41 | return List.of(new SimpleGrantedAuthority(role.name())); 42 | } 43 | 44 | @Override 45 | public String getUsername() { 46 | return email; 47 | } 48 | 49 | @Override 50 | public String getPassword() { 51 | return password; 52 | } 53 | 54 | @Override 55 | public boolean isAccountNonExpired() { 56 | return true; 57 | } 58 | 59 | @Override 60 | public boolean isAccountNonLocked() { 61 | return true; 62 | } 63 | 64 | @Override 65 | public boolean isCredentialsNonExpired() { 66 | return true; 67 | } 68 | 69 | @Override 70 | public boolean isEnabled() { 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.mapper; 2 | 3 | import com.david.ecommerceapi.auth.infrastructure.RegisterRequest; 4 | import com.david.ecommerceapi.user.domain.User; 5 | import com.david.ecommerceapi.user.infrastructure.dto.UserDTO; 6 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 7 | import org.mapstruct.Mapper; 8 | import org.mapstruct.MappingConstants; 9 | 10 | @Mapper(componentModel = MappingConstants.ComponentModel.SPRING) 11 | public interface UserMapper { 12 | 13 | User userEntityToUser(UserEntity userEntity); 14 | 15 | UserEntity userToUserEntity(User user); 16 | 17 | UserDTO userToUserDTO(User user); 18 | 19 | User userDTOToUser(UserDTO userDTO); 20 | 21 | User registerRequestToUser(RegisterRequest registerRequest); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/repository/MySqlUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.repository; 2 | 3 | import com.david.ecommerceapi.user.domain.User; 4 | import com.david.ecommerceapi.user.domain.UserRepository; 5 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 6 | import com.david.ecommerceapi.user.infrastructure.mapper.UserMapper; 7 | import jakarta.transaction.Transactional; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | @Repository 15 | @RequiredArgsConstructor 16 | public class MySqlUserRepository implements UserRepository { 17 | 18 | private final SpringUserRepository springUserRepository; 19 | private final UserMapper userMapper; 20 | 21 | @Override 22 | public User save(User userEntity) { 23 | UserEntity entity = userMapper.userToUserEntity(userEntity); 24 | UserEntity saved = springUserRepository.save(entity); 25 | return userMapper.userEntityToUser(saved); 26 | } 27 | 28 | @Override 29 | public Optional findById(Long id) { 30 | return springUserRepository.findById(id).map(userMapper::userEntityToUser); 31 | } 32 | 33 | @Override 34 | public List findAll() { 35 | return springUserRepository.findAll().stream().map(userMapper::userEntityToUser).toList(); 36 | } 37 | 38 | @Transactional 39 | @Override 40 | public Optional findByEmail(String email) { 41 | return springUserRepository.findByEmail(email).map(userMapper::userEntityToUser); 42 | } 43 | 44 | @Override 45 | public Boolean existsByEmail(String email) { 46 | return springUserRepository.existsByEmail(email); 47 | } 48 | 49 | @Override 50 | public void deleteById(Long id) { 51 | springUserRepository.deleteById(id); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/user/infrastructure/repository/SpringUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure.repository; 2 | 3 | import com.david.ecommerceapi.user.infrastructure.entity.UserEntity; 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 SpringUserRepository extends JpaRepository { 11 | 12 | Boolean existsByEmail(String email); 13 | 14 | Optional findByEmail(String email); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/david/ecommerceapi/util/FileUploadUtil.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.util; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.nio.file.StandardCopyOption; 13 | @Service 14 | public class FileUploadUtil { 15 | 16 | public String saveFile(String fileName, MultipartFile multipartFile) throws IOException { 17 | 18 | Path uploadPath = Paths.get("src/main/resources/uploads"); 19 | 20 | if (!Files.exists(uploadPath)) { 21 | Files.createDirectories(uploadPath); 22 | } 23 | 24 | String fileCode = RandomStringUtils.randomAlphanumeric(8); 25 | 26 | try (InputStream inputStream = multipartFile.getInputStream()) { 27 | 28 | Path filePath = uploadPath.resolve(fileCode + "-" + fileName); 29 | Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); 30 | 31 | } catch (IOException ioe) { 32 | throw new IOException("Could not save file: " + fileName, ioe); 33 | } 34 | 35 | return fileCode; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: ecommerce-service 4 | datasource: 5 | url: jdbc:postgresql://localhost:5432/ecommerce 6 | username: root 7 | password: password 8 | driver-class-name: org.postgresql.Driver 9 | jpa: 10 | hibernate: 11 | ddl-auto: update 12 | naming: 13 | physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 14 | properties: 15 | hibernate: 16 | dialect: org.hibernate.dialect.PostgreSQLDialect 17 | data: 18 | redis: 19 | host: localhost 20 | port: 6379 21 | timeout: 6000 22 | ai: 23 | openai: 24 | api-key: ${OPEN_AI_KEY} 25 | chat: 26 | options: 27 | model: "gpt-4o-mini" 28 | temperature: 0 29 | max-tokens: 1000 30 | top-p: 1.0 31 | presence-penalty: 0.0 32 | frequency-penalty: 0.0 33 | embedding: 34 | options: 35 | model: "text-embedding-3-small" 36 | vectorstore: 37 | pgvector: 38 | initialize-schema: true # Automatically initializes the pgvector schema in the database 39 | index-type: HNSW # Uses HNSW (Hierarchical Navigable Small World) for approximate nearest neighbor search 40 | distance-type: COSINE_DISTANCE # Uses cosine distance to measure vector similarity 41 | dimensions: 1536 # Defines the number of dimensions for stored vectors (e.g., OpenAI embeddings) 42 | max-document-batch-size: 10000 # (Optional) Maximum number of documents per batch insert for better performance 43 | 44 | management: 45 | endpoints: 46 | web: 47 | exposure: 48 | include: "*" 49 | zipkin: 50 | tracing: 51 | endpoint: http://localhost:9411/api/v2/spans 52 | endpoint: 53 | health: 54 | show: 55 | details: always 56 | metrics: 57 | distribution: 58 | percentiles-histogram: 59 | http.server: 60 | requests: true 61 | tracing: 62 | sampling: 63 | probability: 1.0 64 | prometheus: 65 | metrics: 66 | export: 67 | enabled: true 68 | 69 | springdoc: 70 | swagger-ui: 71 | path: /swagger-ui.html 72 | 73 | logging: 74 | pattern: 75 | correlation: [ "${ spring.application.name: },%X{ traceId:- },%X{ spanId:- }" ] 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:postgresql://localhost:5432/ecommerce 4 | username: root 5 | password: password 6 | driver-class-name: org.postgresql.Driver 7 | jpa: 8 | hibernate: 9 | ddl-auto: create-drop 10 | naming: 11 | physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 12 | properties: 13 | hibernate: 14 | dialect: org.hibernate.dialect.PostgreSQLDialect 15 | data: 16 | redis: 17 | repositories: 18 | enabled: false 19 | ai: 20 | openai: 21 | chat: 22 | enabled: false 23 | embedding: 24 | enabled: false 25 | 26 | management: 27 | endpoints: 28 | web: 29 | exposure: 30 | include: "health" 31 | zipkin: 32 | tracing: 33 | endpoint: "" 34 | prometheus: 35 | metrics: 36 | export: 37 | enabled: false 38 | tracing: 39 | sampling: 40 | probability: 0.0 -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: dev 4 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | http://localhost:3100/loki/api/v1/push 10 | 11 | 12 | 22 | 23 | 24 | 25 | { 26 | "timestamp": "%date{ISO8601}", 27 | "level": "%level", 28 | "thread": "%thread", 29 | "logger": "%logger", 30 | "class": "%class", 31 | "method": "%method", 32 | "line": "%line", 33 | "message": "%msg", 34 | "exception": "%ex{full}" 35 | } 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/resources/prompts/productSuggestionPrompt.txt: -------------------------------------------------------------------------------- 1 | Eres un asistente de Ecommerce muy bueno y destacado por darle al cliente la mejor opción siempre 2 | 3 | Dado el siguiente mensaje de un usuario pidiendo una sugerencia de un producto: 4 | "{request}" 5 | 6 | Y dada la siguiente lista de productos más similares obtenida de nuestra base de datos: 7 | "{products}" 8 | 9 | Responde el mejor producto que consideres para el cliente sin dar tu opinion, solo datos del producto que corresponde a cada valor sin inventar ninguno. 10 | 11 | Deberás responder en el idioma que haya introducido el usuario con su mensaje, lo cual puede implicar traducir los valores del producto seleccionado. -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/EcommerceApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | @Disabled 9 | class EcommerceApiApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/config/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.config; 2 | 3 | import com.david.ecommerceapi.user.domain.Role; 4 | import com.david.ecommerceapi.user.domain.User; 5 | import com.david.ecommerceapi.user.domain.UserRepository; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.boot.test.web.client.TestRestTemplate; 8 | import org.springframework.boot.web.client.RestTemplateBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | 13 | @Configuration 14 | @RequiredArgsConstructor 15 | public class TestConfiguration { 16 | 17 | private final UserRepository userRepository; 18 | 19 | private final PasswordEncoder passwordEncoder; 20 | 21 | @Bean 22 | public TestRestTemplate restTemplate() { 23 | 24 | userRepository.findByEmail("test@gmail.com").orElseGet(() -> { 25 | User user = User.builder() 26 | .role(Role.USER) 27 | .email("test@gmail.com") 28 | .password(passwordEncoder.encode("123456789")) 29 | .firstname("test") 30 | .build(); 31 | 32 | return userRepository.save(user); 33 | }); 34 | 35 | RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder() 36 | .rootUri("http://localhost:8080"); 37 | 38 | return new TestRestTemplate(restTemplateBuilder); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/product/application/ProductServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.application; 2 | 3 | import com.david.ecommerceapi.exception.domain.NotFoundException; 4 | import com.david.ecommerceapi.product.domain.Category; 5 | import com.david.ecommerceapi.product.domain.Product; 6 | import com.david.ecommerceapi.product.infrastructure.repository.implementation.PostgresProductRepositoryImpl; 7 | import com.david.ecommerceapi.product.infrastructure.repository.implementation.RedisProductRepositoryImpl; 8 | import com.david.ecommerceapi.util.FileUploadUtil; 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.Mockito; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | import org.springframework.mock.web.MockMultipartFile; 17 | import org.springframework.util.StringUtils; 18 | import org.springframework.web.multipart.MultipartFile; 19 | 20 | import java.io.IOException; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.Optional; 25 | 26 | import static org.junit.jupiter.api.Assertions.*; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class ProductServiceTest { 30 | @Mock 31 | private PostgresProductRepositoryImpl mySqlProductRepository; 32 | @Mock 33 | private RedisProductRepositoryImpl redisProductRepository; 34 | @Mock 35 | private FileUploadUtil fileUploadUtil; 36 | @Mock 37 | public MultipartFile MULTIPARTFILE_PREPARED = new MockMultipartFile("image.png", "image.png".getBytes(StandardCharsets.UTF_8)); 38 | @InjectMocks 39 | private ProductService productService; 40 | 41 | public Product PRODUCT_BASE_PREPARED = Product.builder() 42 | .id(1L) 43 | .name("Samsung") 44 | .description("Galaxy S3") 45 | .price(23.34) 46 | .image("image.png") 47 | .category(Category.PHONE) 48 | .build(); 49 | public Product PRODUCT_MODIFIED_PREPARED = Product.builder() 50 | .id(1L) 51 | .name("Samsung") 52 | .description("Galaxy S3") 53 | .price(23.34) 54 | .image("image.png") 55 | .category(Category.COMPUTER) 56 | .build(); 57 | 58 | 59 | @Test 60 | void save() throws IOException { 61 | 62 | Mockito.when(mySqlProductRepository.save(PRODUCT_BASE_PREPARED)).thenReturn(PRODUCT_BASE_PREPARED); 63 | 64 | Mockito.when(MULTIPARTFILE_PREPARED.getOriginalFilename()).thenReturn("image.png"); 65 | 66 | Mockito.when(fileUploadUtil.saveFile(StringUtils.cleanPath(MULTIPARTFILE_PREPARED.getOriginalFilename()), MULTIPARTFILE_PREPARED)).thenReturn("2023"); 67 | 68 | Product product = productService.save(PRODUCT_BASE_PREPARED, MULTIPARTFILE_PREPARED); 69 | 70 | assertNotNull(product); 71 | 72 | assertEquals(Category.PHONE, product.getCategory()); 73 | 74 | Mockito.verify(fileUploadUtil, Mockito.times(1)) 75 | .saveFile(StringUtils.cleanPath(MULTIPARTFILE_PREPARED.getOriginalFilename()), MULTIPARTFILE_PREPARED); 76 | 77 | assertEquals("2023-image.png", product.getImage()); 78 | } 79 | 80 | @Test 81 | void find_all() { 82 | Mockito.when(mySqlProductRepository.findAll()).thenReturn(Arrays.asList(PRODUCT_BASE_PREPARED)); 83 | 84 | List productList = productService.findAll(); 85 | 86 | assertNotNull(productList); 87 | 88 | assertEquals(1, productList.size()); 89 | } 90 | 91 | @Test 92 | void find_by_id() { 93 | 94 | Mockito.when(mySqlProductRepository.findById(1L)).thenReturn(Optional.ofNullable(PRODUCT_BASE_PREPARED)); 95 | 96 | Product product = productService.findById(1L); 97 | 98 | assertNotNull(product); 99 | 100 | assertEquals(1, product.getId()); 101 | } 102 | 103 | @Test 104 | void update_without_image() throws IOException { 105 | 106 | Mockito.when(mySqlProductRepository.save(PRODUCT_MODIFIED_PREPARED)).thenReturn(PRODUCT_MODIFIED_PREPARED); 107 | 108 | Mockito.when(mySqlProductRepository.findById(1L)).thenReturn(Optional.ofNullable(PRODUCT_BASE_PREPARED)); 109 | 110 | Product product = productService.update(PRODUCT_MODIFIED_PREPARED, Optional.empty()); 111 | 112 | assertNotNull(product); 113 | 114 | assertEquals(Category.COMPUTER, product.getCategory()); 115 | 116 | assertEquals("image.png", product.getImage()); 117 | } 118 | 119 | @Test 120 | @Disabled 121 | void update_with_image() throws IOException { 122 | 123 | Mockito.when(mySqlProductRepository.save(PRODUCT_MODIFIED_PREPARED)).thenReturn(PRODUCT_MODIFIED_PREPARED); 124 | 125 | Mockito.when(MULTIPARTFILE_PREPARED.getOriginalFilename()).thenReturn("image.png"); 126 | 127 | Mockito.when(fileUploadUtil.saveFile(StringUtils.cleanPath(MULTIPARTFILE_PREPARED.getOriginalFilename()), MULTIPARTFILE_PREPARED)).thenReturn("2024"); 128 | 129 | Mockito.when(mySqlProductRepository.findById(1L)).thenReturn(Optional.ofNullable(PRODUCT_BASE_PREPARED)); 130 | 131 | Product product = productService.update(PRODUCT_MODIFIED_PREPARED, Optional.of(MULTIPARTFILE_PREPARED)); 132 | 133 | assertNotNull(product); 134 | 135 | assertNotNull(product.getImage()); 136 | 137 | assertEquals(Category.COMPUTER, product.getCategory()); 138 | 139 | assertEquals("2024-image.png", product.getImage()); 140 | } 141 | 142 | @Test 143 | void delete() { 144 | 145 | Mockito.when(mySqlProductRepository.findById(PRODUCT_BASE_PREPARED.getId())).thenReturn(Optional.ofNullable(PRODUCT_BASE_PREPARED)); 146 | 147 | Product product = productService.delete(PRODUCT_BASE_PREPARED.getId()); 148 | 149 | Mockito.verify(mySqlProductRepository, Mockito.times(1)).deleteById(PRODUCT_BASE_PREPARED.getId()); 150 | 151 | assertNotNull(product); 152 | } 153 | 154 | @Test 155 | void delete_throw_not_found() { 156 | 157 | Mockito.when(mySqlProductRepository.findById(PRODUCT_BASE_PREPARED.getId() - 1)).thenThrow(NotFoundException.class); 158 | 159 | assertThrows(NotFoundException.class, () -> productService.delete(PRODUCT_BASE_PREPARED.getId() - 1)); 160 | } 161 | } -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/product/infrastructure/ProductControllerMvcTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure; 2 | 3 | import com.david.ecommerceapi.EcommerceApiApplication; 4 | import com.david.ecommerceapi.auth.infrastructure.AuthenticationRequest; 5 | import com.david.ecommerceapi.auth.infrastructure.AuthenticationResponse; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Disabled; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 12 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | import org.springframework.test.web.servlet.MvcResult; 16 | 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 20 | 21 | @WebMvcTest(EcommerceApiApplication.class) 22 | @AutoConfigureMockMvc(addFilters = false) 23 | @Disabled 24 | class ProductControllerMvcTest { 25 | @Autowired 26 | private MockMvc mockMvc; 27 | 28 | private String JWT = ""; 29 | 30 | @BeforeEach 31 | void setUp() throws Exception { 32 | AuthenticationRequest authRequest = AuthenticationRequest.builder() 33 | .email("test@gmail.com") 34 | .password("123456789") 35 | .build(); 36 | 37 | ObjectMapper objectMapper = new ObjectMapper(); 38 | String jsonRequest = objectMapper.writeValueAsString(authRequest); 39 | 40 | MvcResult result = mockMvc.perform(post("http://localhost:8080/api/auth/authenticate") 41 | .contentType("application/json") 42 | .content(jsonRequest)) 43 | .andExpect(status().isOk()) 44 | .andReturn(); 45 | 46 | String jsonResponse = result.getResponse().getContentAsString(); 47 | 48 | AuthenticationResponse response = objectMapper.readValue(jsonResponse, AuthenticationResponse.class); 49 | 50 | JWT = response.getToken(); 51 | } 52 | 53 | @Test 54 | void findAll() throws Exception { 55 | 56 | 57 | this.mockMvc.perform(get("http://localhost:8080/api/products") 58 | .header("Authorization", "Bearer " + JWT)) 59 | .andExpect(status().isOk()) 60 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 61 | .andExpect(jsonPath("$[0].name").value("Samsung")); 62 | } 63 | 64 | @Test 65 | void findById() throws Exception { 66 | 67 | this.mockMvc.perform(get("http://localhost:8080/api/products/1").param("id", "1")) 68 | .andExpect(status().isOk()) 69 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 70 | .andExpect(jsonPath("$.name").value("Samsung")); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/product/infrastructure/ProductControllerRestTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure; 2 | 3 | import com.david.ecommerceapi.auth.infrastructure.AuthenticationRequest; 4 | import com.david.ecommerceapi.auth.infrastructure.AuthenticationResponse; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.http.*; 11 | 12 | import java.util.Objects; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertNotNull; 16 | 17 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 18 | class ProductControllerRestTest { 19 | 20 | @Autowired 21 | private TestRestTemplate restTemplate; 22 | 23 | private HttpEntity entity; 24 | 25 | @BeforeEach 26 | void setUp() { 27 | 28 | ResponseEntity response = restTemplate.postForEntity( 29 | "/api/auth/authenticate", 30 | AuthenticationRequest.builder() 31 | .email("test@gmail.com") 32 | .password("123456789") 33 | .build(), 34 | AuthenticationResponse.class 35 | ); 36 | 37 | String jwt = Objects.requireNonNull(response.getBody()).getToken(); 38 | 39 | entity = new HttpEntity<>(null, createHeadersWithJwt(jwt)); 40 | 41 | } 42 | 43 | private HttpHeaders createHeadersWithJwt(String jwt) { 44 | HttpHeaders headers = new HttpHeaders(); 45 | headers.setContentType(MediaType.APPLICATION_JSON); 46 | headers.setBearerAuth(jwt); 47 | return headers; 48 | } 49 | 50 | @Test 51 | void save() { 52 | 53 | } 54 | 55 | @Test 56 | void findAll() { 57 | 58 | ResponseEntity responseEntity = restTemplate.exchange( 59 | "/api/products", 60 | HttpMethod.GET, 61 | entity, 62 | String.class 63 | ); 64 | 65 | assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); 66 | } 67 | 68 | @Test 69 | void findById() { 70 | 71 | ResponseEntity responseEntity = restTemplate.exchange( 72 | "/api/product/1", 73 | HttpMethod.GET, 74 | entity, 75 | String.class 76 | ); 77 | 78 | assertNotNull(responseEntity); 79 | } 80 | 81 | @Test 82 | void update() { 83 | 84 | 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/product/infrastructure/ProductControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.infrastructure; 2 | 3 | import com.david.ecommerceapi.product.application.ProductService; 4 | import com.david.ecommerceapi.product.domain.Category; 5 | import com.david.ecommerceapi.product.domain.Product; 6 | import org.junit.jupiter.api.Disabled; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.Mockito; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import org.springframework.http.ResponseEntity; 14 | 15 | import java.util.Arrays; 16 | import java.util.List; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class ProductControllerTest { 22 | 23 | @Mock 24 | private ProductService productService; 25 | @InjectMocks 26 | private ProductControllerImpl productController; 27 | 28 | public Product PRODUCT_BASE_PREPARED = Product.builder() 29 | .id(1L) 30 | .name("Samsung") 31 | .description("Galaxy S3") 32 | .price(23.34) 33 | .image("image.png") 34 | .category(Category.COMPUTER) 35 | .build(); 36 | 37 | @Test 38 | void findAll() { 39 | 40 | Mockito.when(productService.findAll()).thenReturn((Arrays.asList(PRODUCT_BASE_PREPARED))); 41 | 42 | ResponseEntity> responseEntity = productController.findAll(); 43 | 44 | assertEquals(1, responseEntity.getBody().size()); 45 | assertEquals(200, responseEntity.getStatusCode().value()); 46 | } 47 | 48 | @Test 49 | @Disabled 50 | void update() { 51 | } 52 | 53 | @Test 54 | @Disabled 55 | void findById() { 56 | } 57 | 58 | @Test 59 | @Disabled 60 | void save() { 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/product/repository/ProductRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.product.repository; 2 | 3 | import com.david.ecommerceapi.product.domain.Category; 4 | import com.david.ecommerceapi.product.infrastructure.entity.ProductEntity; 5 | import com.david.ecommerceapi.product.infrastructure.repository.QueryProductRepository; 6 | import org.junit.jupiter.api.Disabled; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 10 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 11 | import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; 12 | import org.springframework.context.annotation.Profile; 13 | 14 | import java.util.List; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | import static org.junit.jupiter.api.Assertions.assertNull; 18 | 19 | @DataJpaTest 20 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 21 | @Profile("dev") 22 | @Disabled 23 | class ProductRepositoryTest { 24 | @Autowired 25 | public QueryProductRepository productRepository; 26 | @Autowired 27 | public TestEntityManager testEntityManager; 28 | 29 | @Test 30 | void save() { 31 | ProductEntity product = new ProductEntity(null, "prueba", "prueba", 22, null, Category.COMPUTER, null); 32 | 33 | assertNull(product.getId()); 34 | 35 | Long id = testEntityManager.persist(product).getId(); 36 | 37 | assertNotNull(id); 38 | } 39 | 40 | @Test 41 | void find_all() { 42 | List productList = productRepository.findAll(); 43 | 44 | assertNotNull(productList); 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/user/application/UserServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.application; 2 | 3 | import com.david.ecommerceapi.user.domain.Role; 4 | import com.david.ecommerceapi.user.domain.User; 5 | import com.david.ecommerceapi.user.domain.UserRepository; 6 | import com.david.ecommerceapi.user.infrastructure.dto.UserDTO; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Optional; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.mockito.Mockito.*; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class UserServiceImplTest { 22 | 23 | @Mock 24 | private UserRepository userRepository; 25 | 26 | @InjectMocks 27 | private UserServiceImpl userService; 28 | 29 | public User USER_PREPARED = User.builder() 30 | .id(1L) 31 | .firstname("John") 32 | .lastname("Doe") 33 | .email("john@doe.com") 34 | .password("password") 35 | .role(Role.USER) 36 | .build(); 37 | 38 | public User USER_MODIFIED_PREPARED = User.builder() 39 | .id(1L) 40 | .firstname("John") 41 | .lastname("Doe") 42 | .email("john@doe.com") 43 | .password("password") 44 | .role(Role.USER) 45 | .build(); 46 | 47 | public UserDTO USER_DTO_PREPARED = new UserDTO(1L, "Pepe", "Lopez", "pepito@gmail.com", Role.USER); 48 | 49 | 50 | @Test 51 | void update() { 52 | 53 | when(userRepository.findById(1L)).thenReturn(Optional.ofNullable(USER_PREPARED)); 54 | when(userRepository.save(USER_PREPARED)).thenReturn(USER_MODIFIED_PREPARED); 55 | 56 | userService.update(USER_PREPARED); 57 | 58 | assertEquals("John", USER_MODIFIED_PREPARED.getFirstname()); 59 | } 60 | 61 | @Test 62 | void findAll() { 63 | 64 | List usersMock = new ArrayList<>(List.of(USER_PREPARED)); 65 | 66 | when(userRepository.findAll()).thenReturn(usersMock); 67 | 68 | List users = userService.findAll(); 69 | 70 | assertEquals(1, users.size()); 71 | } 72 | 73 | @Test 74 | void findById() { 75 | 76 | when(userRepository.findById(1L)).thenReturn(Optional.ofNullable(USER_PREPARED)); 77 | 78 | User userDB = userService.findById(1L); 79 | 80 | assertEquals(1, userDB.getId()); 81 | } 82 | 83 | @Test 84 | void delete() { 85 | 86 | userService.delete(1L); 87 | 88 | verify(userRepository, times(1)).deleteById(1L); 89 | 90 | } 91 | } -------------------------------------------------------------------------------- /src/test/java/com/david/ecommerceapi/user/infrastructure/UserEntityControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.david.ecommerceapi.user.infrastructure; 2 | 3 | import com.david.ecommerceapi.user.application.UserServiceImpl; 4 | import com.david.ecommerceapi.user.domain.Role; 5 | import com.david.ecommerceapi.user.domain.User; 6 | import com.david.ecommerceapi.user.infrastructure.dto.UserDTO; 7 | import com.david.ecommerceapi.user.infrastructure.mapper.UserMapper; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import org.springframework.http.ResponseEntity; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.mockito.Mockito.when; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class UserEntityControllerTest { 20 | 21 | @Mock 22 | private UserServiceImpl userService; 23 | 24 | @Mock 25 | private UserMapper userMapper; 26 | 27 | @InjectMocks 28 | private UserControllerImpl userControllerImpl; 29 | 30 | private final User USER = User.builder() 31 | .id(1L) 32 | .firstname("firstname") 33 | .lastname("lastname") 34 | .email("email") 35 | .role(Role.USER) 36 | .build(); 37 | 38 | private final UserDTO userDTO = UserDTO.builder() 39 | .id(1L) 40 | .firstname("firstname") 41 | .lastname("lastname") 42 | .email("email") 43 | .role(Role.USER) 44 | .build(); 45 | 46 | @Test 47 | void findById() { 48 | when(userService.findById(1L)).thenReturn(USER); 49 | when(userMapper.userToUserDTO(USER)).thenReturn(userDTO); 50 | 51 | ResponseEntity responseEntity = userControllerImpl.findById(1L); 52 | 53 | assertEquals(1, responseEntity.getBody().getId()); 54 | assertEquals(200, responseEntity.getStatusCode().value()); 55 | 56 | } 57 | } --------------------------------------------------------------------------------