From 30cd472f6c4e54c7a1431c7884c0a6f68bfbd52f Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 13:15:51 +0000 Subject: [PATCH 01/25] product-api: initial api impl --- pom.xml | 13 ++ src/main/java/com/amigoscode/Main.java | 23 +++ .../java/com/amigoscode/product/Product.java | 137 ++++++++++++++++++ .../amigoscode/product/ProductController.java | 23 +++ .../amigoscode/product/ProductRepository.java | 9 ++ .../amigoscode/product/ProductService.java | 18 +++ src/main/resources/application.properties | 10 +- 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/amigoscode/product/Product.java create mode 100644 src/main/java/com/amigoscode/product/ProductController.java create mode 100644 src/main/java/com/amigoscode/product/ProductRepository.java create mode 100644 src/main/java/com/amigoscode/product/ProductService.java diff --git a/pom.xml b/pom.xml index a494e2a..3dce32a 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,19 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-web + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-data-jpa + diff --git a/src/main/java/com/amigoscode/Main.java b/src/main/java/com/amigoscode/Main.java index cc8e2f5..2c4c17b 100644 --- a/src/main/java/com/amigoscode/Main.java +++ b/src/main/java/com/amigoscode/Main.java @@ -1,7 +1,14 @@ package com.amigoscode; +import com.amigoscode.product.Product; +import com.amigoscode.product.ProductRepository; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.math.BigDecimal; +import java.util.UUID; @SpringBootApplication public class Main { @@ -10,4 +17,20 @@ public static void main(String[] args) { SpringApplication.run(Main.class, args); } + @Bean + public CommandLineRunner commandLineRunner( + ProductRepository productRepository) { + return args -> { + Product product = new Product(); + product.setName("Macbook Pro"); + product.setDescription("Macbook Pro M4"); + product.setPrice(new BigDecimal(3000)); + product.setId(UUID.fromString( + "d95062e6-9f0b-4224-bc9d-d0723949848f") + ); + product.setStockLevel(100); + productRepository.save(product); + }; + } + } diff --git a/src/main/java/com/amigoscode/product/Product.java b/src/main/java/com/amigoscode/product/Product.java new file mode 100644 index 0000000..c5bea01 --- /dev/null +++ b/src/main/java/com/amigoscode/product/Product.java @@ -0,0 +1,137 @@ +package com.amigoscode.product; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +@Entity +@Table( + name = "product" +) +public class Product { + + @Id + private UUID id; + + @Column(nullable = false, length = 50) + private String name; + + private String description; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal price; + + @Column(length = 200) + private String imageUrl; + + @Column(nullable = false) + private Integer stockLevel; + + @Column(nullable = false, updatable = false) + private Instant createdAt; + + private Instant updatedAt; + + private Instant deletedAt; + + @PrePersist + public void prePersist(){ + if(this.id == null) { + this.id = UUID.randomUUID(); + } + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + } + + @PreUpdate + public void preUpdate(){ + this.updatedAt = Instant.now(); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public int getStockLevel() { + return stockLevel; + } + + public void setStockLevel(int stockLevel) { + this.stockLevel = stockLevel; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public Instant getDeletedAt() { + return deletedAt; + } + + public void setDeletedAt(Instant deletedAt) { + this.deletedAt = deletedAt; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Product product = (Product) o; + return stockLevel == product.stockLevel && Objects.equals(id, product.id) && Objects.equals(name, product.name) && Objects.equals(description, product.description) && Objects.equals(price, product.price) && Objects.equals(imageUrl, product.imageUrl) && Objects.equals(createdAt, product.createdAt) && Objects.equals(updatedAt, product.updatedAt) && Objects.equals(deletedAt, product.deletedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, description, price, imageUrl, stockLevel, createdAt, updatedAt, deletedAt); + } +} diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java new file mode 100644 index 0000000..731023c --- /dev/null +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -0,0 +1,23 @@ +package com.amigoscode.product; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/products") +public class ProductController { + + private final ProductService productService; + + public ProductController(ProductService productService) { + this.productService = productService; + } + + @GetMapping + public List getAllProducts() { + return productService.getAllProducts(); + } +} diff --git a/src/main/java/com/amigoscode/product/ProductRepository.java b/src/main/java/com/amigoscode/product/ProductRepository.java new file mode 100644 index 0000000..0762e60 --- /dev/null +++ b/src/main/java/com/amigoscode/product/ProductRepository.java @@ -0,0 +1,9 @@ +package com.amigoscode.product; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface ProductRepository + extends JpaRepository { +} diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java new file mode 100644 index 0000000..eef395f --- /dev/null +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -0,0 +1,18 @@ +package com.amigoscode.product; + +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ProductService { + private final ProductRepository productRepository; + + public ProductService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public List getAllProducts() { + return productRepository.findAll(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e653aee..670c271 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,9 @@ -spring.application.name=java-springboot-full-stack +spring.application.name=jfs +spring.datasource.url=jdbc:postgresql://localhost:5432/jfs +spring.datasource.username=amigoscode +spring.datasource.password= +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file From 5d5bd1ef1cf2c9aee2afa68802358e3608d6c58d Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 13:17:29 +0000 Subject: [PATCH 02/25] product-api: delete by id sol --- .../com/amigoscode/exception/ResourceNotFound.java | 11 +++++++++++ .../com/amigoscode/product/ProductController.java | 7 +++++++ .../java/com/amigoscode/product/ProductService.java | 9 +++++++++ src/main/resources/application.properties | 4 +++- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/amigoscode/exception/ResourceNotFound.java diff --git a/src/main/java/com/amigoscode/exception/ResourceNotFound.java b/src/main/java/com/amigoscode/exception/ResourceNotFound.java new file mode 100644 index 0000000..4514e92 --- /dev/null +++ b/src/main/java/com/amigoscode/exception/ResourceNotFound.java @@ -0,0 +1,11 @@ +package com.amigoscode.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFound extends RuntimeException { + public ResourceNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index 731023c..4be8183 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -1,10 +1,12 @@ package com.amigoscode.product; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/v1/products") @@ -20,4 +22,9 @@ public ProductController(ProductService productService) { public List getAllProducts() { return productService.getAllProducts(); } + + @GetMapping("{id}") + public Product getProductById(@PathVariable("id") UUID id) { + return productService.getProductById(id); + } } diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index eef395f..806c979 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -1,8 +1,10 @@ package com.amigoscode.product; +import com.amigoscode.exception.ResourceNotFound; import org.springframework.stereotype.Service; import java.util.List; +import java.util.UUID; @Service public class ProductService { @@ -15,4 +17,11 @@ public ProductService(ProductRepository productRepository) { public List getAllProducts() { return productRepository.findAll(); } + + public Product getProductById(UUID id) { + return productRepository.findById(id) + .orElseThrow(() -> new ResourceNotFound( + "product with id [" + id + "] not found" + )); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 670c271..a0398e6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,4 +6,6 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file +spring.jpa.properties.hibernate.format_sql=true + +server.error.include-message=always \ No newline at end of file From 1ce58fb8ee126295a972f8edc80e70d0afa62795 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 14:17:57 +0000 Subject: [PATCH 03/25] product-api: delete and save --- .../amigoscode/product/NewProductRequest.java | 12 ++++++++++ .../java/com/amigoscode/product/Product.java | 16 +++++++++++++ .../amigoscode/product/ProductController.java | 15 ++++++++---- .../amigoscode/product/ProductService.java | 24 +++++++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/amigoscode/product/NewProductRequest.java diff --git a/src/main/java/com/amigoscode/product/NewProductRequest.java b/src/main/java/com/amigoscode/product/NewProductRequest.java new file mode 100644 index 0000000..e1d943d --- /dev/null +++ b/src/main/java/com/amigoscode/product/NewProductRequest.java @@ -0,0 +1,12 @@ +package com.amigoscode.product; + +import java.math.BigDecimal; + +public record NewProductRequest( + String name, + String description, + BigDecimal price, + Integer stockLevel, + String imageUrl +) { +} diff --git a/src/main/java/com/amigoscode/product/Product.java b/src/main/java/com/amigoscode/product/Product.java index c5bea01..607d5e1 100644 --- a/src/main/java/com/amigoscode/product/Product.java +++ b/src/main/java/com/amigoscode/product/Product.java @@ -37,6 +37,22 @@ public class Product { private Instant deletedAt; + public Product() {} + + public Product(UUID id, + String name, + String description, + BigDecimal price, + String imageUrl, + Integer stockLevel) { + this.id = id; + this.name = name; + this.description = description; + this.price = price; + this.imageUrl = imageUrl; + this.stockLevel = stockLevel; + } + @PrePersist public void prePersist(){ if(this.id == null) { diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index 4be8183..1643b08 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -1,9 +1,6 @@ package com.amigoscode.product; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; @@ -27,4 +24,14 @@ public List getAllProducts() { public Product getProductById(@PathVariable("id") UUID id) { return productService.getProductById(id); } + + @DeleteMapping("{id}") + public void deleteProductById(@PathVariable("id") UUID id) { + productService.deleteProductById(id); + } + + @PostMapping + public UUID saveProduct(@RequestBody NewProductRequest product) { + return productService.saveNewProduct(product); + } } diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index 806c979..cf7ca34 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -24,4 +24,28 @@ public Product getProductById(UUID id) { "product with id [" + id + "] not found" )); } + + public void deleteProductById(UUID id) { + boolean exists = productRepository.existsById(id); + if (!exists) { + throw new ResourceNotFound( + "product with id [" + id + "] not found" + ); + } + productRepository.deleteById(id); + } + + public UUID saveNewProduct(NewProductRequest product) { + UUID id = UUID.randomUUID(); + Product newProduct = new Product( + id, + product.name(), + product.description(), + product.price(), + product.imageUrl(), + product.stockLevel() + ); + productRepository.save(newProduct); + return id; + } } From d0ad7184a40c53b0cadbeff389a1c1ff91c9c0a8 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 14:18:51 +0000 Subject: [PATCH 04/25] product-api: dto exercise sol --- .../amigoscode/product/ProductController.java | 4 +-- .../amigoscode/product/ProductResponse.java | 18 +++++++++++++ .../amigoscode/product/ProductService.java | 26 ++++++++++++++++--- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/amigoscode/product/ProductResponse.java diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index 1643b08..f98c597 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -16,12 +16,12 @@ public ProductController(ProductService productService) { } @GetMapping - public List getAllProducts() { + public List getAllProducts() { return productService.getAllProducts(); } @GetMapping("{id}") - public Product getProductById(@PathVariable("id") UUID id) { + public ProductResponse getProductById(@PathVariable("id") UUID id) { return productService.getProductById(id); } diff --git a/src/main/java/com/amigoscode/product/ProductResponse.java b/src/main/java/com/amigoscode/product/ProductResponse.java new file mode 100644 index 0000000..9df785a --- /dev/null +++ b/src/main/java/com/amigoscode/product/ProductResponse.java @@ -0,0 +1,18 @@ +package com.amigoscode.product; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.UUID; + +public record ProductResponse( + UUID id, + String name, + String description, + BigDecimal price, + String imageUrl, + Integer stockLevel, + Instant createdAt, + Instant updatedAt, + Instant deletedAt +) { +} diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index cf7ca34..e8662a5 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; @Service public class ProductService { @@ -14,12 +16,15 @@ public ProductService(ProductRepository productRepository) { this.productRepository = productRepository; } - public List getAllProducts() { - return productRepository.findAll(); + public List getAllProducts() { + return productRepository.findAll().stream() + .map(mapToResponse()) + .collect(Collectors.toList()); } - public Product getProductById(UUID id) { + public ProductResponse getProductById(UUID id) { return productRepository.findById(id) + .map(mapToResponse()) .orElseThrow(() -> new ResourceNotFound( "product with id [" + id + "] not found" )); @@ -48,4 +53,19 @@ public UUID saveNewProduct(NewProductRequest product) { productRepository.save(newProduct); return id; } + + private Function mapToResponse() { + return p -> new ProductResponse( + p.getId(), + p.getName(), + p.getDescription(), + p.getPrice(), + p.getImageUrl(), + p.getStockLevel(), + p.getCreatedAt(), + p.getUpdatedAt(), + p.getDeletedAt() + ); + } + } From 42c89dc2cc62f01ff615a8a5ab1f7cfbe2f9187b Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 15:15:10 +0000 Subject: [PATCH 05/25] product-api: update sol --- .../java/com/amigoscode/product/Product.java | 9 +++--- .../amigoscode/product/ProductController.java | 6 ++++ .../amigoscode/product/ProductService.java | 29 +++++++++++++++++++ .../product/UpdateProductRequest.java | 12 ++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/amigoscode/product/UpdateProductRequest.java diff --git a/src/main/java/com/amigoscode/product/Product.java b/src/main/java/com/amigoscode/product/Product.java index 607d5e1..41e1b02 100644 --- a/src/main/java/com/amigoscode/product/Product.java +++ b/src/main/java/com/amigoscode/product/Product.java @@ -37,7 +37,8 @@ public class Product { private Instant deletedAt; - public Product() {} + public Product() { + } public Product(UUID id, String name, @@ -54,8 +55,8 @@ public Product(UUID id, } @PrePersist - public void prePersist(){ - if(this.id == null) { + public void prePersist() { + if (this.id == null) { this.id = UUID.randomUUID(); } this.createdAt = Instant.now(); @@ -63,7 +64,7 @@ public void prePersist(){ } @PreUpdate - public void preUpdate(){ + public void preUpdate() { this.updatedAt = Instant.now(); } diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index f98c597..564d1dd 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -34,4 +34,10 @@ public void deleteProductById(@PathVariable("id") UUID id) { public UUID saveProduct(@RequestBody NewProductRequest product) { return productService.saveNewProduct(product); } + + @PutMapping("{id}") + public void updateProduct(@PathVariable UUID id, + @RequestBody UpdateProductRequest request) { + productService.updateProduct(id, request); + } } diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index e8662a5..aa91192 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -2,6 +2,9 @@ import com.amigoscode.exception.ResourceNotFound; import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import java.util.List; import java.util.UUID; @@ -68,4 +71,30 @@ private Function mapToResponse() { ); } + + public void updateProduct(UUID id, + UpdateProductRequest updateRequest) { + Product product = productRepository.findById(id) + .orElseThrow(() -> new ResourceNotFound( + "product with id [" + id + "] not found" + )); + + if (updateRequest.name() != null && !updateRequest.name().equals(product.getName())) { + product.setName(updateRequest.name()); + } + if (updateRequest.description() != null && !updateRequest.description().equals(product.getDescription())) { + product.setDescription(updateRequest.description()); + } + if (updateRequest.price() != null && !updateRequest.price().equals(product.getPrice())) { + product.setPrice(updateRequest.price()); + } + if (updateRequest.imageUrl() != null && !updateRequest.imageUrl().equals(product.getImageUrl())) { + product.setImageUrl(updateRequest.imageUrl()); + } + if (updateRequest.stockLevel() != null && !updateRequest.stockLevel().equals(product.getStockLevel())) { + product.setStockLevel(updateRequest.stockLevel()); + } + + productRepository.save(product); + } } diff --git a/src/main/java/com/amigoscode/product/UpdateProductRequest.java b/src/main/java/com/amigoscode/product/UpdateProductRequest.java new file mode 100644 index 0000000..1f6ba61 --- /dev/null +++ b/src/main/java/com/amigoscode/product/UpdateProductRequest.java @@ -0,0 +1,12 @@ +package com.amigoscode.product; + +import java.math.BigDecimal; + +public record UpdateProductRequest( + String name, + String description, + String imageUrl, + BigDecimal price, + Integer stockLevel +) { +} From 8a1f59818a039b98b71b07e865391601428bcb21 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 16:48:59 +0000 Subject: [PATCH 06/25] product-api: java bean validation --- pom.xml | 4 ++++ .../amigoscode/product/NewProductRequest.java | 18 ++++++++++++++++++ .../java/com/amigoscode/product/Product.java | 1 + .../amigoscode/product/ProductController.java | 4 +++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3dce32a..adc788a 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-validation + diff --git a/src/main/java/com/amigoscode/product/NewProductRequest.java b/src/main/java/com/amigoscode/product/NewProductRequest.java index e1d943d..5336784 100644 --- a/src/main/java/com/amigoscode/product/NewProductRequest.java +++ b/src/main/java/com/amigoscode/product/NewProductRequest.java @@ -1,11 +1,29 @@ package com.amigoscode.product; +import jakarta.validation.constraints.*; + import java.math.BigDecimal; public record NewProductRequest( + @NotBlank + @Size( + min = 2, + max = 50, + message = "Name must be between 2 and 50 characters" + ) String name, + @Size( + min = 5, + max = 500, + message = "Description must be between 5 and 500 characters" + ) String description, + @NotNull(message = "Price is required") + @DecimalMin(value = "0.1", message = "Price must be greater than 0.1") BigDecimal price, + + @NotNull(message = "Price is required") + @Min(value = 1, message = "Min Stock Level is 1") Integer stockLevel, String imageUrl ) { diff --git a/src/main/java/com/amigoscode/product/Product.java b/src/main/java/com/amigoscode/product/Product.java index 41e1b02..b8044c7 100644 --- a/src/main/java/com/amigoscode/product/Product.java +++ b/src/main/java/com/amigoscode/product/Product.java @@ -19,6 +19,7 @@ public class Product { @Column(nullable = false, length = 50) private String name; + @Column(length = 500) private String description; @Column(nullable = false, precision = 10, scale = 2) diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index 564d1dd..b80be72 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -1,5 +1,7 @@ package com.amigoscode.product; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -31,7 +33,7 @@ public void deleteProductById(@PathVariable("id") UUID id) { } @PostMapping - public UUID saveProduct(@RequestBody NewProductRequest product) { + public UUID saveProduct(@RequestBody @Valid NewProductRequest product) { return productService.saveNewProduct(product); } From cffbba80f348bb5588aaa25f7b3e411484090156 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 16:52:43 +0000 Subject: [PATCH 07/25] product-api: java bean validation exercise sol --- .../amigoscode/product/ProductController.java | 2 +- .../product/UpdateProductRequest.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index b80be72..42786c7 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -39,7 +39,7 @@ public UUID saveProduct(@RequestBody @Valid NewProductRequest product) { @PutMapping("{id}") public void updateProduct(@PathVariable UUID id, - @RequestBody UpdateProductRequest request) { + @RequestBody @Valid UpdateProductRequest request) { productService.updateProduct(id, request); } } diff --git a/src/main/java/com/amigoscode/product/UpdateProductRequest.java b/src/main/java/com/amigoscode/product/UpdateProductRequest.java index 1f6ba61..50a02f7 100644 --- a/src/main/java/com/amigoscode/product/UpdateProductRequest.java +++ b/src/main/java/com/amigoscode/product/UpdateProductRequest.java @@ -1,12 +1,33 @@ package com.amigoscode.product; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + import java.math.BigDecimal; public record UpdateProductRequest( + @Size( + min = 2, + max = 50, + message = "Name must be between 2 and 50 characters" + ) String name, + + @Size( + min = 5, + max = 500, + message = "Description must be between 5 and 500 characters" + ) String description, + String imageUrl, + + @DecimalMin(value = "0.1", message = "Price must be greater than 0.1") BigDecimal price, + + @Min(value = 1, message = "Min Stock Level is 1") Integer stockLevel ) { } From 9651f44e3d95a2479a8b86cf6ae75d5db550f779 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 19 Nov 2024 20:51:32 +0000 Subject: [PATCH 08/25] product-api: exceptions --- .../amigoscode/exception/ErrorResponse.java | 13 +++ .../exception/GlobalExceptionHandler.java | 88 +++++++++++++++++++ .../exception/ResourceNotFound.java | 4 - 3 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/amigoscode/exception/ErrorResponse.java create mode 100644 src/main/java/com/amigoscode/exception/GlobalExceptionHandler.java diff --git a/src/main/java/com/amigoscode/exception/ErrorResponse.java b/src/main/java/com/amigoscode/exception/ErrorResponse.java new file mode 100644 index 0000000..41aafb4 --- /dev/null +++ b/src/main/java/com/amigoscode/exception/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.amigoscode.exception; + +import java.time.Instant; +import java.util.Map; + +public record ErrorResponse( + String message, + String error, + int statusCode, + String path, + Instant timestamp, + Map fieldErrors) { +} diff --git a/src/main/java/com/amigoscode/exception/GlobalExceptionHandler.java b/src/main/java/com/amigoscode/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..da69694 --- /dev/null +++ b/src/main/java/com/amigoscode/exception/GlobalExceptionHandler.java @@ -0,0 +1,88 @@ +package com.amigoscode.exception; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ResourceNotFound.class) + public ResponseEntity handleResourceNotFound( + ResourceNotFound ex, + HttpServletRequest request + ) { + ErrorResponse errorResponse = new ErrorResponse( + ex.getMessage(), + HttpStatus.NOT_FOUND.getReasonPhrase(), + HttpStatus.NOT_FOUND.value(), + request.getRequestURI(), + Instant.now(), + null + ); + return new ResponseEntity<>( + errorResponse, + HttpStatus.NOT_FOUND + ); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException( + MethodArgumentNotValidException ex, + HttpServletRequest request + ) { + + Map errors = ex.getBindingResult() + .getFieldErrors() + .stream() + .collect(Collectors.toMap( + FieldError::getField, + fieldError -> + Objects.requireNonNullElse( + fieldError.getDefaultMessage(), + "no error available" + ) + )); + ErrorResponse errorResponse = new ErrorResponse( + ex.getMessage(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), + HttpStatus.BAD_REQUEST.value(), + request.getRequestURI(), + Instant.now(), + errors + ); + return new ResponseEntity<>( + errorResponse, + HttpStatus.BAD_REQUEST + ); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException( + Exception ex, + HttpServletRequest request + ) { + ErrorResponse errorResponse = new ErrorResponse( + ex.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), + HttpStatus.INTERNAL_SERVER_ERROR.value(), + request.getRequestURI(), + Instant.now(), + null + ); + return new ResponseEntity<>( + errorResponse, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + +} diff --git a/src/main/java/com/amigoscode/exception/ResourceNotFound.java b/src/main/java/com/amigoscode/exception/ResourceNotFound.java index 4514e92..0b74f6e 100644 --- a/src/main/java/com/amigoscode/exception/ResourceNotFound.java +++ b/src/main/java/com/amigoscode/exception/ResourceNotFound.java @@ -1,9 +1,5 @@ package com.amigoscode.exception; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFound extends RuntimeException { public ResourceNotFound(String message) { super(message); From a50b2d50fa973ebcc88a91101380d5dcd30e0226 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Thu, 21 Nov 2024 18:58:12 +0000 Subject: [PATCH 09/25] product-api: flyway and 1st migration --- pom.xml | 8 ++++++++ src/main/resources/application.properties | 2 +- .../resources/db/migration/V1__Product_Table.sql | 13 +++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V1__Product_Table.sql diff --git a/pom.xml b/pom.xml index adc788a..0385289 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,14 @@ org.springframework.boot spring-boot-starter-validation + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a0398e6..feb5b79 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,7 +4,7 @@ spring.datasource.username=amigoscode spring.datasource.password= spring.datasource.driver-class-name=org.postgresql.Driver -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true diff --git a/src/main/resources/db/migration/V1__Product_Table.sql b/src/main/resources/db/migration/V1__Product_Table.sql new file mode 100644 index 0000000..08d38cd --- /dev/null +++ b/src/main/resources/db/migration/V1__Product_Table.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS product ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(50) NOT NULL CHECK (char_length(name) > 1), + description VARCHAR(500) CHECK (char_length(description) >= 5), + price NUMERIC(10, 2) NOT NULL CHECK (price > 0), + image_url VARCHAR(200) CHECK (char_length(image_url) > 0), + stock_level INT NOT NULL DEFAULT 1 CHECK (stock_level >= 0), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); From 789755dfe370c17849f590bebe9cf7e6a5765c68 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Fri, 22 Nov 2024 14:36:12 +0000 Subject: [PATCH 10/25] exercise 1 sol --- .../resources/db/migration/V2__Add_Is_Published_To_Product.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql diff --git a/src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql b/src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql new file mode 100644 index 0000000..4df7de6 --- /dev/null +++ b/src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql @@ -0,0 +1,2 @@ +ALTER TABLE product +ADD COLUMN is_published BOOLEAN DEFAULT TRUE; \ No newline at end of file From fc8280fdd4d11175304efc95dba085e02437314e Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Fri, 22 Nov 2024 14:36:21 +0000 Subject: [PATCH 11/25] exercise 2 sol --- src/main/java/com/amigoscode/Main.java | 25 +++++++++++++------ .../java/com/amigoscode/product/Product.java | 12 ++++++++- ...__Set_Product_Is_Published_To_Not_Null.sql | 3 +++ 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql diff --git a/src/main/java/com/amigoscode/Main.java b/src/main/java/com/amigoscode/Main.java index 2c4c17b..a17a7b6 100644 --- a/src/main/java/com/amigoscode/Main.java +++ b/src/main/java/com/amigoscode/Main.java @@ -21,15 +21,26 @@ public static void main(String[] args) { public CommandLineRunner commandLineRunner( ProductRepository productRepository) { return args -> { - Product product = new Product(); - product.setName("Macbook Pro"); - product.setDescription("Macbook Pro M4"); - product.setPrice(new BigDecimal(3000)); - product.setId(UUID.fromString( + Product product1 = new Product(); + product1.setName("Macbook Pro"); + product1.setDescription("Macbook Pro M4"); + product1.setPrice(new BigDecimal(3000)); + product1.setId(UUID.fromString( "d95062e6-9f0b-4224-bc9d-d0723949848f") ); - product.setStockLevel(100); - productRepository.save(product); + product1.setStockLevel(100); + productRepository.save(product1); + + Product product2 = new Product(); + product2.setId(UUID.fromString( + "94d2cc8a-ad09-4902-a321-a6bf658e2463" + )); + product2.setName("Mouse"); + product2.setDescription("LG Mouse"); + product2.setPrice(new BigDecimal(78)); + product2.setStockLevel(1000); + + productRepository.save(product2); }; } diff --git a/src/main/java/com/amigoscode/product/Product.java b/src/main/java/com/amigoscode/product/Product.java index b8044c7..a7a620d 100644 --- a/src/main/java/com/amigoscode/product/Product.java +++ b/src/main/java/com/amigoscode/product/Product.java @@ -38,6 +38,8 @@ public class Product { private Instant deletedAt; + private Boolean isPublished = true; + public Product() { } @@ -141,11 +143,19 @@ public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } + public Boolean getPublished() { + return isPublished; + } + + public void setPublished(Boolean published) { + isPublished = published; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Product product = (Product) o; - return stockLevel == product.stockLevel && Objects.equals(id, product.id) && Objects.equals(name, product.name) && Objects.equals(description, product.description) && Objects.equals(price, product.price) && Objects.equals(imageUrl, product.imageUrl) && Objects.equals(createdAt, product.createdAt) && Objects.equals(updatedAt, product.updatedAt) && Objects.equals(deletedAt, product.deletedAt); + return Objects.equals(stockLevel, product.stockLevel) && Objects.equals(id, product.id) && Objects.equals(name, product.name) && Objects.equals(description, product.description) && Objects.equals(price, product.price) && Objects.equals(imageUrl, product.imageUrl) && Objects.equals(createdAt, product.createdAt) && Objects.equals(updatedAt, product.updatedAt) && Objects.equals(deletedAt, product.deletedAt); } @Override diff --git a/src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql b/src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql new file mode 100644 index 0000000..015cfdf --- /dev/null +++ b/src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql @@ -0,0 +1,3 @@ +ALTER TABLE product +ALTER COLUMN is_published +SET NOT NULL; \ No newline at end of file From b6ab94aad0de780526739245cc535b3f22e06ce9 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Fri, 22 Nov 2024 14:38:53 +0000 Subject: [PATCH 12/25] exercise 3 sol --- src/main/java/com/amigoscode/product/ProductService.java | 4 ++++ .../java/com/amigoscode/product/UpdateProductRequest.java | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index aa91192..9d13815 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -95,6 +95,10 @@ public void updateProduct(UUID id, product.setStockLevel(updateRequest.stockLevel()); } + if (updateRequest.isPublished() != null && !updateRequest.isPublished().equals(product.getPublished())) { + product.setPublished(updateRequest.isPublished()); + } + productRepository.save(product); } } diff --git a/src/main/java/com/amigoscode/product/UpdateProductRequest.java b/src/main/java/com/amigoscode/product/UpdateProductRequest.java index 50a02f7..5c8191f 100644 --- a/src/main/java/com/amigoscode/product/UpdateProductRequest.java +++ b/src/main/java/com/amigoscode/product/UpdateProductRequest.java @@ -28,6 +28,8 @@ public record UpdateProductRequest( BigDecimal price, @Min(value = 1, message = "Min Stock Level is 1") - Integer stockLevel + Integer stockLevel, + + Boolean isPublished ) { } From b408bfe0e778594d32d215df5affb181240351c3 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 26 Nov 2024 09:48:26 +0000 Subject: [PATCH 13/25] product-service-docker: Exercise jar solution --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 0385289..8c0139a 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ com.amigoscode jfs 0.0.1-SNAPSHOT + jar java-springboot-full-stack java-springboot-full-stack @@ -68,6 +69,7 @@ + product-service org.springframework.boot From 6966c02ce48c9a48921e612672bb54169cc2aa46 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 26 Nov 2024 11:26:05 +0000 Subject: [PATCH 14/25] product-service-docker: docker compose --- Dockerfile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3867cfa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Build argument for Java version +ARG JAVA_VERSION=21 + +# Stage 1: Build the application +FROM maven:3.9.4 AS builder +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn clean package + +# Stage 2: Run the application +FROM openjdk:${JAVA_VERSION}-jdk-slim +WORKDIR /app +COPY --from=builder /app/target/product-service.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] From 37dd720651687c591d780e161b10aac52b35ca6b Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 26 Nov 2024 14:27:26 +0000 Subject: [PATCH 15/25] product-service-docker: exercise docker compose sol --- docker-compose.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3683058 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +services: + product-service: + container_name: product + build: + context: . + dockerfile: Dockerfile + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/jfs + SPRING_DATASOURCE_USERNAME: amigoscode + SPRING_DATASOURCE_PASSWORD: password + ports: + - "8090:8080" + networks: + - amigos + db: + container_name: jfs-postgres + image: postgres + environment: + POSTGRES_USER: amigoscode + POSTGRES_PASSWORD: password + POSTGRES_DB: jfs + ports: + - "5333:5432" + restart: unless-stopped + volumes: + - db:/data/postgres + networks: + - amigos + +networks: + amigos: + driver: bridge + +volumes: + db: From 99c7dc821fc8716672b433fc70f0b5d3484be3b6 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 26 Nov 2024 15:24:21 +0000 Subject: [PATCH 16/25] product-service-docker: jib and maven build --- docker-compose.yml | 1 + pom.xml | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3683058..e8ac947 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: product-service: container_name: product +# image: amigoscode/product-service:jibMaven build: context: . dockerfile: Dockerfile diff --git a/pom.xml b/pom.xml index 8c0139a..87d8f49 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,9 @@ 21 + amigoscode + product-service + @@ -75,6 +78,32 @@ org.springframework.boot spring-boot-maven-plugin + + com.google.cloud.tools + jib-maven-plugin + 3.3.2 + + + eclipse-temurin:21-jre + + + amd64 + linux + + + arm64 + linux + + + + + docker.io/${docker.username}/${docker.image.name}:${docker.image.tag} + + latest + + + + From 7ecc61a19daed9293d2b1ccacf823524edfbf99c Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Tue, 3 Jun 2025 16:15:40 +0100 Subject: [PATCH 17/25] testcontainers --- docker-compose-db-only.yml | 22 ++++ pom.xml | 12 ++ .../amigoscode/product/ProductRepository.java | 5 + .../amigoscode/product/ProductService.java | 5 +- .../amigoscode/SharedPostgresContainer.java | 42 +++++++ .../product/ProductRepositoryTest.java | 95 ++++++++++++++ .../product/ProductServiceTest.java | 117 ++++++++++++++++++ 7 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 docker-compose-db-only.yml create mode 100644 src/test/java/com/amigoscode/SharedPostgresContainer.java create mode 100644 src/test/java/com/amigoscode/product/ProductRepositoryTest.java create mode 100644 src/test/java/com/amigoscode/product/ProductServiceTest.java diff --git a/docker-compose-db-only.yml b/docker-compose-db-only.yml new file mode 100644 index 0000000..abbc1fe --- /dev/null +++ b/docker-compose-db-only.yml @@ -0,0 +1,22 @@ +services: + db-local-postgres: + container_name: jfs-postgres-local + image: postgres + environment: + POSTGRES_USER: amigoscode + POSTGRES_PASSWORD: password + POSTGRES_DB: jfs + ports: + - "5333:5432" + restart: unless-stopped + volumes: + - db-local:/data/postgres + networks: + - amigos + +networks: + amigos: + driver: bridge + +volumes: + db-local: \ No newline at end of file diff --git a/pom.xml b/pom.xml index 87d8f49..ba3a809 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,18 @@ org.flywaydb flyway-database-postgresql + + org.testcontainers + junit-jupiter + + + org.testcontainers + postgresql + + + org.springframework.boot + spring-boot-testcontainers + diff --git a/src/main/java/com/amigoscode/product/ProductRepository.java b/src/main/java/com/amigoscode/product/ProductRepository.java index 0762e60..fc88d02 100644 --- a/src/main/java/com/amigoscode/product/ProductRepository.java +++ b/src/main/java/com/amigoscode/product/ProductRepository.java @@ -1,9 +1,14 @@ package com.amigoscode.product; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.UUID; public interface ProductRepository extends JpaRepository { + + @Query("SELECT p FROM Product p WHERE p.isPublished AND p.stockLevel > 0 ORDER BY p.price ASC") + List findAvailablePublishedProducts(); } diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index 9d13815..15c35d9 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -2,9 +2,6 @@ import com.amigoscode.exception.ResourceNotFound; import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; import java.util.List; import java.util.UUID; @@ -57,7 +54,7 @@ public UUID saveNewProduct(NewProductRequest product) { return id; } - private Function mapToResponse() { + Function mapToResponse() { return p -> new ProductResponse( p.getId(), p.getName(), diff --git a/src/test/java/com/amigoscode/SharedPostgresContainer.java b/src/test/java/com/amigoscode/SharedPostgresContainer.java new file mode 100644 index 0000000..83ce976 --- /dev/null +++ b/src/test/java/com/amigoscode/SharedPostgresContainer.java @@ -0,0 +1,42 @@ +package com.amigoscode; + +import org.flywaydb.core.Flyway; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +public class SharedPostgresContainer extends PostgreSQLContainer { + private static final DockerImageName IMAGE_NAME + = DockerImageName.parse("postgres:17-alpine"); + + private static volatile SharedPostgresContainer sharedPostgresContainer; + + public SharedPostgresContainer(DockerImageName dockerImageName) { + super(dockerImageName); + this.withReuse(true) + .withUsername("amigoscode") + .withDatabaseName("amigos") + .withLabel("name", "amigscode") + .withPassword("password"); + } + + public static SharedPostgresContainer getInstance() { + if(sharedPostgresContainer == null) { + synchronized (SharedPostgresContainer.class) { + sharedPostgresContainer = new SharedPostgresContainer( + IMAGE_NAME + ); + sharedPostgresContainer.start(); + Flyway flyway = Flyway.configure() + .dataSource( + sharedPostgresContainer.getJdbcUrl(), + sharedPostgresContainer.getUsername(), + sharedPostgresContainer.getPassword() + ) + .load(); + flyway.migrate(); + System.out.println("flyway applied migrations"); + } + } + return sharedPostgresContainer; + } +} diff --git a/src/test/java/com/amigoscode/product/ProductRepositoryTest.java b/src/test/java/com/amigoscode/product/ProductRepositoryTest.java new file mode 100644 index 0000000..96ee6ad --- /dev/null +++ b/src/test/java/com/amigoscode/product/ProductRepositoryTest.java @@ -0,0 +1,95 @@ +package com.amigoscode.product; + +import com.amigoscode.SharedPostgresContainer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase( + replace = AutoConfigureTestDatabase.Replace.NONE +) +@Testcontainers +class ProductRepositoryTest { + + @Container + @ServiceConnection + private static final SharedPostgresContainer POSTGRES = + SharedPostgresContainer.getInstance(); + + + @Autowired + private ProductRepository underTest; + + @BeforeEach + void setUp() { + underTest.deleteAll(); + } + + @Test + void canFindAvailablePublishedProducts() { + // given + Product product1 = new Product( + UUID.randomUUID(), + "iphone", + "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank", + new BigDecimal("1000"), + "https://amigoscode.com/logo.png", + 10 + ); + + Product product2 = new Product( + UUID.randomUUID(), + "samsung", + "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank", + new BigDecimal("1200"), + "https://amigoscode.com/logo.png", + 5 + ); + product2.setPublished(false); + + Product product3 = new Product( + UUID.randomUUID(), + "watch", + "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank", + new BigDecimal("5000"), + "https://amigoscode.com/logo.png", + 0 + ); + + Product product4 = new Product( + UUID.randomUUID(), + "PS5", + "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank", + new BigDecimal("300"), + "https://amigoscode.com/logo.png", + 90 + ); + + underTest.saveAll( + List.of(product1, product2, product3, product4) + ); + // when + List availablePublishedProducts = + underTest.findAvailablePublishedProducts(); + // then + assertThat(availablePublishedProducts) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields( + "updatedAt", "createdAt" + ) + .containsExactly( + product4, product1 + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/amigoscode/product/ProductServiceTest.java b/src/test/java/com/amigoscode/product/ProductServiceTest.java new file mode 100644 index 0000000..c5b3f31 --- /dev/null +++ b/src/test/java/com/amigoscode/product/ProductServiceTest.java @@ -0,0 +1,117 @@ +package com.amigoscode.product; + +import com.amigoscode.SharedPostgresContainer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Import; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase( + replace = AutoConfigureTestDatabase.Replace.NONE +) +@Import({ + ProductService.class +}) +@Testcontainers +class ProductServiceTest { + + @Container + @ServiceConnection + private static final SharedPostgresContainer POSTGRES = + SharedPostgresContainer.getInstance(); + + @Autowired + private ProductRepository productRepository; + + @Autowired + private ProductService underTest; + + @BeforeAll + static void beforeAll() { + System.out.println(POSTGRES.getDatabaseName()); + System.out.println(POSTGRES.getJdbcUrl()); + System.out.println(POSTGRES.getPassword()); + System.out.println(POSTGRES.getDriverClassName()); + System.out.println(POSTGRES.getTestQueryString()); + } + + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + @Test + @Disabled + void canGetAllProducts() { + // given + Product product = new Product( + UUID.randomUUID(), + "foo", + "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank", + BigDecimal.TEN, + "https://amigoscode.com/logo.png", + 10 + ); + + productRepository.save(product); + + // when + List allProducts = + underTest.getAllProducts(); + // then + ProductResponse expected = underTest.mapToResponse().apply(product); + + assertThat(allProducts) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields( + "updatedAt", "createdAt" + ) + .containsOnly(expected); + + } + + @Test + @Disabled + void getProductById() { + // given + // when + // then + } + + @Test + @Disabled + void deleteProductById() { + // given + // when + // then + } + + @Test + @Disabled + void saveNewProduct() { + // given + // when + // then + } + + @Test + @Disabled + void updateProduct() { + // given + // when + // then + } +} \ No newline at end of file From 3a7ff6004ba2ea134d381f1cc4c671b891579375 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Thu, 26 Jun 2025 18:01:34 +0100 Subject: [PATCH 18/25] integration tests --- pom.xml | 25 ++ src/main/java/com/amigoscode/Main.java | 2 +- .../amigoscode/product/ProductController.java | 3 +- .../amigoscode/product/ProductResponse.java | 2 +- .../amigoscode/product/ProductService.java | 1 + src/main/resources/application.properties | 4 +- .../com/amigoscode/AbstractTestConfig.java | 20 ++ .../com/amigoscode/journey/ProductIT.java | 252 ++++++++++++++++++ 8 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/amigoscode/AbstractTestConfig.java create mode 100644 src/test/java/com/amigoscode/journey/ProductIT.java diff --git a/pom.xml b/pom.xml index ba3a809..3b304bb 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,11 @@ org.springframework.boot spring-boot-testcontainers + + org.springframework.boot + spring-boot-starter-webflux + test + @@ -116,6 +121,26 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IntegrationTest.java + **/*IT.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*IntegrationTest.java + **/*IT.java + + + diff --git a/src/main/java/com/amigoscode/Main.java b/src/main/java/com/amigoscode/Main.java index a17a7b6..2278728 100644 --- a/src/main/java/com/amigoscode/Main.java +++ b/src/main/java/com/amigoscode/Main.java @@ -17,7 +17,7 @@ public static void main(String[] args) { SpringApplication.run(Main.class, args); } - @Bean + // @Bean public CommandLineRunner commandLineRunner( ProductRepository productRepository) { return args -> { diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java index 42786c7..ef95609 100644 --- a/src/main/java/com/amigoscode/product/ProductController.java +++ b/src/main/java/com/amigoscode/product/ProductController.java @@ -1,7 +1,7 @@ package com.amigoscode.product; import jakarta.validation.Valid; -import org.springframework.validation.annotation.Validated; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -33,6 +33,7 @@ public void deleteProductById(@PathVariable("id") UUID id) { } @PostMapping + @ResponseStatus(HttpStatus.CREATED) public UUID saveProduct(@RequestBody @Valid NewProductRequest product) { return productService.saveNewProduct(product); } diff --git a/src/main/java/com/amigoscode/product/ProductResponse.java b/src/main/java/com/amigoscode/product/ProductResponse.java index 9df785a..8ff5d16 100644 --- a/src/main/java/com/amigoscode/product/ProductResponse.java +++ b/src/main/java/com/amigoscode/product/ProductResponse.java @@ -11,7 +11,7 @@ public record ProductResponse( BigDecimal price, String imageUrl, Integer stockLevel, - Instant createdAt, + boolean isPublished, Instant createdAt, Instant updatedAt, Instant deletedAt ) { diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index 15c35d9..8bcfb9d 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -62,6 +62,7 @@ Function mapToResponse() { p.getPrice(), p.getImageUrl(), p.getStockLevel(), + p.getPublished(), p.getCreatedAt(), p.getUpdatedAt(), p.getDeletedAt() diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index feb5b79..0d37076 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ spring.application.name=jfs -spring.datasource.url=jdbc:postgresql://localhost:5432/jfs +spring.datasource.url=jdbc:postgresql://localhost:5333/jfs spring.datasource.username=amigoscode -spring.datasource.password= +spring.datasource.password=password spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=validate diff --git a/src/test/java/com/amigoscode/AbstractTestConfig.java b/src/test/java/com/amigoscode/AbstractTestConfig.java new file mode 100644 index 0000000..a9ff141 --- /dev/null +++ b/src/test/java/com/amigoscode/AbstractTestConfig.java @@ -0,0 +1,20 @@ +package com.amigoscode; + +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureWebTestClient +@Testcontainers +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public abstract class AbstractTestConfig { + + @Container + @ServiceConnection + private static final SharedPostgresContainer POSTGRES = + SharedPostgresContainer.getInstance(); +} diff --git a/src/test/java/com/amigoscode/journey/ProductIT.java b/src/test/java/com/amigoscode/journey/ProductIT.java new file mode 100644 index 0000000..dae920e --- /dev/null +++ b/src/test/java/com/amigoscode/journey/ProductIT.java @@ -0,0 +1,252 @@ +package com.amigoscode.journey; + +import com.amigoscode.AbstractTestConfig; +import com.amigoscode.product.NewProductRequest; +import com.amigoscode.product.ProductResponse; +import com.amigoscode.product.UpdateProductRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProductIT extends AbstractTestConfig { + + public static final String PRODUCT_BASE_URL = "api/v1/products"; + @Autowired + private WebTestClient webTestClient; + + @Test + void canCreateProduct() { + createProduct(new NewProductRequest( + "Laptop", + "1gb ram etc", + BigDecimal.TEN, + 100, + "https://amigoscode.com/laptop.png" + )); + } + + private UUID createProduct(NewProductRequest request) { + // when + return webTestClient + .post() + .uri(PRODUCT_BASE_URL) + .bodyValue(request) + .exchange() + .expectStatus() + .isCreated() + .expectBody(UUID.class) + .value(uuid -> assertThat(uuid).isNotNull()) + .returnResult() + .getResponseBody(); + } + + @Test + void canGetAllProducts() { + // given 1st product + NewProductRequest laptop = new NewProductRequest( + "Laptop", + "1gb ram etc", + new BigDecimal("10.00"), + 100, + "https://amigoscode.com/laptop.png" + ); + var laptopId = createProduct(laptop); + + // given 2nd product + NewProductRequest tv = new NewProductRequest( + "Tv", + "LED with crystal clear nano...", + new BigDecimal("1.00"), + 50, + "https://amigoscode.com/tv.png" + ); + var tvId = createProduct(tv); + + // when + List products = webTestClient.get() + .uri(PRODUCT_BASE_URL) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBodyList(new ParameterizedTypeReference() { + }) + .returnResult() + .getResponseBody(); + // then + var productsFromDb = products.stream() + .filter( + p -> p.id().equals(tvId) || p.id().equals(laptopId) + ) + .toList(); + + assertThat(productsFromDb) + .hasSize(2) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields( + "createdAt", "updatedAt", "deletedAt" + ) + .containsExactlyInAnyOrder( + new ProductResponse( + tvId, + tv.name(), + tv.description(), + tv.price(), + tv.imageUrl(), + tv.stockLevel(), + true, null, null, null + ), + new ProductResponse( + laptopId, + laptop.name(), + laptop.description(), + laptop.price(), + laptop.imageUrl(), + laptop.stockLevel(), + true, null, null, null + ) + ); + } + + @Test + void canGetProductById() { + // given + NewProductRequest laptop = new NewProductRequest( + "Laptop", + "1gb ram etc", + new BigDecimal("10.00"), + 100, + "https://amigoscode.com/laptop.png" + ); + var laptopId = createProduct(laptop); + + // when + ProductResponse responseBody = webTestClient.get() + .uri(PRODUCT_BASE_URL + "/" + laptopId) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody(new ParameterizedTypeReference() { + }) + .returnResult() + .getResponseBody(); + + // then + assertThat(responseBody) + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt", "deletedAt") + .isEqualTo( + new ProductResponse( + laptopId, + laptop.name(), + laptop.description(), + laptop.price(), + laptop.imageUrl(), + laptop.stockLevel(), + true, null, null, null + ) + ); + } + + @Test + void canGetDeleteProductById() { + // given + NewProductRequest laptop = new NewProductRequest( + "Laptop", + "1gb ram etc", + new BigDecimal("10.00"), + 100, + "https://amigoscode.com/laptop.png" + ); + var laptopId = createProduct(laptop); + + // when + webTestClient.delete() + .uri(PRODUCT_BASE_URL + "/" + laptopId) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .isEmpty(); + + // then + webTestClient.get() + .uri(PRODUCT_BASE_URL + "/" + laptopId) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isNotFound() + .expectBody() + .jsonPath("$.message").isEqualTo("product with id [" + laptopId +"] not found") + .jsonPath("$.error").isEqualTo("Not Found") + .jsonPath("$.statusCode").isEqualTo("404") + .jsonPath("$.path").isEqualTo("/api/v1/products/" + laptopId) + .jsonPath("$.fieldError").doesNotExist(); + +// {"message":"product with id [02682eec-6d8e-43ac-a5c8-60dd7bb6fa14] not found","error":"Not Found","statusCode":404,"path":"/api/v1/products/02682eec-6d8e-43ac-a5c8-60dd7bb6fa14","timestamp":"2025-06-26T16:00:01.854366Z","fieldErrors":null} + + + } + + @Test + void canUpdateProduct() { + // given + NewProductRequest laptop = new NewProductRequest( + "Laptop", + "1gb ram etc", + new BigDecimal("10.00"), + 100, + "https://amigoscode.com/laptop.png" + ); + var laptopId = createProduct(laptop); + + // when + webTestClient.put() + .uri(PRODUCT_BASE_URL + "/" + laptopId) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(new UpdateProductRequest( + null, null, null, new BigDecimal("200.00"), 500, false + )) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .isEmpty(); + // then + ProductResponse responseBody = webTestClient.get() + .uri(PRODUCT_BASE_URL + "/" + laptopId) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody(new ParameterizedTypeReference() { + }) + .returnResult() + .getResponseBody(); + + // then + assertThat(responseBody) + .usingRecursiveComparison() + .ignoringFields("createdAt", "updatedAt", "deletedAt") + .isEqualTo( + new ProductResponse( + laptopId, + laptop.name(), + laptop.description(), + new BigDecimal("200.00"), + laptop.imageUrl(), + 500, + false, null, null, null + ) + ); + } +} From 8f237f67932511aadb6e0d9490255ad35c087adb Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 12:14:08 +0100 Subject: [PATCH 19/25] build workflow --- .github/workflows/build.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4519a61 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,11 @@ +name: Build workflow +on: + pull_request: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Test + run: echo "Amigoscode" \ No newline at end of file From bb886966c6bc6484b38894cabc69258240b492c6 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 12:31:42 +0100 Subject: [PATCH 20/25] add checkout and jdk actions --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4519a61..8e4ff7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,5 +7,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Test - run: echo "Amigoscode" \ No newline at end of file + - name: Checkout + uses: actions/checkout@v4 + - name: JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Maven Clean Verify + run: mvn -B -ntb clean verify From 30aca28226c40299d9670fa7e20dfb885002a70f Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 12:33:00 +0100 Subject: [PATCH 21/25] fix typo --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8e4ff7f..b2e599a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,4 +15,4 @@ jobs: distribution: 'temurin' java-version: '21' - name: Maven Clean Verify - run: mvn -B -ntb clean verify + run: mvn -B -ntp clean verify From 446d06f6f253ccb9026761fcb3a3b478ca99f778 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 12:38:41 +0100 Subject: [PATCH 22/25] change name to foo --- src/main/java/com/amigoscode/product/ProductService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index 8bcfb9d..a1bb12a 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -57,7 +57,7 @@ public UUID saveNewProduct(NewProductRequest product) { Function mapToResponse() { return p -> new ProductResponse( p.getId(), - p.getName(), + "foo", p.getDescription(), p.getPrice(), p.getImageUrl(), From ecb02ddd2309a647d6d451017ba14466f5675540 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 12:46:05 +0100 Subject: [PATCH 23/25] fix bug --- src/main/java/com/amigoscode/product/ProductService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index a1bb12a..8bcfb9d 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -57,7 +57,7 @@ public UUID saveNewProduct(NewProductRequest product) { Function mapToResponse() { return p -> new ProductResponse( p.getId(), - "foo", + p.getName(), p.getDescription(), p.getPrice(), p.getImageUrl(), From 5d83483c8f79774d8a76e06c6aa98e77c5f0810d Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 13:09:39 +0100 Subject: [PATCH 24/25] introduce bug --- src/main/java/com/amigoscode/product/ProductService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index 8bcfb9d..dcc9ae8 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -51,7 +51,7 @@ public UUID saveNewProduct(NewProductRequest product) { product.stockLevel() ); productRepository.save(newProduct); - return id; + return null; } Function mapToResponse() { From 467588602ff1c0a9931239b86147cadbd3d42b3d Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Mon, 14 Jul 2025 13:11:26 +0100 Subject: [PATCH 25/25] fix bug --- src/main/java/com/amigoscode/product/ProductService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java index dcc9ae8..8bcfb9d 100644 --- a/src/main/java/com/amigoscode/product/ProductService.java +++ b/src/main/java/com/amigoscode/product/ProductService.java @@ -51,7 +51,7 @@ public UUID saveNewProduct(NewProductRequest product) { product.stockLevel() ); productRepository.save(newProduct); - return null; + return id; } Function mapToResponse() {