From f6f87f657a81ef9d5f00b6c761d25bab9072eb31 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 13 Jun 2025 22:44:40 -0600 Subject: [PATCH 01/26] BS successfully connected to DB --- pom.xml | 6 +- .../InventoryManagerController.java | 2 +- .../inventory_manager/domain/Product.java | 84 +------------------ .../repository/ProductRepository.java | 1 - .../service/ProductService.java | 2 +- .../service/ProductServiceImpl.java | 2 +- src/main/resources/application.properties | 6 ++ 7 files changed, 15 insertions(+), 88 deletions(-) diff --git a/pom.xml b/pom.xml index 38b1c16..170f2b7 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,11 @@ org.springframework.boot spring-boot-starter-web - + + com.oracle.database.jdbc + ojdbc8 + 21.3.0.0 + org.springframework.boot spring-boot-devtools diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 2c6311c..275aa34 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -1,6 +1,6 @@ package com.encorazone.inventory_manager.controller; -import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.repository.Product; import com.encorazone.inventory_manager.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/encorazone/inventory_manager/domain/Product.java b/src/main/java/com/encorazone/inventory_manager/domain/Product.java index e92d424..4e4c0a8 100644 --- a/src/main/java/com/encorazone/inventory_manager/domain/Product.java +++ b/src/main/java/com/encorazone/inventory_manager/domain/Product.java @@ -1,109 +1,27 @@ package com.encorazone.inventory_manager.domain; -import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -@Entity -@Table(name = "products") + public class Product { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 120) private String name; - @Column(nullable = false) private String category; - @Column(nullable = false) private BigDecimal unitPrice; private LocalDate expirationDate; - @Column(nullable = false) private Integer quantityInStock; - @Column(updatable = false) private LocalDateTime creationDate; private LocalDateTime updateDate; - @PrePersist - public void onCreate() { - creationDate = LocalDateTime.now(); - updateDate = creationDate; - } - - @PreUpdate - public void onUpdate() { - updateDate = LocalDateTime.now(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public BigDecimal getUnitPrice() { - return unitPrice; - } - - public void setUnitPrice(BigDecimal unitPrice) { - this.unitPrice = unitPrice; - } - - public LocalDate getExpirationDate() { - return expirationDate; - } - - public void setExpirationDate(LocalDate expirationDate) { - this.expirationDate = expirationDate; - } - - public Integer getQuantityInStock() { - return quantityInStock; - } - - public void setQuantityInStock(Integer quantityInStock) { - this.quantityInStock = quantityInStock; - } - - public LocalDateTime getCreationDate() { - return creationDate; - } - - public void setCreationDate(LocalDateTime creationDate) { - this.creationDate = creationDate; - } - - public LocalDateTime getUpdateDate() { - return updateDate; - } - public void setUpdateDate(LocalDateTime updateDate) { - this.updateDate = updateDate; - } } diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index c5ef7b9..42fd6f9 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -1,6 +1,5 @@ package com.encorazone.inventory_manager.repository; -import com.encorazone.inventory_manager.domain.Product; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepository { diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java index dbb58e2..aee56f7 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java @@ -1,6 +1,6 @@ package com.encorazone.inventory_manager.service; -import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.repository.Product; import java.util.List; import java.util.Optional; diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java index fa70a97..8e3430a 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java @@ -1,6 +1,6 @@ package com.encorazone.inventory_manager.service; -import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.repository.Product; import com.encorazone.inventory_manager.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9c813cc..0066a40 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=inventory-manager-spark + +spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1 +spring.datasource.username=#INVENTORY +spring.datasource.password=admin +spring.datasource.driver-class-name=oracle.jdbc.OracleDriver +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect From 9722586fc1e1ead946d46cf1bd944e5b7aee060e Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 13 Jun 2025 22:46:41 -0600 Subject: [PATCH 02/26] Modified Product object requirements --- .../InventoryManagerController.java | 2 +- .../inventory_manager/domain/Product.java | 91 ++++++++++++++++++- .../repository/ProductRepository.java | 1 + .../service/ProductService.java | 2 +- .../service/ProductServiceImpl.java | 2 +- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 275aa34..2c6311c 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -1,6 +1,6 @@ package com.encorazone.inventory_manager.controller; -import com.encorazone.inventory_manager.repository.Product; +import com.encorazone.inventory_manager.domain.Product; import com.encorazone.inventory_manager.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/encorazone/inventory_manager/domain/Product.java b/src/main/java/com/encorazone/inventory_manager/domain/Product.java index 4e4c0a8..a847480 100644 --- a/src/main/java/com/encorazone/inventory_manager/domain/Product.java +++ b/src/main/java/com/encorazone/inventory_manager/domain/Product.java @@ -1,27 +1,114 @@ package com.encorazone.inventory_manager.domain; +import jakarta.persistence.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.UUID; - +@Entity +@Table(name = "PRODUCTS") public class Product { - private Long id; + @Id + @GeneratedValue(generator = "uuid2") + @Column(name = "ID", columnDefinition = "RAW(16)") + private UUID id; + @Column(nullable = false, length = 120, name = "NAME") private String name; + @Column(nullable = false, name = "CATEGORY") private String category; + @Column(nullable = false, name = "UNIT_PRICE") private BigDecimal unitPrice; + @Column(name = "EXPIRATION_DATE") private LocalDate expirationDate; + @Column(nullable = false, name = "QUANTITY_IN_STOCK") private Integer quantityInStock; + @Column(updatable = false, name = "CREATION_DATE") private LocalDateTime creationDate; + @Column(name = "UPDATE_DATE") private LocalDateTime updateDate; + @PrePersist + public void onCreate() { + creationDate = LocalDateTime.now(); + updateDate = creationDate; + } + + @PreUpdate + public void onUpdate() { + updateDate = LocalDateTime.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 getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(BigDecimal unitPrice) { + this.unitPrice = unitPrice; + } + + public LocalDate getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(LocalDate expirationDate) { + this.expirationDate = expirationDate; + } + + public Integer getQuantityInStock() { + return quantityInStock; + } + + public void setQuantityInStock(Integer quantityInStock) { + this.quantityInStock = quantityInStock; + } + + public LocalDateTime getCreationDate() { + return creationDate; + } + + public void setCreationDate(LocalDateTime creationDate) { + this.creationDate = creationDate; + } + + public LocalDateTime getUpdateDate() { + return updateDate; + } + public void setUpdateDate(LocalDateTime updateDate) { + this.updateDate = updateDate; + } } diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index 42fd6f9..c5ef7b9 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -1,5 +1,6 @@ package com.encorazone.inventory_manager.repository; +import com.encorazone.inventory_manager.domain.Product; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepository { diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java index aee56f7..dbb58e2 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java @@ -1,6 +1,6 @@ package com.encorazone.inventory_manager.service; -import com.encorazone.inventory_manager.repository.Product; +import com.encorazone.inventory_manager.domain.Product; import java.util.List; import java.util.Optional; diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java index 8e3430a..fa70a97 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java @@ -1,6 +1,6 @@ package com.encorazone.inventory_manager.service; -import com.encorazone.inventory_manager.repository.Product; +import com.encorazone.inventory_manager.domain.Product; import com.encorazone.inventory_manager.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; From 131ec637470de98e7d1dc679a965c0ff2e2c2bd3 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 13 Jun 2025 23:11:23 -0600 Subject: [PATCH 03/26] Modified Product Modified Product's ID attribute to comply with database RAW Id type --- .../controller/InventoryManagerController.java | 8 +++++--- .../repository/ProductRepository.java | 4 +++- .../inventory_manager/service/ProductService.java | 11 ++++++++--- .../inventory_manager/service/ProductServiceImpl.java | 8 +++++--- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 2c6311c..0328bdd 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -5,7 +5,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; + import java.util.List; +import java.util.UUID; //@Controller @RestController @@ -30,21 +32,21 @@ public ResponseEntity create(@RequestBody Product product) { } @PutMapping("/{id}") - public ResponseEntity update(@PathVariable Long id, @RequestBody Product product) { + public ResponseEntity update(@PathVariable UUID id, @RequestBody Product product) { return productService.update(id, product) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping("/{id}/outofstock") - public ResponseEntity markOutOfStock(@PathVariable Long id) { + public ResponseEntity markOutOfStock(@PathVariable UUID id) { return productService.markOutOfStock(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PutMapping("/{id}/instock") - public ResponseEntity restoreStock(@PathVariable Long id) { + public ResponseEntity restoreStock(@PathVariable UUID id) { return productService.restoreStock(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index c5ef7b9..ab4c31e 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -3,5 +3,7 @@ import com.encorazone.inventory_manager.domain.Product; import org.springframework.data.jpa.repository.JpaRepository; -public interface ProductRepository extends JpaRepository { +import java.util.UUID; + +public interface ProductRepository extends JpaRepository { } diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java index dbb58e2..9aefa53 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java @@ -4,11 +4,16 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; public interface ProductService { List getAll(String filter, String sort, int page, int size); + Product create(Product product); - Optional update(Long id, Product product); - Optional markOutOfStock(Long id); - Optional restoreStock(Long id); + + Optional update(UUID id, Product product); + + Optional markOutOfStock(UUID id); + + Optional restoreStock(UUID id); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java index fa70a97..9f95fa0 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java @@ -5,8 +5,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; + import java.util.List; import java.util.Optional; +import java.util.UUID; @Service public class ProductServiceImpl implements ProductService { @@ -26,7 +28,7 @@ public Product create(Product product) { } @Override - public Optional update(Long id, Product newProduct) { + public Optional update(UUID id, Product newProduct) { return productRepository.findById(id).map(existing -> { existing.setName(newProduct.getName()); existing.setCategory(newProduct.getCategory()); @@ -38,7 +40,7 @@ public Optional update(Long id, Product newProduct) { } @Override - public Optional markOutOfStock(Long id) { + public Optional markOutOfStock(UUID id) { return productRepository.findById(id).map(product -> { product.setQuantityInStock(0); return productRepository.save(product); @@ -46,7 +48,7 @@ public Optional markOutOfStock(Long id) { } @Override - public Optional restoreStock(Long id) { + public Optional restoreStock(UUID id) { return productRepository.findById(id).map(product -> { product.setQuantityInStock(10); // Default restore value return productRepository.save(product); From d65d4062cb48aa9e3d79894df45c5f848275c672 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sun, 15 Jun 2025 23:43:56 -0600 Subject: [PATCH 04/26] Business service updated BS is operative now. Complying to the requirements. Changing the version to 1.0.0, although is not going tio be necessary. Added javadocs comments for documentation. Nice to have in the feature: *Availability to restore the stock using web browser cache, or something like that. Probably cookies of some sort. --- pom.xml | 7 +- .../InventoryManagerController.java | 78 ++++++++++++++++--- .../inventory_manager/domain/Product.java | 12 +-- .../domain/ProductFilter.java | 42 ++++++++++ .../repository/ProductRepository.java | 21 ++++- .../service/FilteredSearch.java | 47 +++++++++++ .../service/ProductService.java | 48 +++++++++++- .../service/ProductServiceImpl.java | 28 +++++-- 8 files changed, 256 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java create mode 100644 src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java diff --git a/pom.xml b/pom.xml index 170f2b7..1137c0f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.encorazone inventory-manager - 0.0.1-SNAPSHOT + 1.0.0 inventory-manager-spark Demo project for Spring Boot @@ -72,7 +72,10 @@ springdoc-openapi-starter-webmvc-ui 2.3.0 - + + org.hibernate.validator + hibernate-validator + diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 0328bdd..909e56e 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -2,14 +2,17 @@ import com.encorazone.inventory_manager.domain.Product; import com.encorazone.inventory_manager.service.ProductService; + +import org.springdoc.core.annotations.ParameterObject; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; -//@Controller +@CrossOrigin(origins = "http://localhost:3000") @RestController @RequestMapping("/products") final class InventoryManagerController { @@ -17,20 +20,61 @@ final class InventoryManagerController { @Autowired private ProductService productService; + /** + * Edpoin to get all the elemnts from database, no sorting nor filtering + * Just pagination for the client usability + * + * @param page represents the page for the table. Example 0. + * @param size represents the number of elements per page, default is 10. Example 20. + * @return response status amd a list containing the pagexsize elements + */ @GetMapping public ResponseEntity> getAll( - @RequestParam(required = false) String filter, - @RequestParam(required = false) String sort, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(productService.getAll(filter, sort, page, size)); + @RequestParam(required = false) int page, + @RequestParam(required = false, defaultValue = "10") int size) { + return ResponseEntity.ok(productService.getAll(page, size)); + } + + /** + * Endpoint for filtered data retrieving, including name, category and availability + * filtering from DB objects. + * + * @param name represent the name of the product. Example, Watermelon. + * @param category represents the category the element is part of; like food. + * @param stockQuantity represents the amount of elements in the inventory. Example 10. + * @param pageable Builtin object for sorting and pagination, the API asks for the json by itself + * @return responsse status and a list containing the pagexsixe elements + * complying with the sort and filter parameters + */ + @GetMapping("/filters") + public ResponseEntity> findByFilter( + @ModelAttribute @RequestParam(required = false) String name, + @ModelAttribute @RequestParam(required = false) String category, + @ModelAttribute @RequestParam(required = false) Integer stockQuantity, + @ParameterObject Pageable pageable) { + return ResponseEntity.ok(productService.findByNameAndCategoryAndStockQuantity( + name, category, stockQuantity, pageable)); } + /** + * Endpoint to create a new product + * + * @param product object representing the product to be added to the inventory + * @return status. Example 200(OK) + */ @PostMapping public ResponseEntity create(@RequestBody Product product) { return ResponseEntity.ok(productService.create(product)); } + /** + * endpoint to update a product + * + * @param id represents the DB/BS internal id fro managing elements. + * Example 785c0229-b7e5-4ea4-853b-fa5ad4eb84f4 + * @param product Object with the changes fto be made to the product + * @return status. Example 500 (Internal server error) + */ @PutMapping("/{id}") public ResponseEntity update(@PathVariable UUID id, @RequestBody Product product) { return productService.update(id, product) @@ -38,16 +82,30 @@ public ResponseEntity update(@PathVariable UUID id, @RequestBody Produc .orElse(ResponseEntity.notFound().build()); } - @PostMapping("/{id}/outofstock") + /** + * endpoint to automatically set stock to 0 + * + * @param id Represents the id of the element we want the stock to be 0 + * @return status. Example 200 + */ + @PatchMapping("/{id}/outofstock") public ResponseEntity markOutOfStock(@PathVariable UUID id) { return productService.markOutOfStock(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } - @PutMapping("/{id}/instock") - public ResponseEntity restoreStock(@PathVariable UUID id) { - return productService.restoreStock(id) + /** + * endpoint to automatically set stock to a given number, By default 10 + * + * @param id Represents the id of the element we want the stock to be 0 + * @param stockQuantity Represents the amount to put into stock. Example 10 + * @return status. Example 200(OK) + */ + @PatchMapping("/{id}/instock") + public ResponseEntity restoreStock(@PathVariable UUID id, + @RequestParam(defaultValue = "10") Integer stockQuantity) { + return productService.restoreStock(id, stockQuantity) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } diff --git a/src/main/java/com/encorazone/inventory_manager/domain/Product.java b/src/main/java/com/encorazone/inventory_manager/domain/Product.java index a847480..35e8fec 100644 --- a/src/main/java/com/encorazone/inventory_manager/domain/Product.java +++ b/src/main/java/com/encorazone/inventory_manager/domain/Product.java @@ -28,8 +28,8 @@ public class Product { @Column(name = "EXPIRATION_DATE") private LocalDate expirationDate; - @Column(nullable = false, name = "QUANTITY_IN_STOCK") - private Integer quantityInStock; + @Column(nullable = false, name = "STOCK_QUANTITY") + private Integer stockQuantity; @Column(updatable = false, name = "CREATION_DATE") private LocalDateTime creationDate; @@ -88,12 +88,12 @@ public void setExpirationDate(LocalDate expirationDate) { this.expirationDate = expirationDate; } - public Integer getQuantityInStock() { - return quantityInStock; + public Integer getStockQuantity() { + return stockQuantity; } - public void setQuantityInStock(Integer quantityInStock) { - this.quantityInStock = quantityInStock; + public void setStockQuantity(Integer stockQuantity) { + this.stockQuantity = stockQuantity; } public LocalDateTime getCreationDate() { diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java new file mode 100644 index 0000000..d344b60 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java @@ -0,0 +1,42 @@ +package com.encorazone.inventory_manager.domain; + +import org.springframework.data.jpa.domain.Specification; + +public class ProductFilter { + + /** + * Creates a specification for filtering products whose names contain the given substring, + * + * @param name the name substring to search for; if null or blank, no filter is applied + * @return a Specification for matching product names, or null if the input is invalid + */ + public static Specification nameContains(String name) { + return (root, query, cb) -> + name == null || name.isBlank() ? null : + cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"); + } + + /** + * Creates a specification for filtering products where the category contain the given substring, + * + * @param category the name substring to search for; if null or blank, no filter is applied + * @return a Specification for matching product categories, or null if the input is invalid + */ + public static Specification categoryContains(String category) { + return (root, query, cb) -> + category == null || category.isBlank() ? null : + cb.like(cb.lower(root.get("category")), "%" + category.toLowerCase() + "%"); + } + + /** + * Creates a specification for filtering products that have exactly the specified stock quantity. + * + * @param stock the stock quantity to match; if null, no filter is applied + * @return a spec for matching stock quantities, or a null if the input is {@code null} + */ + public static Specification quantityEquals(Integer stock) { + return (root, query, cb) -> + stock == null ? null : + cb.equal(root.get("stockQuantity"), stock); + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index ab4c31e..c583684 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -1,9 +1,28 @@ package com.encorazone.inventory_manager.repository; import com.encorazone.inventory_manager.domain.Product; + +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import java.util.List; import java.util.UUID; -public interface ProductRepository extends JpaRepository { +public interface ProductRepository extends JpaRepository, JpaSpecificationExecutor { + /** + * This method was my second try to filter and sort data. It filters well, + * cannot sort, explained better on the DEMO if I don't forget, Probably will. + * Basically the method was filtering always the availability, + * setting the stockQuantity to 0, meaning if the product had a stock different + * to 0, It wouldn't appear in the resulting list. + * + * @param name name + * @param category category + * @param quantityInStock availability of the product + * @param pageable objet for sorting and pagination + * @return a list of product matchin the description + */ + List findByNameContainingIgnoreCaseAndCategoryContainingIgnoreCaseAndStockQuantity( + String name, String category, Integer quantityInStock, Pageable pageable); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java b/src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java new file mode 100644 index 0000000..0eadc07 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java @@ -0,0 +1,47 @@ +package com.encorazone.inventory_manager.service; + +/** + * This class represents the first try to sorting and filtering the data. + * Should have deleted it, but le agarré cariño. Not gonna explain the method though. + * The method explain themselves + */ +public class FilteredSearch { + public Number attributesCounter(String name, String category, Number stock) { + int num = 0; + if (!name.isEmpty()) { + num++; + } + if (!category.isEmpty()) { + num++; + } + if (stock.equals(0)) { + num++; + } + return num; + } + + public static Number attributeFilter(String name, String category, Number stock) { + if (name != null) { + if (category != null) { + if (stock != null) { + return 1; + } + return 2; + } else if (stock != null) { + return 3; + } else { + return 4; + } + } else if (category != null) { + if (stock != null) { + return 5; + } else { + return 6; + } + } else if (stock != null) { + return 7; + } else { + return 0; + } + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java index 9aefa53..b6715e3 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java @@ -2,18 +2,62 @@ import com.encorazone.inventory_manager.domain.Product; +import org.springframework.data.domain.Pageable; + import java.util.List; import java.util.Optional; import java.util.UUID; public interface ProductService { - List getAll(String filter, String sort, int page, int size); + /** + * method to get all the elemnts from database, no sorting nor filtering + * Just pagination for the client usability + * @param page represents the page for the table. Example 0. + * @param size represents the number of elements per page, default is 10. Example 20. + * @return a list containing the pagexsize elements + */ + List getAll(int page, int size); + + /** + * Method to create a new product and save it + * @param product object representing the product to be added to the inventory + * @return the product creeated + */ Product create(Product product); + /** + * Updates an existing product identified by the given ID. + * @param id the UUID of the product to update + * @param product the updated product data + * @return an Optional containing the updated product if found, or empty if not found + */ Optional update(UUID id, Product product); + /** + * Method to automatically set stock to 0 + * @param id Represents the id of the element we want the stock to be 0 + * @return an optional containing the updated product if the operation succeeded, or empty if not found + */ Optional markOutOfStock(UUID id); - Optional restoreStock(UUID id); + /** + * method to automatically set stock to the given number + * @param id Represents the id of the element we want the stock to be 0 + * @param stock Represents the amount to put into stock. Example 10 + * @return n optional containing the updated product if the operation succeeded, or empty if not found + */ + Optional restoreStock(UUID id, Integer stock); + + /** + * Endpoint for filtered data retrieving, including name, category and availability + * filtering from DB objects. + * @param name represent the name of the product (or part of it). Example, Watermelon. + * @param category represents the category the element is part of; like food. + * @param stockQuantity represents the amount of elements in the inventory. Example 10. + * @param pageable Builtin object for sorting and pagination, the API asks for the json by itself + * @return a list of products matching the criteria + */ + List findByNameAndCategoryAndStockQuantity(String name, String category, + Integer stockQuantity, Pageable pageable); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java index 9f95fa0..226cbcc 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java @@ -1,9 +1,14 @@ package com.encorazone.inventory_manager.service; import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.ProductFilter; import com.encorazone.inventory_manager.repository.ProductRepository; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import java.util.List; @@ -17,8 +22,7 @@ public class ProductServiceImpl implements ProductService { private ProductRepository productRepository; @Override - public List getAll(String filter, String sort, int page, int size) { - // Simplified: no filter/sort logic, just pagination + public List getAll(int page, int size) { return productRepository.findAll(PageRequest.of(page, size)).getContent(); } @@ -34,7 +38,7 @@ public Optional update(UUID id, Product newProduct) { existing.setCategory(newProduct.getCategory()); existing.setUnitPrice(newProduct.getUnitPrice()); existing.setExpirationDate(newProduct.getExpirationDate()); - existing.setQuantityInStock(newProduct.getQuantityInStock()); + existing.setStockQuantity(newProduct.getStockQuantity()); return productRepository.save(existing); }); } @@ -42,16 +46,28 @@ public Optional update(UUID id, Product newProduct) { @Override public Optional markOutOfStock(UUID id) { return productRepository.findById(id).map(product -> { - product.setQuantityInStock(0); + product.setStockQuantity(0); return productRepository.save(product); }); } @Override - public Optional restoreStock(UUID id) { + public Optional restoreStock(UUID id, Integer stock) { return productRepository.findById(id).map(product -> { - product.setQuantityInStock(10); // Default restore value + product.setStockQuantity(10); return productRepository.save(product); }); } + + @Override + public List findByNameAndCategoryAndStockQuantity(String name, String category, + Integer stockQuantity, Pageable pageable) { + Specification spec = ProductFilter.nameContains(name) + .and(ProductFilter.categoryContains(category)) + .and(ProductFilter.quantityEquals(stockQuantity)); + + Page page = productRepository.findAll(spec, pageable); + return page.getContent(); + } + } From 83276bef0a5a4510d7be4999c671a691579053b3 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 12:37:55 -0600 Subject: [PATCH 05/26] Moved hardcoded versions Modified pom.xml: - Moved hardcoded versions to the properties block --- pom.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1137c0f..0306d17 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ inventory-manager 1.0.0 inventory-manager-spark - Demo project for Spring Boot + Inventory management application @@ -28,6 +28,8 @@ 17 + 21.3.0.0 + 2.3.0 @@ -49,7 +51,7 @@ com.oracle.database.jdbc ojdbc8 - 21.3.0.0 + ${ojdbc.version} org.springframework.boot @@ -70,7 +72,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.3.0 + ${springdoc.version} org.hibernate.validator From df8e90aab756b08c5c1d5c1556bc12721147d275 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 12:40:31 -0600 Subject: [PATCH 06/26] Added lombok dependency to the project Modified pom.xml: - Added lombok dependency (version:1.18.38) to manage getters & setters --- pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0306d17..ae7d970 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent 3.5.0 - + com.encorazone inventory-manager @@ -30,6 +30,7 @@ 17 21.3.0.0 2.3.0 + 1.18.38 @@ -78,6 +79,11 @@ org.hibernate.validator hibernate-validator + + org.projectlombok + lombok + ${lombok.version} + From 5453e4aef72ac0b2e590a8625f604b046d0d9aae Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 12:44:57 -0600 Subject: [PATCH 07/26] Deleted getters & setters Modified Product.java: - Deleted getters & setters - Enabled lombok dependency to keep the code clean - ...Next project will use ID instead of UUID --- .../inventory_manager/domain/Product.java | 64 +------------------ 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/encorazone/inventory_manager/domain/Product.java b/src/main/java/com/encorazone/inventory_manager/domain/Product.java index 35e8fec..c056eb2 100644 --- a/src/main/java/com/encorazone/inventory_manager/domain/Product.java +++ b/src/main/java/com/encorazone/inventory_manager/domain/Product.java @@ -1,12 +1,14 @@ package com.encorazone.inventory_manager.domain; import jakarta.persistence.*; +import lombok.Data; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; +@Data @Entity @Table(name = "PRODUCTS") public class Product { @@ -48,67 +50,5 @@ public void onUpdate() { updateDate = LocalDateTime.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 getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public BigDecimal getUnitPrice() { - return unitPrice; - } - - public void setUnitPrice(BigDecimal unitPrice) { - this.unitPrice = unitPrice; - } - public LocalDate getExpirationDate() { - return expirationDate; - } - - public void setExpirationDate(LocalDate expirationDate) { - this.expirationDate = expirationDate; - } - - public Integer getStockQuantity() { - return stockQuantity; - } - - public void setStockQuantity(Integer stockQuantity) { - this.stockQuantity = stockQuantity; - } - - public LocalDateTime getCreationDate() { - return creationDate; - } - - public void setCreationDate(LocalDateTime creationDate) { - this.creationDate = creationDate; - } - - public LocalDateTime getUpdateDate() { - return updateDate; - } - - public void setUpdateDate(LocalDateTime updateDate) { - this.updateDate = updateDate; - } } From f31818e953a774161a36de53c9fc420fa9e5d0eb Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 12:48:59 -0600 Subject: [PATCH 08/26] Changed to .yml for the properties file * Deleted application.properties * Created application.yml --- src/main/resources/application.properties | 7 ------- src/main/resources/application.yml | 12 ++++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 0066a40..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,7 +0,0 @@ -spring.application.name=inventory-manager-spark - -spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1 -spring.datasource.username=#INVENTORY -spring.datasource.password=admin -spring.datasource.driver-class-name=oracle.jdbc.OracleDriver -spring.jpa.database-platform=org.hibernate.dialect.OracleDialect diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..687c0cc --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,12 @@ +spring: + application: + name: inventory-manager-spark + + datasource: + url: jdbc:oracle:thin:@localhost:1521/XEPDB1 + username: '#INVENTORY' + password: admin + driver-class-name: oracle.jdbc.OracleDriver + + jpa: + database-platform: org.hibernate.dialect.OracleDialect From faa56da95d67729ee56f64cfeafddc064cc96b9c Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 12:50:38 -0600 Subject: [PATCH 09/26] Deleted unused class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Deleted unused FilteredSearch.java. Following the YAGNI principle. Wasn´t needed --- .../service/FilteredSearch.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java diff --git a/src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java b/src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java deleted file mode 100644 index 0eadc07..0000000 --- a/src/main/java/com/encorazone/inventory_manager/service/FilteredSearch.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.encorazone.inventory_manager.service; - -/** - * This class represents the first try to sorting and filtering the data. - * Should have deleted it, but le agarré cariño. Not gonna explain the method though. - * The method explain themselves - */ -public class FilteredSearch { - public Number attributesCounter(String name, String category, Number stock) { - int num = 0; - if (!name.isEmpty()) { - num++; - } - if (!category.isEmpty()) { - num++; - } - if (stock.equals(0)) { - num++; - } - return num; - } - - public static Number attributeFilter(String name, String category, Number stock) { - if (name != null) { - if (category != null) { - if (stock != null) { - return 1; - } - return 2; - } else if (stock != null) { - return 3; - } else { - return 4; - } - } else if (category != null) { - if (stock != null) { - return 5; - } else { - return 6; - } - } else if (stock != null) { - return 7; - } else { - return 0; - } - } -} From 237672371bf576e583e543af7b0f47904ab3f230 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 17:36:38 -0600 Subject: [PATCH 10/26] New method to delete products Modified ProductServiceImpl.java - Added method/logic to delete products. * Products are DELETED as no safe-delete was needed in this project --- .../controller/InventoryManagerController.java | 12 ++++++++++++ .../service/ProductServiceImpl.java | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 909e56e..3ab9e4d 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -109,4 +109,16 @@ public ResponseEntity restoreStock(@PathVariable UUID id, .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } + + /** + * Endpoint to delete products + * + * @param id Represemts tje id of the element to be deleted + * @return status code + */ + @DeleteMapping("/{id}") + public ResponseEntity deleteProduct(@PathVariable UUID id) { + productService.delete(id); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java index 226cbcc..36a91a6 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java @@ -43,6 +43,15 @@ public Optional update(UUID id, Product newProduct) { }); } + @Override + public void delete(UUID id) { + if (productRepository.existsById(id)) { + productRepository.deleteById(id); + } else { + throw new NoSuchElementException("Id " + id + "not found in db"); + } + } + @Override public Optional markOutOfStock(UUID id) { return productRepository.findById(id).map(product -> { From 10c7eaae1a4053a53c113ade85555021732629a2 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 18:15:31 -0600 Subject: [PATCH 11/26] Updated the body type of the endpoints response entities with a DTO ProductServiceImpl.java: - Removed hardcoded value from updateStock method - Added condition to avoid a db call to save the same information ProductService.java & InventoryManagerController.java: - Updated method signatures to return a DTO to prevent exposing sensitive data. No multiple layers needed. ProductFilter.java: - Updated quantityEquals method to comply with client requests --- .../InventoryManagerController.java | 11 ++++--- .../domain/ProductResponse.java | 20 ++++++++++++ .../mapper/ProductMapper.java | 15 +++++++++ .../{domain => service}/ProductFilter.java | 22 +++++++++---- .../service/ProductService.java | 32 +++++++++++++------ .../service/ProductServiceImpl.java | 30 ++++++++++------- 6 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java create mode 100644 src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java rename src/main/java/com/encorazone/inventory_manager/{domain => service}/ProductFilter.java (65%) diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 3ab9e4d..ca48758 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -1,6 +1,7 @@ package com.encorazone.inventory_manager.controller; import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.ProductResponse; import com.encorazone.inventory_manager.service.ProductService; import org.springdoc.core.annotations.ParameterObject; @@ -63,7 +64,7 @@ public ResponseEntity> findByFilter( * @return status. Example 200(OK) */ @PostMapping - public ResponseEntity create(@RequestBody Product product) { + public ResponseEntity create(@RequestBody Product product) { return ResponseEntity.ok(productService.create(product)); } @@ -76,7 +77,7 @@ public ResponseEntity create(@RequestBody Product product) { * @return status. Example 500 (Internal server error) */ @PutMapping("/{id}") - public ResponseEntity update(@PathVariable UUID id, @RequestBody Product product) { + public ResponseEntity update(@PathVariable UUID id, @RequestBody Product product) { return productService.update(id, product) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -89,7 +90,7 @@ public ResponseEntity update(@PathVariable UUID id, @RequestBody Produc * @return status. Example 200 */ @PatchMapping("/{id}/outofstock") - public ResponseEntity markOutOfStock(@PathVariable UUID id) { + public ResponseEntity markOutOfStock(@PathVariable UUID id) { return productService.markOutOfStock(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -103,9 +104,9 @@ public ResponseEntity markOutOfStock(@PathVariable UUID id) { * @return status. Example 200(OK) */ @PatchMapping("/{id}/instock") - public ResponseEntity restoreStock(@PathVariable UUID id, + public ResponseEntity restoreStock(@PathVariable UUID id, @RequestParam(defaultValue = "10") Integer stockQuantity) { - return productService.restoreStock(id, stockQuantity) + return productService.updateStock(id, stockQuantity) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java new file mode 100644 index 0000000..9f1208f --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java @@ -0,0 +1,20 @@ +package com.encorazone.inventory_manager.domain; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class ProductResponse { + private UUID id; + + private String name; + + private LocalDateTime creationDate; + + private LocalDateTime updateDate; + + public ProductResponse(UUID id, String name, LocalDateTime creationDate, LocalDateTime updateDate) { + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java b/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java new file mode 100644 index 0000000..1e13958 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java @@ -0,0 +1,15 @@ +package com.encorazone.inventory_manager.mapper; + +import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.ProductResponse; + +public class ProductMapper { + public static ProductResponse toProductResponse(Product product) { + return new ProductResponse( + product.getId(), + product.getName(), + product.getCreationDate(), + product.getUpdateDate() + ); + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java b/src/main/java/com/encorazone/inventory_manager/service/ProductFilter.java similarity index 65% rename from src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java rename to src/main/java/com/encorazone/inventory_manager/service/ProductFilter.java index d344b60..735c3ca 100644 --- a/src/main/java/com/encorazone/inventory_manager/domain/ProductFilter.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductFilter.java @@ -1,5 +1,6 @@ -package com.encorazone.inventory_manager.domain; +package com.encorazone.inventory_manager.service; +import com.encorazone.inventory_manager.domain.Product; import org.springframework.data.jpa.domain.Specification; public class ProductFilter { @@ -29,14 +30,21 @@ public static Specification categoryContains(String category) { } /** - * Creates a specification for filtering products that have exactly the specified stock quantity. + * Creates a specification for filtering the availability of the products. * - * @param stock the stock quantity to match; if null, no filter is applied - * @return a spec for matching stock quantities, or a null if the input is {@code null} + * @param stock the stock quantity parameter: + * 0 -> don't filter, 1 -> only in stock, 2 -> Not in stock, 3 -> to reset status + * @return a spec for matching stock quantities, or a null if the input is null */ public static Specification quantityEquals(Integer stock) { - return (root, query, cb) -> - stock == null ? null : - cb.equal(root.get("stockQuantity"), stock); + return switch (stock) { + case 0, 3 -> (root, query, cb) -> + cb.greaterThanOrEqualTo(root.get("stockQuantity"), 0); + case 1 -> (root, query, cb) -> + cb.greaterThan(root.get("stockQuantity"), 0); + case 2 -> (root, query, cb) -> + cb.equal(root.get("stockQuantity"), 0); + default -> null; + }; } } diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java index b6715e3..440c8f8 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductService.java @@ -2,6 +2,7 @@ import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.ProductResponse; import org.springframework.data.domain.Pageable; import java.util.List; @@ -13,6 +14,7 @@ public interface ProductService { /** * method to get all the elemnts from database, no sorting nor filtering * Just pagination for the client usability + * * @param page represents the page for the table. Example 0. * @param size represents the number of elements per page, default is 10. Example 20. * @return a list containing the pagexsize elements @@ -21,41 +23,53 @@ public interface ProductService { /** * Method to create a new product and save it + * * @param product object representing the product to be added to the inventory * @return the product creeated */ - Product create(Product product); + ProductResponse create(Product product); /** * Updates an existing product identified by the given ID. - * @param id the UUID of the product to update + * + * @param id the UUID of the product to update * @param product the updated product data * @return an Optional containing the updated product if found, or empty if not found */ - Optional update(UUID id, Product product); + Optional update(UUID id, Product product); + + /** + * Method to delet product + * + * @param id Reoresents the id of the element to be deleted + */ + void delete(UUID id); /** * Method to automatically set stock to 0 + * * @param id Represents the id of the element we want the stock to be 0 * @return an optional containing the updated product if the operation succeeded, or empty if not found */ - Optional markOutOfStock(UUID id); + Optional markOutOfStock(UUID id); /** * method to automatically set stock to the given number - * @param id Represents the id of the element we want the stock to be 0 + * + * @param id Represents the id of the element we want the stock to be 0 * @param stock Represents the amount to put into stock. Example 10 * @return n optional containing the updated product if the operation succeeded, or empty if not found */ - Optional restoreStock(UUID id, Integer stock); + Optional updateStock(UUID id, Integer stock); /** * Endpoint for filtered data retrieving, including name, category and availability * filtering from DB objects. - * @param name represent the name of the product (or part of it). Example, Watermelon. - * @param category represents the category the element is part of; like food. + * + * @param name represent the name of the product (or part of it). Example, Watermelon. + * @param category represents the category the element is part of; like food. * @param stockQuantity represents the amount of elements in the inventory. Example 10. - * @param pageable Builtin object for sorting and pagination, the API asks for the json by itself + * @param pageable Builtin object for sorting and pagination, the API asks for the json by itself * @return a list of products matching the criteria */ List findByNameAndCategoryAndStockQuantity(String name, String category, diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java index 36a91a6..74a48d5 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java @@ -1,8 +1,9 @@ package com.encorazone.inventory_manager.service; import com.encorazone.inventory_manager.domain.Product; -import com.encorazone.inventory_manager.domain.ProductFilter; +import com.encorazone.inventory_manager.domain.ProductResponse; import com.encorazone.inventory_manager.repository.ProductRepository; +import com.encorazone.inventory_manager.mapper.ProductMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -12,6 +13,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.NoSuchElementException; import java.util.Optional; import java.util.UUID; @@ -27,12 +29,12 @@ public List getAll(int page, int size) { } @Override - public Product create(Product product) { - return productRepository.save(product); + public ProductResponse create(Product product) { + return ProductMapper.toProductResponse(productRepository.save(product)); } @Override - public Optional update(UUID id, Product newProduct) { + public Optional update(UUID id, Product newProduct) { return productRepository.findById(id).map(existing -> { existing.setName(newProduct.getName()); existing.setCategory(newProduct.getCategory()); @@ -40,7 +42,7 @@ public Optional update(UUID id, Product newProduct) { existing.setExpirationDate(newProduct.getExpirationDate()); existing.setStockQuantity(newProduct.getStockQuantity()); return productRepository.save(existing); - }); + }).map(ProductMapper::toProductResponse); } @Override @@ -53,19 +55,23 @@ public void delete(UUID id) { } @Override - public Optional markOutOfStock(UUID id) { + public Optional markOutOfStock(UUID id) { return productRepository.findById(id).map(product -> { - product.setStockQuantity(0); - return productRepository.save(product); - }); + if (product.getStockQuantity() > 0) { + product.setStockQuantity(0); + return productRepository.save(product); + } else { + return product; + } + }).map(ProductMapper::toProductResponse); } @Override - public Optional restoreStock(UUID id, Integer stock) { + public Optional updateStock(UUID id, Integer stock) { return productRepository.findById(id).map(product -> { - product.setStockQuantity(10); + product.setStockQuantity(stock); return productRepository.save(product); - }); + }).map(ProductMapper::toProductResponse); } @Override From 755ab542ce601a281cd0bb4a8791b573bddc6555 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Fri, 20 Jun 2025 21:24:38 -0600 Subject: [PATCH 12/26] Added feature to retrieve number of pages per data fetch --- .../InventoryManagerController.java | 40 +++++++++---------- .../domain/ProductListResponse.java | 17 ++++++++ .../domain/ProductResponse.java | 28 ++++++++++++- .../domain/ProductShortResponse.java | 28 +++++++++++++ .../domain/ProductSummaryResponse.java | 20 ++++++++++ .../mapper/ProductMapper.java | 26 ++++++++++++ ...lter.java => InventoryProductsFilter.java} | 2 +- ...ductService.java => InventoryService.java} | 20 +++++----- ...iceImpl.java => InventoryServiceImpl.java} | 39 +++++++++--------- .../service/InventorySummary.java | 4 ++ 10 files changed, 173 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/ProductListResponse.java create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/ProductShortResponse.java create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java rename src/main/java/com/encorazone/inventory_manager/service/{ProductFilter.java => InventoryProductsFilter.java} (98%) rename src/main/java/com/encorazone/inventory_manager/service/{ProductService.java => InventoryService.java} (77%) rename src/main/java/com/encorazone/inventory_manager/service/{ProductServiceImpl.java => InventoryServiceImpl.java} (57%) create mode 100644 src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index ca48758..335ce93 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -1,8 +1,9 @@ package com.encorazone.inventory_manager.controller; import com.encorazone.inventory_manager.domain.Product; -import com.encorazone.inventory_manager.domain.ProductResponse; -import com.encorazone.inventory_manager.service.ProductService; +import com.encorazone.inventory_manager.domain.ProductListResponse; +import com.encorazone.inventory_manager.domain.ProductShortResponse; +import com.encorazone.inventory_manager.service.InventoryService; import org.springdoc.core.annotations.ParameterObject; import org.springframework.beans.factory.annotation.Autowired; @@ -10,7 +11,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; import java.util.UUID; @CrossOrigin(origins = "http://localhost:3000") @@ -19,7 +19,7 @@ final class InventoryManagerController { @Autowired - private ProductService productService; + private InventoryService inventoryService; /** * Edpoin to get all the elemnts from database, no sorting nor filtering @@ -30,10 +30,10 @@ final class InventoryManagerController { * @return response status amd a list containing the pagexsize elements */ @GetMapping - public ResponseEntity> getAll( - @RequestParam(required = false) int page, + public ResponseEntity getAll( + @RequestParam(required = false, defaultValue = "0") int page, @RequestParam(required = false, defaultValue = "10") int size) { - return ResponseEntity.ok(productService.getAll(page, size)); + return ResponseEntity.ok(inventoryService.getAll(page, size)); } /** @@ -48,12 +48,12 @@ public ResponseEntity> getAll( * complying with the sort and filter parameters */ @GetMapping("/filters") - public ResponseEntity> findByFilter( + public ResponseEntity findByFilter( @ModelAttribute @RequestParam(required = false) String name, @ModelAttribute @RequestParam(required = false) String category, - @ModelAttribute @RequestParam(required = false) Integer stockQuantity, + @ModelAttribute @RequestParam(required = false, defaultValue = "0") Integer stockQuantity, @ParameterObject Pageable pageable) { - return ResponseEntity.ok(productService.findByNameAndCategoryAndStockQuantity( + return ResponseEntity.ok(inventoryService.findByNameAndCategoryAndStockQuantity( name, category, stockQuantity, pageable)); } @@ -64,8 +64,8 @@ public ResponseEntity> findByFilter( * @return status. Example 200(OK) */ @PostMapping - public ResponseEntity create(@RequestBody Product product) { - return ResponseEntity.ok(productService.create(product)); + public ResponseEntity create(@RequestBody Product product) { + return ResponseEntity.ok(inventoryService.create(product)); } /** @@ -77,8 +77,8 @@ public ResponseEntity create(@RequestBody Product product) { * @return status. Example 500 (Internal server error) */ @PutMapping("/{id}") - public ResponseEntity update(@PathVariable UUID id, @RequestBody Product product) { - return productService.update(id, product) + public ResponseEntity update(@PathVariable UUID id, @RequestBody Product product) { + return inventoryService.update(id, product) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @@ -90,8 +90,8 @@ public ResponseEntity update(@PathVariable UUID id, @RequestBod * @return status. Example 200 */ @PatchMapping("/{id}/outofstock") - public ResponseEntity markOutOfStock(@PathVariable UUID id) { - return productService.markOutOfStock(id) + public ResponseEntity markOutOfStock(@PathVariable UUID id) { + return inventoryService.markOutOfStock(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @@ -104,9 +104,9 @@ public ResponseEntity markOutOfStock(@PathVariable UUID id) { * @return status. Example 200(OK) */ @PatchMapping("/{id}/instock") - public ResponseEntity restoreStock(@PathVariable UUID id, - @RequestParam(defaultValue = "10") Integer stockQuantity) { - return productService.updateStock(id, stockQuantity) + public ResponseEntity restoreStock(@PathVariable UUID id, + @RequestParam(defaultValue = "10") Integer stockQuantity) { + return inventoryService.updateStock(id, stockQuantity) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @@ -119,7 +119,7 @@ public ResponseEntity restoreStock(@PathVariable UUID id, */ @DeleteMapping("/{id}") public ResponseEntity deleteProduct(@PathVariable UUID id) { - productService.delete(id); + inventoryService.delete(id); return ResponseEntity.noContent().build(); } } \ No newline at end of file diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductListResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductListResponse.java new file mode 100644 index 0000000..fb4959b --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/ProductListResponse.java @@ -0,0 +1,17 @@ +package com.encorazone.inventory_manager.domain; + +import lombok.Data; + +import java.util.List; + +@Data +public class ProductListResponse { + private List products; + + private Integer totalPages; + + public ProductListResponse(List products, Integer totalPages) { + this.products = products; + this.totalPages = totalPages; + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java index 9f1208f..4f28f7a 100644 --- a/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java +++ b/src/main/java/com/encorazone/inventory_manager/domain/ProductResponse.java @@ -2,6 +2,8 @@ import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; @@ -11,10 +13,34 @@ public class ProductResponse { private String name; + private String category; + + private BigDecimal unitPrice; + + private LocalDate expirationDate; + + private Integer stockQuantity; + private LocalDateTime creationDate; private LocalDateTime updateDate; - public ProductResponse(UUID id, String name, LocalDateTime creationDate, LocalDateTime updateDate) { + public ProductResponse( + UUID id, + String name, + String category, + BigDecimal unitPrice, + LocalDate expirationDate, + Integer stockQuantity, + LocalDateTime creationDate, + LocalDateTime updateDate) { + this.id = id; + this.name = name; + this.category = category; + this.unitPrice = unitPrice; + this.expirationDate = expirationDate; + this.stockQuantity = stockQuantity; + this.creationDate = creationDate; + this.updateDate = updateDate; } } diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductShortResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductShortResponse.java new file mode 100644 index 0000000..2c168a7 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/ProductShortResponse.java @@ -0,0 +1,28 @@ +package com.encorazone.inventory_manager.domain; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class ProductShortResponse { + private UUID id; + + private String name; + + private LocalDateTime creationDate; + + private LocalDateTime updateDate; + + public ProductShortResponse( + UUID id, + String name, + LocalDateTime creationDate, + LocalDateTime updateDate) { + this.id = id; + this.name = name; + this.creationDate = creationDate; + this.updateDate = updateDate; + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java new file mode 100644 index 0000000..67c34e7 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java @@ -0,0 +1,20 @@ +package com.encorazone.inventory_manager.domain; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ProductSummaryResponse { + private Integer productsInStock; + + private BigDecimal valueInStock; + + private BigDecimal averageValue; + + public ProductSummaryResponse(Integer productsInStock, BigDecimal valueInStock, BigDecimal averageValue) { + this.productsInStock = productsInStock; + this.valueInStock = valueInStock; + this.averageValue = averageValue; + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java b/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java index 1e13958..8678741 100644 --- a/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java +++ b/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java @@ -1,15 +1,41 @@ package com.encorazone.inventory_manager.mapper; import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.ProductListResponse; import com.encorazone.inventory_manager.domain.ProductResponse; +import com.encorazone.inventory_manager.domain.ProductShortResponse; + +import java.util.List; +import java.util.stream.Collectors; public class ProductMapper { + public static ProductShortResponse toProductShortResponse(Product product) { + return new ProductShortResponse( + product.getId(), + product.getName(), + product.getCreationDate(), + product.getUpdateDate() + ); + } + public static ProductResponse toProductResponse(Product product) { return new ProductResponse( product.getId(), product.getName(), + product.getCategory(), + product.getUnitPrice(), + product.getExpirationDate(), + product.getStockQuantity(), product.getCreationDate(), product.getUpdateDate() ); } + + public static ProductListResponse toProductListResponse(List products, Integer totalPages) { + return new ProductListResponse( + products.stream() + .map(ProductMapper::toProductResponse) + .collect(Collectors.toList()), + totalPages); + } } diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductFilter.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryProductsFilter.java similarity index 98% rename from src/main/java/com/encorazone/inventory_manager/service/ProductFilter.java rename to src/main/java/com/encorazone/inventory_manager/service/InventoryProductsFilter.java index 735c3ca..ce476d2 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductFilter.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryProductsFilter.java @@ -3,7 +3,7 @@ import com.encorazone.inventory_manager.domain.Product; import org.springframework.data.jpa.domain.Specification; -public class ProductFilter { +public class InventoryProductsFilter { /** * Creates a specification for filtering products whose names contain the given substring, diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java similarity index 77% rename from src/main/java/com/encorazone/inventory_manager/service/ProductService.java rename to src/main/java/com/encorazone/inventory_manager/service/InventoryService.java index 440c8f8..16006a2 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java @@ -2,14 +2,14 @@ import com.encorazone.inventory_manager.domain.Product; -import com.encorazone.inventory_manager.domain.ProductResponse; +import com.encorazone.inventory_manager.domain.ProductListResponse; +import com.encorazone.inventory_manager.domain.ProductShortResponse; import org.springframework.data.domain.Pageable; -import java.util.List; import java.util.Optional; import java.util.UUID; -public interface ProductService { +public interface InventoryService { /** * method to get all the elemnts from database, no sorting nor filtering @@ -19,7 +19,7 @@ public interface ProductService { * @param size represents the number of elements per page, default is 10. Example 20. * @return a list containing the pagexsize elements */ - List getAll(int page, int size); + ProductListResponse getAll(int page, int size); /** * Method to create a new product and save it @@ -27,7 +27,7 @@ public interface ProductService { * @param product object representing the product to be added to the inventory * @return the product creeated */ - ProductResponse create(Product product); + ProductShortResponse create(Product product); /** * Updates an existing product identified by the given ID. @@ -36,7 +36,7 @@ public interface ProductService { * @param product the updated product data * @return an Optional containing the updated product if found, or empty if not found */ - Optional update(UUID id, Product product); + Optional update(UUID id, Product product); /** * Method to delet product @@ -51,7 +51,7 @@ public interface ProductService { * @param id Represents the id of the element we want the stock to be 0 * @return an optional containing the updated product if the operation succeeded, or empty if not found */ - Optional markOutOfStock(UUID id); + Optional markOutOfStock(UUID id); /** * method to automatically set stock to the given number @@ -60,7 +60,7 @@ public interface ProductService { * @param stock Represents the amount to put into stock. Example 10 * @return n optional containing the updated product if the operation succeeded, or empty if not found */ - Optional updateStock(UUID id, Integer stock); + Optional updateStock(UUID id, Integer stock); /** * Endpoint for filtered data retrieving, including name, category and availability @@ -72,6 +72,6 @@ public interface ProductService { * @param pageable Builtin object for sorting and pagination, the API asks for the json by itself * @return a list of products matching the criteria */ - List findByNameAndCategoryAndStockQuantity(String name, String category, - Integer stockQuantity, Pageable pageable); + ProductListResponse findByNameAndCategoryAndStockQuantity(String name, String category, + Integer stockQuantity, Pageable pageable); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java similarity index 57% rename from src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java rename to src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java index 74a48d5..6fcac90 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/ProductServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java @@ -1,7 +1,8 @@ package com.encorazone.inventory_manager.service; import com.encorazone.inventory_manager.domain.Product; -import com.encorazone.inventory_manager.domain.ProductResponse; +import com.encorazone.inventory_manager.domain.ProductListResponse; +import com.encorazone.inventory_manager.domain.ProductShortResponse; import com.encorazone.inventory_manager.repository.ProductRepository; import com.encorazone.inventory_manager.mapper.ProductMapper; @@ -12,29 +13,29 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; -import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.UUID; @Service -public class ProductServiceImpl implements ProductService { +public class InventoryServiceImpl implements InventoryService { @Autowired private ProductRepository productRepository; @Override - public List getAll(int page, int size) { - return productRepository.findAll(PageRequest.of(page, size)).getContent(); + public ProductListResponse getAll(int page, int size) { + Page products = productRepository.findAll(PageRequest.of(page, size)); + return ProductMapper.toProductListResponse(products.getContent(),products.getTotalPages()); } @Override - public ProductResponse create(Product product) { - return ProductMapper.toProductResponse(productRepository.save(product)); + public ProductShortResponse create(Product product) { + return ProductMapper.toProductShortResponse(productRepository.save(product)); } @Override - public Optional update(UUID id, Product newProduct) { + public Optional update(UUID id, Product newProduct) { return productRepository.findById(id).map(existing -> { existing.setName(newProduct.getName()); existing.setCategory(newProduct.getCategory()); @@ -42,7 +43,7 @@ public Optional update(UUID id, Product newProduct) { existing.setExpirationDate(newProduct.getExpirationDate()); existing.setStockQuantity(newProduct.getStockQuantity()); return productRepository.save(existing); - }).map(ProductMapper::toProductResponse); + }).map(ProductMapper::toProductShortResponse); } @Override @@ -55,7 +56,7 @@ public void delete(UUID id) { } @Override - public Optional markOutOfStock(UUID id) { + public Optional markOutOfStock(UUID id) { return productRepository.findById(id).map(product -> { if (product.getStockQuantity() > 0) { product.setStockQuantity(0); @@ -63,26 +64,26 @@ public Optional markOutOfStock(UUID id) { } else { return product; } - }).map(ProductMapper::toProductResponse); + }).map(ProductMapper::toProductShortResponse); } @Override - public Optional updateStock(UUID id, Integer stock) { + public Optional updateStock(UUID id, Integer stock) { return productRepository.findById(id).map(product -> { product.setStockQuantity(stock); return productRepository.save(product); - }).map(ProductMapper::toProductResponse); + }).map(ProductMapper::toProductShortResponse); } @Override - public List findByNameAndCategoryAndStockQuantity(String name, String category, - Integer stockQuantity, Pageable pageable) { - Specification spec = ProductFilter.nameContains(name) - .and(ProductFilter.categoryContains(category)) - .and(ProductFilter.quantityEquals(stockQuantity)); + public ProductListResponse findByNameAndCategoryAndStockQuantity(String name, String category, + Integer stockQuantity, Pageable pageable) { + Specification spec = InventoryProductsFilter.nameContains(name) + .and(InventoryProductsFilter.categoryContains(category)) + .and(InventoryProductsFilter.quantityEquals(stockQuantity)); Page page = productRepository.findAll(spec, pageable); - return page.getContent(); + return ProductMapper.toProductListResponse(page.getContent(), page.getTotalPages()); } } diff --git a/src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java b/src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java new file mode 100644 index 0000000..b6e4465 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java @@ -0,0 +1,4 @@ +package com.encorazone.inventory_manager.service; + +public class InventorySummary { +} From ebc6eaeca070dfb2f7ddd6668c0f023dbc4e3e8c Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sat, 21 Jun 2025 00:48:55 -0600 Subject: [PATCH 13/26] Added functionality so client can fetch all the different categories --- .../InventoryManagerController.java | 13 ++++++++++++ .../repository/ProductRepository.java | 20 ++++--------------- .../service/InventoryService.java | 8 ++++++++ .../service/InventoryServiceImpl.java | 6 ++++++ 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index 335ce93..c39bc61 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -11,6 +11,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.UUID; @CrossOrigin(origins = "http://localhost:3000") @@ -122,4 +123,16 @@ public ResponseEntity deleteProduct(@PathVariable UUID id) { inventoryService.delete(id); return ResponseEntity.noContent().build(); } + + /** + * Endpoint to retrieve categories + * + * @return list with the categories + */ + @GetMapping("/categories") + public ResponseEntity> fetchCategories() { + return inventoryService.fetchCategories() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } } \ No newline at end of file diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index c583684..7c65a79 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -2,27 +2,15 @@ import com.encorazone.inventory_manager.domain.Product; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; import java.util.List; +import java.util.Optional; import java.util.UUID; public interface ProductRepository extends JpaRepository, JpaSpecificationExecutor { - /** - * This method was my second try to filter and sort data. It filters well, - * cannot sort, explained better on the DEMO if I don't forget, Probably will. - * Basically the method was filtering always the availability, - * setting the stockQuantity to 0, meaning if the product had a stock different - * to 0, It wouldn't appear in the resulting list. - * - * @param name name - * @param category category - * @param quantityInStock availability of the product - * @param pageable objet for sorting and pagination - * @return a list of product matchin the description - */ - List findByNameContainingIgnoreCaseAndCategoryContainingIgnoreCaseAndStockQuantity( - String name, String category, Integer quantityInStock, Pageable pageable); + @Query("SELECT DISTINCT p.category FROM Product p") + Optional> findDistinctCategories(); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java index 16006a2..45c17d3 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java @@ -6,6 +6,7 @@ import com.encorazone.inventory_manager.domain.ProductShortResponse; import org.springframework.data.domain.Pageable; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -74,4 +75,11 @@ public interface InventoryService { */ ProductListResponse findByNameAndCategoryAndStockQuantity(String name, String category, Integer stockQuantity, Pageable pageable); + + /** + * Method to retrieve categories + * + * @return a list with the categories + */ + Optional> fetchCategories(); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java index 6fcac90..f436136 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java @@ -13,6 +13,7 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; import java.util.UUID; @@ -86,4 +87,9 @@ public ProductListResponse findByNameAndCategoryAndStockQuantity(String name, St return ProductMapper.toProductListResponse(page.getContent(), page.getTotalPages()); } + @Override + public Optional> fetchCategories(){ + return productRepository.findDistinctCategories(); + } + } From 6a7c42b6079672123c6857061004c48646ac5fe1 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sun, 22 Jun 2025 23:18:27 -0600 Subject: [PATCH 14/26] Added summary/metrics functionality logic and endpoint Modified files: - ProductRepository.java: added definition to repository to make the necessary query - ProductMapper.java: Transform the InventorySummaryInterface from the repository method to a response appropriate data type for the endpoint. - InventoryService.java: added definition for the summary/metrics new method - InventoryServiceImpl.java: added implementation of the new method from the InventoryService.java - InventoryManagerController.java: added endpoint for summary --- .../InventoryManagerController.java | 13 ++++++++++ .../domain/InventorySummaryInterface.java | 10 ++++++++ .../domain/InventorySummaryResponse.java | 24 +++++++++++++++++++ .../domain/ProductSummaryResponse.java | 20 ---------------- .../mapper/ProductMapper.java | 21 ++++++++++++---- .../repository/ProductRepository.java | 6 +++++ .../service/InventoryService.java | 8 +++++++ .../service/InventoryServiceImpl.java | 6 +++++ 8 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryInterface.java create mode 100644 src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryResponse.java delete mode 100644 src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index c39bc61..f99438c 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -1,5 +1,6 @@ package com.encorazone.inventory_manager.controller; +import com.encorazone.inventory_manager.domain.InventorySummaryResponse; import com.encorazone.inventory_manager.domain.Product; import com.encorazone.inventory_manager.domain.ProductListResponse; import com.encorazone.inventory_manager.domain.ProductShortResponse; @@ -135,4 +136,16 @@ public ResponseEntity> fetchCategories() { .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } + + /** + * Endpoint to retrieve inventory summary + * + * @return list with the summary + */ + @GetMapping("/summary") + public ResponseEntity> fetchSummary() { + return inventoryService.fetchInventorySummary() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } } \ No newline at end of file diff --git a/src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryInterface.java b/src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryInterface.java new file mode 100644 index 0000000..a9c22a4 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryInterface.java @@ -0,0 +1,10 @@ +package com.encorazone.inventory_manager.domain; + +import java.math.BigDecimal; + +public interface InventorySummaryInterface { + String getCategory(); + Long getProductsInStock(); + BigDecimal getValueInStock(); + BigDecimal getAverageValue(); +} \ No newline at end of file diff --git a/src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryResponse.java new file mode 100644 index 0000000..91741b5 --- /dev/null +++ b/src/main/java/com/encorazone/inventory_manager/domain/InventorySummaryResponse.java @@ -0,0 +1,24 @@ +package com.encorazone.inventory_manager.domain; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class InventorySummaryResponse{ + private String category; + + private Integer productsInStock; + + private BigDecimal valueInStock; + + private BigDecimal averageValue; + + public InventorySummaryResponse(String category, long productsInStock, BigDecimal valueInStock, + BigDecimal averageValue) { + this.category = category; + this.productsInStock = (int) productsInStock; + this.valueInStock = valueInStock; + this.averageValue = averageValue; + } +} diff --git a/src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java b/src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java deleted file mode 100644 index 67c34e7..0000000 --- a/src/main/java/com/encorazone/inventory_manager/domain/ProductSummaryResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.encorazone.inventory_manager.domain; - -import lombok.Data; - -import java.math.BigDecimal; - -@Data -public class ProductSummaryResponse { - private Integer productsInStock; - - private BigDecimal valueInStock; - - private BigDecimal averageValue; - - public ProductSummaryResponse(Integer productsInStock, BigDecimal valueInStock, BigDecimal averageValue) { - this.productsInStock = productsInStock; - this.valueInStock = valueInStock; - this.averageValue = averageValue; - } -} diff --git a/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java b/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java index 8678741..a8cde9f 100644 --- a/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java +++ b/src/main/java/com/encorazone/inventory_manager/mapper/ProductMapper.java @@ -1,9 +1,6 @@ package com.encorazone.inventory_manager.mapper; -import com.encorazone.inventory_manager.domain.Product; -import com.encorazone.inventory_manager.domain.ProductListResponse; -import com.encorazone.inventory_manager.domain.ProductResponse; -import com.encorazone.inventory_manager.domain.ProductShortResponse; +import com.encorazone.inventory_manager.domain.*; import java.util.List; import java.util.stream.Collectors; @@ -38,4 +35,20 @@ public static ProductListResponse toProductListResponse(List products, .collect(Collectors.toList()), totalPages); } + + public static InventorySummaryResponse toInventorySummaryResponse(InventorySummaryInterface product) { + return new InventorySummaryResponse( + product.getCategory(), + product.getProductsInStock(), + product.getValueInStock(), + product.getAverageValue() + ); + } + + public static List toInventorySummaryResponseList(List lista){ + return lista + .stream() + .map(ProductMapper::toInventorySummaryResponse) + .toList(); + } } diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index 7c65a79..551cfff 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -1,5 +1,6 @@ package com.encorazone.inventory_manager.repository; +import com.encorazone.inventory_manager.domain.InventorySummaryInterface; import com.encorazone.inventory_manager.domain.Product; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,4 +14,9 @@ public interface ProductRepository extends JpaRepository, JpaSpecificationExecutor { @Query("SELECT DISTINCT p.category FROM Product p") Optional> findDistinctCategories(); + + @Query("SELECT p.category AS category, COUNT(p) AS productsInStock, " + + "SUM(p.unitPrice) AS valueInStock, AVG(p.unitPrice) AS averageValue " + + "FROM Product p GROUP BY p.category") + List findCategoriesSummary(); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java index 45c17d3..fb56b53 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryService.java @@ -4,6 +4,7 @@ import com.encorazone.inventory_manager.domain.ProductListResponse; import com.encorazone.inventory_manager.domain.ProductShortResponse; +import com.encorazone.inventory_manager.domain.InventorySummaryResponse; import org.springframework.data.domain.Pageable; import java.util.List; @@ -82,4 +83,11 @@ ProductListResponse findByNameAndCategoryAndStockQuantity(String name, String ca * @return a list with the categories */ Optional> fetchCategories(); + + /** + * Method to retrieve the inventory summary + * + * @return a list with the summary + */ + Optional> fetchInventorySummary(); } diff --git a/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java b/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java index f436136..5119e9e 100644 --- a/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java +++ b/src/main/java/com/encorazone/inventory_manager/service/InventoryServiceImpl.java @@ -1,5 +1,6 @@ package com.encorazone.inventory_manager.service; +import com.encorazone.inventory_manager.domain.InventorySummaryResponse; import com.encorazone.inventory_manager.domain.Product; import com.encorazone.inventory_manager.domain.ProductListResponse; import com.encorazone.inventory_manager.domain.ProductShortResponse; @@ -92,4 +93,9 @@ public Optional> fetchCategories(){ return productRepository.findDistinctCategories(); } + @Override + public Optional> fetchInventorySummary(){ + return Optional.ofNullable(ProductMapper.toInventorySummaryResponseList(productRepository.findCategoriesSummary())); + } + } From 378946fa48370b145e99eb54931e3fdc25f36f99 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Tue, 24 Jun 2025 10:22:37 -0600 Subject: [PATCH 15/26] testing --- pom.xml | 5 ++ .../InventoryManagerControllerTests.java | 35 ++++++++++++ .../resources/application-test.yml | 12 +++++ .../inventory_manager/resources/data.sql | 21 ++++++++ .../inventory_manager/resources/schema.sql | 11 ++++ .../service/InventoryProductsFilterTests.java | 4 ++ .../service/InventoryServiceITests.java | 53 +++++++++++++++++++ .../service/InventoryServiceImplTests.java | 4 ++ 8 files changed, 145 insertions(+) create mode 100644 src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java create mode 100644 src/test/java/com/encorazone/inventory_manager/resources/application-test.yml create mode 100644 src/test/java/com/encorazone/inventory_manager/resources/data.sql create mode 100644 src/test/java/com/encorazone/inventory_manager/resources/schema.sql create mode 100644 src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java create mode 100644 src/test/java/com/encorazone/inventory_manager/service/InventoryServiceITests.java create mode 100644 src/test/java/com/encorazone/inventory_manager/service/InventoryServiceImplTests.java diff --git a/pom.xml b/pom.xml index ae7d970..c998f4d 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,11 @@ lombok ${lombok.version} + + com.h2database + h2 + test + diff --git a/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java b/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java new file mode 100644 index 0000000..27c10a4 --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java @@ -0,0 +1,35 @@ +package com.encorazone.inventory_manager.controller; + + +import com.encorazone.inventory_manager.service.InventoryService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class InventoryManagerControllerTests { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private InventoryService inventoryService; + + @Test + void getAllProducts_shouldReturnDataFromDatabase() throws Exception { + mockMvc.perform(get("/products")) + .andExpect(status().isOk()); + } + +} diff --git a/src/test/java/com/encorazone/inventory_manager/resources/application-test.yml b/src/test/java/com/encorazone/inventory_manager/resources/application-test.yml new file mode 100644 index 0000000..ce99546 --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/resources/application-test.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate.ddl-auto: none + show-sql: true + sql: + init: + mode: always \ No newline at end of file diff --git a/src/test/java/com/encorazone/inventory_manager/resources/data.sql b/src/test/java/com/encorazone/inventory_manager/resources/data.sql new file mode 100644 index 0000000..402fde8 --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/resources/data.sql @@ -0,0 +1,21 @@ +INSERT INTO PRODUCTS (ID, NAME, CATEGORY, UNIT_PRICE, EXPIRATION_DATE, STOCK_QUANTITY, CREATION_DATE, UPDATE_DATE) VALUES ( + '00212243-4455-6677-8899-aabbccdd8eff',, + 'Fresh Milk', + 'Dairy', + 25.50, + '2025-12-31', + 100, + CURRENT_TIMESTAMP, + NULL +); + +INSERT INTO PRODUCTS (ID, NAME, CATEGORY, UNIT_PRICE, EXPIRATION_DATE, STOCK_QUANTITY, CREATION_DATE, UPDATE_DATE) VALUES ( + '00112233-4455-6677-8899-aabbccddeeff',, + 'Whole Wheat Bread', + 'Bakery', + 15.25, + '2025-06-30', + 50, + CURRENT_TIMESTAMP, + NULL +); diff --git a/src/test/java/com/encorazone/inventory_manager/resources/schema.sql b/src/test/java/com/encorazone/inventory_manager/resources/schema.sql new file mode 100644 index 0000000..89d4224 --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/resources/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE PRODUCTS ( + ID RAW(16) NOT NULL, + NAME VARCHAR(120) NOT NULL, + CATEGORY VARCHAR(255) NOT NULL, + UNIT_PRICE DECIMAL(38,2) NOT NULL, + EXPIRATION_DATE DATE, + STOCK_QUANTITY INT NOT NULL, + CREATION_DATE TIMESTAMP, + UPDATE_DATE TIMESTAMP, + PRIMARY KEY (ID) +); \ No newline at end of file diff --git a/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java b/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java new file mode 100644 index 0000000..edf236a --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java @@ -0,0 +1,4 @@ +package com.encorazone.inventory_manager.service; + +public class InventoryProductsFilterTests { +} diff --git a/src/test/java/com/encorazone/inventory_manager/service/InventoryServiceITests.java b/src/test/java/com/encorazone/inventory_manager/service/InventoryServiceITests.java new file mode 100644 index 0000000..59ce884 --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/service/InventoryServiceITests.java @@ -0,0 +1,53 @@ +package com.encorazone.inventory_manager.service; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class InventoryServiceITests { + @Test + void getAll() { + + } + + @Test + void create() { + + } + + @Test + void update() { + + } + + @Test + void delete() { + + } + + @Test + void markOutOfStock() { + + } + + @Test + void updateStock() { + + } + + @Test + void findByNameAndCategoryAndStockQuantity() { + + } + + @Test + void fetchCategories() { + + } + + @Test + void fetchInventorySummary() { + + } +} diff --git a/src/test/java/com/encorazone/inventory_manager/service/InventoryServiceImplTests.java b/src/test/java/com/encorazone/inventory_manager/service/InventoryServiceImplTests.java new file mode 100644 index 0000000..3e0c6ab --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/service/InventoryServiceImplTests.java @@ -0,0 +1,4 @@ +package com.encorazone.inventory_manager.service; + +public class InventoryServiceImplTests { +} From 4f4c19034c2d7b7bd81c9c7042634eefe565eddf Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Tue, 24 Jun 2025 13:47:20 -0600 Subject: [PATCH 16/26] * (#10) * --- README.md | 44 +++++++++++++++++++++++++++++--------------- pom.xml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 81690a4..1f7057d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This is a Spring Boot-based inventory management application designed to help ma --- -## 🚀 Features +## Features - **Product Management** - Create new products with name, category, quantity, unit price, and optional expiration date. @@ -30,10 +30,10 @@ This is a Spring Boot-based inventory management application designed to help ma --- -## 🧱 Technical Overview +## Technical Overview ### Model: `Product` -- `id` (Unique Identifier) +- `id` (Unique Identifier. Auto-generated) - `name` (Required, max 120 characters) - `category` (Required) - `unitPrice` (Required) @@ -44,30 +44,35 @@ This is a Spring Boot-based inventory management application designed to help ma ### API Endpoints -| Method | Endpoint | Description | -|--------|----------------------------------|---------------------------------------------------| -| GET | `/products` | Retrieve products with filtering, sorting, and pagination | -| POST | `/products` | Create a new product | -| PUT | `/products/{id}` | Update an existing product | -| POST | `/products/{id}/outofstock` | Mark a product as out of stock | -| PUT | `/products/{id}/instock` | Restore a product's stock | +| Method | Endpoint | Description | +|--------|-----------------------------|------------------------------------------------------------------| +| GET | `/products` | Retrieve products with pagination support | +| POST | `/products` | Create a new product | +| DELETE | `/products` | Delete a products | +| PUT | `/products/{id}` | Update an existing product | +| PATCH | `/products/{id}/outofstock` | Mark a product as out of stock | +| PATCH | `/products/{id}/instock` | Restore a product's stock
| +| GET | `/products/summary` | Retrieves inventory metrics | +| GET | `/products/filters` | Retrieves products based on filters, sort methods and pagination | +| GET | `/products/categories` | Retrieves all the categories available | + ### Storage -Currently, product data is stored in-memory using Java Collections. The application is designed to allow easy migration to a persistent storage layer in the future. +Currently, product data is stored in a local database using docker. --- -## 🛠️ Tech Stack +## Tech Stack - **Language:** Java - **Framework:** Spring Boot - **Build Tool:** Maven -- **Data Storage:** In-Memory (Java Collections) +- **Data Storage:** Oracle DB --- -## ⚙️ Getting Started +## Getting Started ### Prerequisites @@ -77,4 +82,13 @@ Currently, product data is stored in-memory using Java Collections. The applicat ### Running the Application ```bash -mvn spring-boot:run \ No newline at end of file +docker run -d \ +--name oracle-xe \ +-e ORACLE_PASSWORD=admin \ +-p 1521:1521 \ +-p 5500:5500 \ +oracle-xe-inventory-manager:1.0 +``` +```bash +mvn spring-boot:run +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index c998f4d..f0f5459 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,8 @@ 21.3.0.0 2.3.0 1.18.38 + 1.5.5.Final + 3.14.0 @@ -89,10 +91,38 @@ h2 test + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler.version} + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + org.springframework.boot spring-boot-maven-plugin From f6529c78b90908cb571ae46784e40e4876b589db Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Wed, 23 Jul 2025 12:57:38 -0600 Subject: [PATCH 17/26] Modified project to run locally --- pom.xml | 5 +++++ src/main/resources/application.yml | 16 +++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index f0f5459..114eb60 100644 --- a/pom.xml +++ b/pom.xml @@ -102,6 +102,11 @@ ${mapstruct.version} provided + + com.h2database + h2 + runtime + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 687c0cc..4a0cb95 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,10 +3,16 @@ spring: name: inventory-manager-spark datasource: - url: jdbc:oracle:thin:@localhost:1521/XEPDB1 - username: '#INVENTORY' - password: admin - driver-class-name: oracle.jdbc.OracleDriver + url: jdbc:h2:mem:testdb + username: 'sa' + password: + driver-class-name: org.h2.Driver + + h2: + console: + enabled: true jpa: - database-platform: org.hibernate.dialect.OracleDialect + hibernate: + ddl-auto: update + show-sql: true From aadfc2b266210f890723f558a36dfe52cffd6185 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sat, 4 Oct 2025 16:54:20 -0600 Subject: [PATCH 18/26] Create AI agent for PRs (#14) * Create ai agent for PRs * Update ai-pr-summary.yml --- .github/workflows/ai-pr-summary.yml | 124 ++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .github/workflows/ai-pr-summary.yml diff --git a/.github/workflows/ai-pr-summary.yml b/.github/workflows/ai-pr-summary.yml new file mode 100644 index 0000000..3172969 --- /dev/null +++ b/.github/workflows/ai-pr-summary.yml @@ -0,0 +1,124 @@ +name: PR AI Summary + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + summarize: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Get PR diff + id: diff + run: | + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + # Trae exactamente esos commits (evita problemas de merge-base y shallow clones) + git fetch --no-tags --prune --depth=1 origin $BASE $HEAD + git diff $BASE $HEAD > pr.diff + echo "path=pr.diff" >> $GITHUB_OUTPUT + + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install deps + run: | + python -m pip install --upgrade pip + pip install openai==1.* # SDK oficial + + - name: Generate AI summary (OpenAI) + id: ai + continue-on-error: true + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + MODEL: gpt-4o-mini + run: | + python - << 'PY' + import os + from openai import OpenAI + client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + + with open("pr.diff","r",encoding="utf-8") as f: + diff = f.read()[:200000] # tope por costos/ruido + + prompt = ( + "You are a code reviewer. Summarize this PR in 2-20 bullets. " + "Include WHAT changed, WHY it matters, RISKS, TESTS to add, and any BREAKING CHANGES. " + "Highlight key fetures or changes. Consider markdown as the default output format." + "Keep it concise and actionable.\n\nDIFF:\n" + diff + ) + + resp = client.chat.completions.create( + model=os.getenv("MODEL","gpt-4o-mini"), + temperature=0.2, + messages=[{"role":"user","content":prompt}], + ) + text = resp.choices[0].message.content.strip() + with open("summary.txt","w",encoding="utf-8") as f: + f.write(text) + PY + + - name: Heuristic fallback if AI failed + if: ${{ steps.ai.outcome == 'failure' }} + run: | + python - << 'PY' + import re, pathlib + diff = pathlib.Path("pr.diff").read_text(encoding="utf-8") + + added = len(re.findall(r"^\\+[^+].*$", diff, flags=re.M)) + removed = len(re.findall(r"^\\-[^-].*$", diff, flags=re.M)) + files = re.findall(r"^\\+\\+\\+ b/(.+)$", diff, flags=re.M) + + scopes = set() + for f in files: + fl = f.lower() + if "/controller" in fl: scopes.add("controller") + elif "/service" in fl: scopes.add("service") + elif "/repository" in fl or "jparepository" in diff.lower(): scopes.add("repository") + elif "/entity" in fl or "/model" in fl: scopes.add("entity") + elif "application" in fl and (fl.endswith(".yml") or fl.endswith(".yaml") or fl.endswith(".properties")): + scopes.add("config") + elif fl.endswith("test.java"): scopes.add("test") + + scope = ",".join(sorted(scopes)) if scopes else "core" + kind = "refactor" + if added and not removed: kind = "feat" + if removed and not added: kind = "chore" + if re.search(r"@Test", diff): kind = "test" + if re.search(r"fix|bug|exception|stacktrace", diff, re.I): kind = "fix" + + subject = f"[Fallback] {kind}({scope}): {len(files)} file(s), +{added}/-{removed}" + + bullets = [] + bullets.append(f"- Files changed: {len(files)}") + bullets.append(f"- Lines: +{added} / -{removed}") + if scopes: + bullets.append(f"- Layers: {', '.join(sorted(scopes))}") + if re.search(r"@Transactional", diff): bullets.append("- Touches transactional boundaries") + if re.search(r"@RestController|@Controller", diff): bullets.append("- Controller changes present") + if re.search(r"@Service", diff): bullets.append("- Service-layer changes present") + if re.search(r"@Repository|JpaRepository", diff): bullets.append("- Repository-layer changes present") + if re.search(r"todo|fixme", diff, re.I): bullets.append("- Contains TODO/FIXME markers") + + text = subject + "\\n\\n" + "\\n".join(bullets) + pathlib.Path("summary.txt").write_text(text, encoding="utf-8") + PY + + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: ai-pr-summary + recreate: true + path: summary.txt + From e94ad2d199a8a6135a407eb45af9e35e2b330fb5 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sat, 4 Oct 2025 17:19:29 -0600 Subject: [PATCH 19/26] Deleted empty classes Following YAGNI principles, deleted unused InventorySummary.java; deleted unused hello.html and reformatted other files. --- .github/workflows/ai-pr-summary.yml | 5 +---- .../inventory_manager/service/InventorySummary.java | 4 ---- src/main/resources/templates/hello.html | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java delete mode 100644 src/main/resources/templates/hello.html diff --git a/.github/workflows/ai-pr-summary.yml b/.github/workflows/ai-pr-summary.yml index 3172969..2ebbdc7 100644 --- a/.github/workflows/ai-pr-summary.yml +++ b/.github/workflows/ai-pr-summary.yml @@ -25,7 +25,6 @@ jobs: git diff $BASE $HEAD > pr.diff echo "path=pr.diff" >> $GITHUB_OUTPUT - - name: Set up Python uses: actions/setup-python@v5 with: @@ -114,11 +113,9 @@ jobs: pathlib.Path("summary.txt").write_text(text, encoding="utf-8") PY - - name: Comment on PR uses: marocchino/sticky-pull-request-comment@v2 with: header: ai-pr-summary recreate: true - path: summary.txt - + path: summary.txt \ No newline at end of file diff --git a/src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java b/src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java deleted file mode 100644 index b6e4465..0000000 --- a/src/main/java/com/encorazone/inventory_manager/service/InventorySummary.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.encorazone.inventory_manager.service; - -public class InventorySummary { -} diff --git a/src/main/resources/templates/hello.html b/src/main/resources/templates/hello.html deleted file mode 100644 index 9615ada..0000000 --- a/src/main/resources/templates/hello.html +++ /dev/null @@ -1 +0,0 @@ -"Hello from Thymeleaf" \ No newline at end of file From 040d4ba046226f0f5a8908850618a2fa9b1dc062 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sat, 4 Oct 2025 17:49:22 -0600 Subject: [PATCH 20/26] API project runs on port 9090 Added server port property Added port property to server within application.yml file and set the port to 9090 to make the server run on this port. PR Summary WHAT Changed Updated the application.yml configuration file to change the server port from the default to 9090. --- src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4a0cb95..ad1d987 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,6 @@ +server: + port: 9090 + spring: application: name: inventory-manager-spark From 7ab4af7a41ae39be4f520fea65b7cbd0b044c42e Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sun, 5 Oct 2025 00:46:48 -0600 Subject: [PATCH 21/26] Project running with no external DBs Modified README Modified readme to show the current storage technology -> runtime local database. --- README.md | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1f7057d..ac0068d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # InventoryManagerBC -Breakable Toy 1 - Inventory Manager +Breakable Toy 1 (Gen AI Augmented) - Inventory Manager # Inventory Management Application @@ -57,10 +57,6 @@ This is a Spring Boot-based inventory management application designed to help ma | GET | `/products/categories` | Retrieves all the categories available | -### Storage - -Currently, product data is stored in a local database using docker. - --- ## Tech Stack @@ -68,7 +64,7 @@ Currently, product data is stored in a local database using docker. - **Language:** Java - **Framework:** Spring Boot - **Build Tool:** Maven -- **Data Storage:** Oracle DB +- **Data Storage:** H2 local Runtime via JDBC --- @@ -82,13 +78,5 @@ Currently, product data is stored in a local database using docker. ### Running the Application ```bash -docker run -d \ ---name oracle-xe \ --e ORACLE_PASSWORD=admin \ --p 1521:1521 \ --p 5500:5500 \ -oracle-xe-inventory-manager:1.0 -``` -```bash -mvn spring-boot:run + mvn spring-boot:run ``` \ No newline at end of file From 7d4fca71639ec05849febc581eb44f3ec3f93218 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Sun, 5 Oct 2025 01:24:46 -0600 Subject: [PATCH 22/26] Fine-tune agent behavior Modified the agent prompt and strengthen the fallback function --- .github/workflows/ai-pr-summary.yml | 40 ++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ai-pr-summary.yml b/.github/workflows/ai-pr-summary.yml index 2ebbdc7..915bfe1 100644 --- a/.github/workflows/ai-pr-summary.yml +++ b/.github/workflows/ai-pr-summary.yml @@ -53,7 +53,12 @@ jobs: prompt = ( "You are a code reviewer. Summarize this PR in 2-20 bullets. " "Include WHAT changed, WHY it matters, RISKS, TESTS to add, and any BREAKING CHANGES. " - "Highlight key fetures or changes. Consider markdown as the default output format." + "Highlight key features or changes. Consider markdown as the default output format." + "Keep in mind the following points:" + "1) If DIFF shows only documentation files (e.g., .md/.mdx/.txt/README), state 'Docs-only change', " + " make clear that the change is included only in documentation files, if that is the case, " + " otherwise explain normally, considering the DIFF changes like normal. " + "2) Include a short list of changed file paths as extracted from DIFF. " "Keep it concise and actionable.\n\nDIFF:\n" + diff ) @@ -77,6 +82,39 @@ jobs: added = len(re.findall(r"^\\+[^+].*$", diff, flags=re.M)) removed = len(re.findall(r"^\\-[^-].*$", diff, flags=re.M)) files = re.findall(r"^\\+\\+\\+ b/(.+)$", diff, flags=re.M) + + lower_paths = [f.lower() for f in files] + DOC_EXT = (".md", ".mdx", ".txt", ".rst", ".adoc") + is_doc = lambda p: p.endswith(DOC_EXT) or "/docs/" in p or "/doc/" in p + docs_only = len(files) > 0 and all(is_doc(p) for p in lower_paths) + + # ---------- Doc-only summary ---------- + if docs_only: + bullets_changed = [] + for f in files[:20]: # evita listas enormes + bullets_changed.append(f"- `{f}`") + doc_summary = [ + "## PR Summary", + "", + "### WHAT Changed", + "- **Docs-only change** detected from DIFF.", + f"- Files changed ({len(files)}):", + *bullets_changed, + "", + "### WHY It Matters", + "- Improves documentation/README clarity and onboarding experience.", + "", + "### RISKS", + "- None to runtime behavior (documentation only).", + "", + "### TESTS to Add", + "- N/A (no code changes).", + "", + "### BREAKING CHANGES", + "- None.", + ] + pathlib.Path("summary.txt").write_text("\n".join(doc_summary), encoding="utf-8") + raise SystemExit(0) scopes = set() for f in files: From 948c96575eb78eb843a7c46ac8c96ee4ee65b7a3 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Mon, 13 Oct 2025 14:12:41 -0600 Subject: [PATCH 23/26] Update CORS to allow client on localhost:8080 Allows cross-origin requests from the new frontend port. --- .../controller/InventoryManagerController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java index f99438c..6bd321a 100644 --- a/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java +++ b/src/main/java/com/encorazone/inventory_manager/controller/InventoryManagerController.java @@ -15,7 +15,7 @@ import java.util.List; import java.util.UUID; -@CrossOrigin(origins = "http://localhost:3000") +@CrossOrigin(origins = "http://localhost:8080") @RestController @RequestMapping("/products") final class InventoryManagerController { From 22ccd6395b0d408a82579e7807df8d33636fe10a Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Thu, 16 Oct 2025 11:48:11 -0600 Subject: [PATCH 24/26] Update repository query * Update repository query logic - Fixed logic of the query to maths the requirements. * Updated show-sql setting - Updated the show-sql as theres no longer need of the logs --------- Signed-off-by: Leonardo Trevizo --- .../inventory_manager/repository/ProductRepository.java | 4 ++-- src/main/resources/application.yml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java index 551cfff..da7a811 100644 --- a/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java +++ b/src/main/java/com/encorazone/inventory_manager/repository/ProductRepository.java @@ -15,8 +15,8 @@ public interface ProductRepository extends JpaRepository, JpaSpec @Query("SELECT DISTINCT p.category FROM Product p") Optional> findDistinctCategories(); - @Query("SELECT p.category AS category, COUNT(p) AS productsInStock, " + - "SUM(p.unitPrice) AS valueInStock, AVG(p.unitPrice) AS averageValue " + + @Query("SELECT p.category AS category, SUM(p.stockQuantity) AS productsInStock, " + + "SUM(p.unitPrice * p.stockQuantity) AS valueInStock, AVG(p.unitPrice) AS averageValue " + "FROM Product p GROUP BY p.category") List findCategoriesSummary(); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ad1d987..f6a63d8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,4 +18,3 @@ spring: jpa: hibernate: ddl-auto: update - show-sql: true From eca633d0b9d3f29be44fa36cc341eab98e96e93a Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Thu, 16 Oct 2025 22:00:30 -0600 Subject: [PATCH 25/26] Upgraded test compliance - Updated the test to be more compliant --- .../InventoryManagerControllerTests.java | 253 +++++++++++++++++- .../mapper/ProductMapperTests.java | 100 +++++++ .../service/InventoryProductsFilterTests.java | 81 +++++- 3 files changed, 424 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java diff --git a/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java b/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java index 27c10a4..b073c0c 100644 --- a/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java +++ b/src/test/java/com/encorazone/inventory_manager/controller/InventoryManagerControllerTests.java @@ -1,19 +1,44 @@ package com.encorazone.inventory_manager.controller; - +import com.encorazone.inventory_manager.domain.*; import com.encorazone.inventory_manager.service.InventoryService; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.hasItems; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @@ -21,15 +46,225 @@ public class InventoryManagerControllerTests { @Autowired - private MockMvc mockMvc; + private MockMvc mvc; + + @Autowired + ObjectMapper objectMapper; @MockitoBean private InventoryService inventoryService; @Test - void getAllProducts_shouldReturnDataFromDatabase() throws Exception { - mockMvc.perform(get("/products")) - .andExpect(status().isOk()); + @DisplayName("returns selected page with products when getting all products") + void getAll_returnsPage() throws Exception { + ProductListResponse payload = new ProductListResponse(List.of(sampleProductRes()), 1); + given(inventoryService.getAll(0, 10)).willReturn(payload); + + mvc.perform(get("/products")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products", hasSize(1))) + .andExpect(jsonPath("$.totalPages", is(1))); + } + + @Test + @DisplayName("returns selected page according to de filter/sort orders") + void findByFilter_passesPageable() throws Exception { + ProductListResponse payload = new ProductListResponse(List.of(sampleProductRes()), 1); + ArgumentCaptor captor = ArgumentCaptor.forClass(Pageable.class); + Pageable expected = PageRequest.of(1, 10, Sort.by(Sort.Order.desc("price"))); + given(inventoryService.findByNameAndCategoryAndStockQuantity( + "Watermelon", "food", 0, expected)) + .willReturn(payload); + + mvc.perform(get("/products/filters") + .param("name", "Watermelon") + .param("category", "food") + .param("stockQuantity", "0") + .param("page", "1") + .param("size", "10") + .param("sort", "price,desc")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.products", hasSize(1))) + .andExpect(jsonPath("$.totalPages", is(1))); + verify(inventoryService) + .findByNameAndCategoryAndStockQuantity( + eq("Watermelon"), + eq("food"), + eq(0), + captor.capture()); + + Pageable p = captor.getValue(); + assert p.getPageNumber() == 1; + assert p.getPageSize() == 10; + Sort.Order o = p.getSort().getOrderFor("price"); + assert o != null && o.getDirection() == Sort.Direction.DESC; + } + + @Test + @DisplayName("returns small product descriptions corresponding to the created product") + void create_returnsShortResp() throws Exception { + ProductShortResponse created = sampleProductShort(); + given(inventoryService.create(nullable(Product.class))).willReturn(created); + + Product toCreate = sampleProduct(); + mvc.perform(post("/products") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(toCreate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", notNullValue())) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns small product descriptions corresponding to the updated product") + void update_returnsShortResp() throws Exception { + ProductShortResponse updated = sampleProductShort(); + UUID id = updated.getId(); + given(inventoryService.update(eq(id), nullable(Product.class))).willReturn(Optional.of(updated)); + + mvc.perform(put("/products/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sampleProduct()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product id corresponds to the sent one - update") + void update_returnsNotFound() throws Exception { + UUID id = UUID.randomUUID(); + given(inventoryService.update(eq(id), nullable(Product.class))).willReturn(Optional.empty()); + + mvc.perform(put("/products/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sampleProduct()))) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns small product descriptions corresponding to the marked out of stock product") + void markOutOfStock_returnsShortResp() throws Exception { + ProductShortResponse resp = sampleProductShort(); + UUID id = resp.getId(); + given(inventoryService.markOutOfStock(id)).willReturn(Optional.of(resp)); + + mvc.perform(patch("/products/{id}/outofstock", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product id corresponds to the sent one - markOutOfStock") + void markOutOfStock_returnsNotFound() throws Exception { + UUID id = UUID.randomUUID(); + given(inventoryService.markOutOfStock(id)).willReturn(Optional.empty()); + + mvc.perform(patch("/products/{id}/outofstock", id)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns small product descriptions corresponding to the restored stock product") + void restoreStock_returnsShortResp() throws Exception { + ProductShortResponse resp = sampleProductShort(); + UUID id = resp.getId(); + given(inventoryService.updateStock(id, 10)).willReturn(Optional.of(resp)); + + mvc.perform(patch("/products/{id}/instock", id).param("stockQuantity", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(id.toString()))) + .andExpect(jsonPath("$.name", is("Watermelon"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product id corresponds to the sent one - restoreStock") + void restoreStock_returnsNotFound() throws Exception { + UUID id = UUID.randomUUID(); + given(inventoryService.updateStock(id, 10)).willReturn(Optional.empty()); + + mvc.perform(patch("/products/{id}/instock", id).param("stockQuantity", "10")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns 204-No Content when deleting a product") + void delete_returnsNoContent() throws Exception { + UUID id = UUID.randomUUID(); + doNothing().when(inventoryService).delete(id); + + mvc.perform(delete("/products/{id}", id)) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("returns a list with all the categories with products") + void fetchCategories_returnsCategories() throws Exception { + given(inventoryService.fetchCategories()).willReturn(Optional.of(List.of("food", "drinks"))); + + mvc.perform(get("/products/categories")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasItems("food", "drinks"))); } + @Test + @DisplayName("returns 404-Not Found when no category is available") + void fetchCategories_returnsNotFound() throws Exception { + given(inventoryService.fetchCategories()).willReturn(Optional.empty()); + + mvc.perform(get("/products/categories")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("returns Inventory Summary when calling for metrics") + void fetchSummary_returnsInventorySummary() throws Exception { + InventorySummaryResponse row = new InventorySummaryResponse("food", 100L, BigDecimal.valueOf(2500), BigDecimal.valueOf(25)); + given(inventoryService.fetchInventorySummary()).willReturn(Optional.of(List.of(row))); + + mvc.perform(get("/products/summary")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].category", is("food"))); + } + + @Test + @DisplayName("returns 404-Not Found when no product in stock") + void fetchSummary_returnsNotFound() throws Exception { + given(inventoryService.fetchInventorySummary()).willReturn(Optional.empty()); + + mvc.perform(get("/products/summary")) + .andExpect(status().isNotFound()); + } + + private Product sampleProduct() { + Product p = new Product(); + p.setName("Melon"); + p.setCategory("food"); + p.setUnitPrice(BigDecimal.valueOf(25)); + p.setStockQuantity(5); + return p; + } + + private ProductResponse sampleProductRes() { + return new ProductResponse( + UUID.randomUUID(), + "Watermelon", + "food", + BigDecimal.valueOf(20), + null, + 10, + LocalDateTime.now(), + LocalDateTime.now()); + } + + private ProductShortResponse sampleProductShort() { + return new ProductShortResponse( + UUID.randomUUID(), + "Watermelon", + LocalDateTime.now(), + LocalDateTime.now()); + } } diff --git a/src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java b/src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java new file mode 100644 index 0000000..52eef7a --- /dev/null +++ b/src/test/java/com/encorazone/inventory_manager/mapper/ProductMapperTests.java @@ -0,0 +1,100 @@ +package com.encorazone.inventory_manager.mapper; + +import com.encorazone.inventory_manager.domain.Product; +import com.encorazone.inventory_manager.domain.InventorySummaryInterface; +import com.encorazone.inventory_manager.domain.ProductListResponse; +import com.encorazone.inventory_manager.domain.ProductResponse; +import com.encorazone.inventory_manager.domain.ProductShortResponse; +import com.encorazone.inventory_manager.domain.InventorySummaryResponse; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class ProductMapperTests { + + @Test + void toProductShortResponse_mapsFields() { + Product p = sampleProduct(); + ProductShortResponse r = ProductMapper.toProductShortResponse(p); + assertEquals(p.getId(), r.getId()); + assertEquals(p.getName(), r.getName()); + assertEquals(p.getCreationDate(), r.getCreationDate()); + assertEquals(p.getUpdateDate(), r.getUpdateDate()); + } + + @Test + void toProductResponse_mapsFields() { + Product p = sampleProduct(); + ProductResponse r = ProductMapper.toProductResponse(p); + assertEquals(p.getId(), r.getId()); + assertEquals(p.getName(), r.getName()); + assertEquals(p.getCategory(), r.getCategory()); + assertEquals(p.getUnitPrice(), r.getUnitPrice()); + assertEquals(p.getExpirationDate(), r.getExpirationDate()); + assertEquals(p.getStockQuantity(), r.getStockQuantity()); + assertEquals(p.getCreationDate(), r.getCreationDate()); + assertEquals(p.getUpdateDate(), r.getUpdateDate()); + } + + @Test + void toProductListResponse_mapsListAndTotalPages() { + Product p1 = sampleProduct(); + Product p2 = sampleProduct(); + p2.setId(UUID.randomUUID()); + p2.setName("Another"); + ProductListResponse r = ProductMapper.toProductListResponse(List.of(p1, p2), 7); + assertEquals(2, r.getProducts().size()); + assertEquals(7, r.getTotalPages()); + assertEquals(p1.getId(), r.getProducts().get(0).getId()); + assertEquals(p2.getId(), r.getProducts().get(1).getId()); + } + + @Test + void toInventorySummaryResponse_mapsFields() { + InventorySummaryInterface row = sampleSummaryRow("food", 12L, new BigDecimal("345.67"), new BigDecimal("28.81")); + InventorySummaryResponse r = ProductMapper.toInventorySummaryResponse(row); + assertEquals("food", r.getCategory()); + assertEquals(12L, Long.valueOf(r.getProductsInStock())); + assertEquals(new BigDecimal("345.67"), r.getValueInStock()); + assertEquals(new BigDecimal("28.81"), r.getAverageValue()); + } + + @Test + void toInventorySummaryResponseList_mapsList() { + List list = ProductMapper.toInventorySummaryResponseList(List.of( + sampleSummaryRow("food", 10L, new BigDecimal("100.00"), new BigDecimal("10.00")), + sampleSummaryRow("drinks", 5L, new BigDecimal("55.50"), new BigDecimal("11.10")) + )); + assertEquals(2, list.size()); + assertEquals("food", list.get(0).getCategory()); + assertEquals("drinks", list.get(1).getCategory()); + } + + private Product sampleProduct() { + Product p = new Product(); + p.setId(UUID.randomUUID()); + p.setName("Watermelon"); + p.setCategory("food"); + p.setUnitPrice(new BigDecimal("19.99")); + p.setExpirationDate(LocalDate.now().plusDays(30)); + p.setStockQuantity(7); + p.setCreationDate(LocalDateTime.now().minusDays(1)); + p.setUpdateDate(LocalDateTime.now()); + return p; + } + + private InventorySummaryInterface sampleSummaryRow(String category, Long inStock, BigDecimal value, BigDecimal avg) { + return new InventorySummaryInterface() { + @Override public String getCategory() { return category; } + @Override public Long getProductsInStock() { return inStock; } + @Override public BigDecimal getValueInStock() { return value; } + @Override public BigDecimal getAverageValue() { return avg; } + }; + } +} diff --git a/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java b/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java index edf236a..bff1b5f 100644 --- a/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java +++ b/src/test/java/com/encorazone/inventory_manager/service/InventoryProductsFilterTests.java @@ -1,4 +1,83 @@ package com.encorazone.inventory_manager.service; -public class InventoryProductsFilterTests { +import com.encorazone.inventory_manager.domain.Product; +import org.junit.jupiter.api.Test; +import org.springframework.data.jpa.domain.Specification; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +class InventoryProductsFilterTests { + + @Test + void nameContains_builds_like_lowercased() { + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + @SuppressWarnings("unchecked") + Path path = (Path) mock(Path.class); + @SuppressWarnings("unchecked") Expression lower = (Expression) mock(Expression.class); + Predicate pred = mock(Predicate.class); + given(root.get("name")).willReturn((path)); + given(cb.lower(path)).willReturn(lower); + given(cb.like(lower, "%apple%")).willReturn(pred); + + Specification spec = InventoryProductsFilter.nameContains("Apple"); + assertSame(pred, spec.toPredicate(root, query, cb)); + } + + @Test + void categoryContains_returns_null_when_blank() { + Specification spec = InventoryProductsFilter.categoryContains(" "); + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + assertNull(spec.toPredicate(root, query, cb)); + } + + @Test + void quantityEquals_inStock_builds_gt_zero() { + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + @SuppressWarnings("unchecked") + Path path = (Path) mock(Path.class); + Predicate pred = mock(Predicate.class); + + given(root.get("stockQuantity")).willReturn(path); + given(cb.greaterThan(path, 0)).willReturn(pred); + + Specification spec = InventoryProductsFilter.quantityEquals(1); + assertNotNull(spec); + assertSame(pred, spec.toPredicate(root, query, cb)); + } + + @Test + void quantityEquals_outOfStock_builds_eq_zero() { + @SuppressWarnings("unchecked") + Root root = mock(Root.class); + CriteriaQuery query = mock(CriteriaQuery.class); + CriteriaBuilder cb = mock(CriteriaBuilder.class); + @SuppressWarnings("unchecked") + Path path = (Path) mock(Path.class); + Predicate pred = mock(Predicate.class); + + given(root.get("stockQuantity")).willReturn(path); + given(cb.equal(path, 0)).willReturn(pred); + + Specification spec = InventoryProductsFilter.quantityEquals(2); + assertNotNull(spec); + assertSame(pred, spec.toPredicate(root, query, cb)); + } } From 48e0cf1c31e0187ca7c55ad0e37e8f81ff553587 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Thu, 16 Oct 2025 22:02:49 -0600 Subject: [PATCH 26/26] Upgraded pom to avoid vulnerabilities (#26) - Updated the pom to comply with security, avoiding current vulnerabilities Signed-off-by: Leonardo Trevizo --- pom.xml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 114eb60..addac6b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.0 + 3.5.6 com.encorazone @@ -27,12 +27,14 @@ + 1.5.19 17 21.3.0.0 - 2.3.0 + 2.8.13 1.18.38 1.5.5.Final 3.14.0 + 3.18.0 @@ -86,11 +88,6 @@ lombok ${lombok.version} - - com.h2database - h2 - test - org.mapstruct mapstruct @@ -127,11 +124,17 @@ - org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-surefire-plugin + + -XX:+EnableDynamicAgentLoading -Xshare:off + +