├── .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 extends GrantedAuthority> 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 | }
--------------------------------------------------------------------------------