From 635b32dd5c025eb09f45c7657e3c186e5e4aee0d Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 13 Jun 2025 13:15:40 -0600 Subject: [PATCH 1/7] feat: updated product model --- .../com/inventory/backend/model/Product.java | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/inventory/backend/model/Product.java b/backend/src/main/java/com/inventory/backend/model/Product.java index 32a9197..7695688 100644 --- a/backend/src/main/java/com/inventory/backend/model/Product.java +++ b/backend/src/main/java/com/inventory/backend/model/Product.java @@ -1,6 +1,5 @@ package com.inventory.backend.model; -import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; @@ -9,9 +8,94 @@ public class Product { private UUID id; private String name; private String category; - private BigDecimal number; - private LocalDate expirationDate; + private double unitPrice; private int quantityInStock; + private LocalDate expirationDate; private LocalDateTime createdAt; private LocalDateTime updatedAt; + + public Product(){} + + public Product(UUID id, String name, String category, double unitPrice, int quantityInStock, LocalDate expirationDate) { + this.id = id; + this.name = name; + this.category = category; + this.unitPrice = unitPrice; + this.quantityInStock = quantityInStock; + this.expirationDate = expirationDate; + this.createdAt = LocalDateTime.now(); + this.updatedAt = 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 double getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(double unitPrice) { + this.unitPrice = unitPrice; + } + + public int getQuantityInStock() { + return quantityInStock; + } + + public void setQuantityInStock(int quantityInStock) { + this.quantityInStock = quantityInStock; + } + + public LocalDate getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(LocalDate expirationDate) { + this.expirationDate = expirationDate; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public boolean isInStock() { + return quantityInStock > 0; + } + + public double getTotalValue() { + return unitPrice * quantityInStock; + } } \ No newline at end of file From 675523cb151eed1cf51a5ca3ca6f3ed6016925c2 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 16 Jun 2025 09:44:37 -0600 Subject: [PATCH 2/7] feat: added ProductDTO --- backend/pom.xml | 9 +++ .../com/inventory/backend/dto/ProductDTO.java | 62 +++++++++++++++++++ .../backend/model/InventoryMetrics.java | 0 .../backend/repository/ProductRepository.java | 0 4 files changed, 71 insertions(+) create mode 100644 backend/src/main/java/com/inventory/backend/dto/ProductDTO.java create mode 100644 backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java create mode 100644 backend/src/main/java/com/inventory/backend/repository/ProductRepository.java diff --git a/backend/pom.xml b/backend/pom.xml index 7abf8a1..ed6dde8 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -46,6 +46,15 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + true + diff --git a/backend/src/main/java/com/inventory/backend/dto/ProductDTO.java b/backend/src/main/java/com/inventory/backend/dto/ProductDTO.java new file mode 100644 index 0000000..fe61fc8 --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/dto/ProductDTO.java @@ -0,0 +1,62 @@ +package com.inventory.backend.dto; + +import jakarta.validation.constraints.*; +import java.time.LocalDate; + +public class ProductDTO { + @NotBlank(message = "Name is required") + @Size(max = 120, message = "Name must not exceed 120 characters") + private String name; + + @NotBlank(message = "Category is required") + private String category; + + @NotNull(message = "Unit price is required") + @PositiveOrZero(message = "Unit price must be 0 or more") + private Double unitPrice; + + @PositiveOrZero(message = "Stock must be 0 or more") + private Integer quantityInStock; + + private LocalDate expirationDate; + + 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 Double getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(Double unitPrice) { + this.unitPrice = unitPrice; + } + + public Integer getQuantityInStock() { + return quantityInStock; + } + + public void setQuantityInStock(Integer quantityInStock) { + this.quantityInStock = quantityInStock; + } + + public LocalDate getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(LocalDate expirationDate) { + this.expirationDate = expirationDate; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java new file mode 100644 index 0000000..e69de29 From 77174ee1e06f0bcf740b15062b6f4f7821c47763 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 16 Jun 2025 14:03:45 -0600 Subject: [PATCH 3/7] feat: added metrics to backend --- .../backend/model/InventoryMetrics.java | 124 ++++++++++++++++++ .../backend/repository/ProductRepository.java | 49 +++++++ .../backend/service/ProductService.java | 122 +++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 backend/src/main/java/com/inventory/backend/service/ProductService.java diff --git a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java index e69de29..b3b064b 100644 --- a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java +++ b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java @@ -0,0 +1,124 @@ +package com.inventory.backend.model; + +import java.util.*; +import java.util.stream.Collectors; + +public class InventoryMetrics { + private int totalInStock; + private double totalValue; + private double averagePrice; + + private Map byCategory; + + public InventoryMetrics() {} + + public InventoryMetrics(int totalInStock, double totalValue, double averagePrice, Map byCategory) { + this.totalInStock = totalInStock; + this.totalValue = totalValue; + this.averagePrice = averagePrice; + this.byCategory = byCategory; + } + + public static InventoryMetrics fromProductList(List products) { + int totalStock = products.stream().mapToInt(Product::getQuantityInStock).sum(); + + double totalValue = products.stream() + .maptoDouble(Product::getTotalValue) + .sum(); + + List inStock = products.stream() + .filter(p -> p.getQuantityInStock() > 0) + .collect(Collectors.toList()); + + double averagePrice = inStock.isEmpty() ? 0: + inStock.stream().mapToDouble(Product::getUnitPrice).average().orElse(0); + + Map categoryMetrics = products.stream() + .collect(Collectors.groupingBy( + Product::getCategory, + Collectors.collectingAndThen(Collectors.toList(), CategoryMetrics::fromProductList) + )); + return new InventoryMetrics(totalStock, totalValue, averagePrice, categoryMetrics); + } + + public int getTotalInStock() { + return totalInStock; + } + + public void setTotalInStock(int totalInStock) { + this.totalInStock = totalInStock; + } + + public double getTotalValue() { + return totalValue; + } + + public void setTotalValue(double totalValue) { + this.totalValue = totalValue; + } + + public double getAveragePrice() { + return averagePrice; + } + + public void setAveragePrice(double averagePrice) { + this.averagePrice = averagePrice; + } + + public Map getByCategory() { + return byCategory; + } + + public void setByCategory(Map byCategory) { + this.byCategory = byCategory; + } + + public static class CategoryMetrics { + private int inStock; + private double totalValue; + private double averagePrice; + + public static CategoryMetrics fromProductList(List products) { + int stock = products.stream().mapToInt(Product::getQuantityInStock).sum(); + double value = products.stream().mapToDouble(Product::getTotalValue).sum(); + + List inStock = products.stream().filter(p -> p.getQuantityInStock() > 0).toList(); + double avg = inStock.isEmpty() ? 0 : + inStock.stream().mapToDouble(Product::getUnitPrice).average().orElse(0); + + return new CategoryMetrics(stock, value, avg); + } + + public CategoryMetrics() {} + + public CategoryMetrics(int inStock, double totalValue, double averagePrice) { + this.inStock = inStock; + this.totalValue = totalValue; + this.averagePrice = averagePrice; + } + + public double getTotalValue() { + return totalValue; + } + + public void setTotalValue(double totalValue) { + this.totalValue = totalValue; + } + + public double getAveragePrice() { + return averagePrice; + } + + public void setAveragePrice(double averagePrice) { + this.averagePrice = averagePrice; + } + + public int getInStock() { + return inStock; + } + + public void setInStock(int inStock) { + this.inStock = inStock; + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index e69de29..477e225 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -0,0 +1,49 @@ +package com.inventory.backend.repository; + +import com.inventory.backend.model.Product; +import org.springframework.stereotype.Repository; + +import java.util.*; +import java.util.stream.Collectors; + +@Repository +public class ProductRepository { + private final Map products = new HashMap<>(); + + public List findAll(){ + return new ArrayList<>(products.values()); + } + + public Optional findById(UUID id) { + return Optional.ofNullable(products) + } + + public Product save(Product product) { + products.put(product.getId(), product); + return product; + } + + public void delete(UUID id) { + products.remove(id); + } + + public boolean existsById(UUID id) { + return products.containsKey(id); + } + + public void clear() { + products.clear(); + } + + public List findByNameOrCategoryOrAvailability( + Optional nameFilter, + Optional> categoryFilter, + Optional availability, + ) { + return products.values().stream() + .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLoweCase())).orElse(true)) + .filter(p -> categoryFilter.map(categories -> categories.contains(p.category())).orElse(true)) + .filter(p -> availability.map(avail -> avail == p.isInStock()).orElse(true)) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/inventory/backend/service/ProductService.java b/backend/src/main/java/com/inventory/backend/service/ProductService.java new file mode 100644 index 0000000..555a1f7 --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/service/ProductService.java @@ -0,0 +1,122 @@ +package com.inventory.backend.service; + +import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.model.Product; +import com.inventory.backend.model.InventoryMetrics; +import com.inventory.backend.repository.ProductRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class ProductService { + private final ProductRepository repository; + + public ProductService(ProductRepository repository) { + this.repository = repository; + } + + public List getFilteredAndSortedProducts( + Optional nameFilter, + Optional> categoryFilter, + Optional availability, + Optional sortBy, + Optional sortBy2, + boolean ascending, + boolean ascending2, + int page, + int size + ){ + List filtered = repository.findByNameOrCategoryOrAvailability(nameFilter, categoryFilter,availability); + + Comparator comparator = getComparator(sortBy.orElse(null), ascending); + if(sortBy2.isPresent()) { + comparator = comparator.thenComparing(getComparator(sortBy2.get(), ascending2)); + } + + return filtered.stream() + .sorted(comparator) + .skip((long) page * size) + .limit(size) + .collect(Collectors.toList()); + } + + private Comparator getComparator(String sortField, boolean ascending) { + Comparator comparator; + + switch (sortField) { + case "name": + comparator = Comparator.comparing(Product::getName, String.CASE_INSENSITIVE_ORDER); + break; + case "category": + comparator = Comparator.comparing(Product::getCategory, String.CASE_INSENSITIVE_ORDER); + break; + case "unitPrice": + comparator = Comparator.comparingDouble(Product::getUnitPrice); + break; + case "quantityInStock": + comparator = Comparator.comparingInt(Product::getQuantityInStock); + break; + case "expirationDate": + comparator = Comparator.comparing(p -> Optional.ofNullable(p.getExpirationDate()).orElse(null), + Comparator.nullsLast(Comparator.naturalOrder())); + break; + default: + comparator = Comparator.comparing(Product::getName); + } + return ascending ? comparator : comparator.reversed(); + } + + public Product createProduct(ProductDTO dto) { + Product product = new Product( + UUID.randomUUID(), + dto.getName(), + dto.getCategory(), + dto.getUnitPrice(), + dto.getQuantityInStock() != null ? dto.getQuantityInStock() : 0, + dto.getExpirationDate() + ); + return repository.save(product); + } + + public Product updateProduct(UUID id, ProductDTO dto) { + Product product = repository.findById(id).orElseThrow(() -> new NoSuchElementException("Product not found")); + + product.setName(dto.getName()); + product.setCategory(dto.getCategory()); + product.setUnitPrice(dto.getUnitPrice()); + product.setQuantityInStock(dto.getQuantityInStock()); + product.setExpirationDate(dto.getExpirationDate()); + product.setUpdatedAt(LocalDateTime.now()); + + return repository.save(product); + } + + public void deleteProduct(UUID id) { + if(!repository.existsById(id)) { + throw new NoSuchElementException("Product not found"); + } + repository.delete(id); + } + + public void markOutOfStock(UUID id) { + Product product = repository.findById(id).orElseThrow(() -> new NoSuchElementException("Product not found")); + product.setQuantityInStock(0); + product.setUpdatedAt(LocalDateTime.now()); + repository.save(product); + } + + public void markInStock(UUID id, int defaultQuantity) { + Product product = repository.findById(id).orElseThrow(() -> new NoSuchElementException("Product not found")); + product.setQuantityInStock(defaultQuantity); + product.setUpdatedAt(LocalDateTime.now()); + repository.save(product); + } + + public InventoryMetrics getMetrics() { + List all = repository.findAll(); + return InventoryMetrics.fromProductList(all); + } +} \ No newline at end of file From 66f3018f006d95932b2aa6c4b791bb59ba9e356e Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:17:31 -0600 Subject: [PATCH 4/7] feat: added Controller --- .../backend/controller/ProductController.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 backend/src/main/java/com/inventory/backend/controller/ProductController.java diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java new file mode 100644 index 0000000..9bf349b --- /dev/null +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -0,0 +1,84 @@ +package com.inventory.backend.controller; + +import com.inventory.backend.dto.ProductDTO; +import com.inventory.backend.model.InventoryMetrics; +import com.inventory.backend.model.Product; +import com.inventory.backend.service.ProductService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@RestController +@RequestMapping("/products") +@CrossOrigin(origins = "http://localhost:8080") +public class ProductController { + private final ProductService service; + + public ProductController(ProductService service) { + this.service = service; + } + + // GET /products + @GetMapping + public List getProducts( + @RequestParam Optional name, + @RequestParam Optional> category, + @RequestParam Optional availability, + @RequestParam Optional sortBy, + @RequestParam Optional sortBy2, + @RequestParam(defaultValue = "true") boolean asc, + @RequestParam(defaultValue = "true") boolean asc2, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + ){ + Set categorySet = category.map(HashSet::new).orElse(null); + return service.getFilteredAndSortedProducts( + name, + Optional.ofNullable(categorySet), + availability, + sortBy, + sortBy2, + asc, + asc2, + page, + size + ); + } + + // POST /products + @PostMapping + public Product createProduct(@RequestBody @Valid ProductDTO dto) { + return service.createProduct(id, dto); + } + + // PUT /products/{id} + @PutMapping("/{id}") + public Product updateProduct(@PathVariable UUID id, @RequestBody @Valid ProductDTO dto) { + return service.updateProduct(id,dto); + } + + // DELETE /products/{id} + @DelteMapping("/{id}") + public void deleteProduct(@PathVariable UUID id) { + service.deleteProduct(id); + } + + // POST /products/{id}/outofstock + @PostMapping("/{id}/outofstock") + public void markOutOfStock(@PathVariable UUID id) { + service.markOutOfStock(id); + } + + // PUT /products/{id}/instock?defaultQuantity=10 + @PutMapping("/{id}/instock") + public void markInStock(@PathVariable UUID id, @RequestParam(defaultValue = "10") int defaultQuantity) { + service.markInStock(id, defaultQuantity); + } + + // GET /products/metrics + @GetMapping("/metrics") + public InventoryMetrics getMetrics() { + return service.getMetrics(); + } +} \ No newline at end of file From fedb3d04fb157d784e30d7364d9b86570b04b953 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:34:06 -0600 Subject: [PATCH 5/7] fix: added missing ; --- .../com/inventory/backend/controller/ProductController.java | 2 +- .../com/inventory/backend/repository/ProductRepository.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index 9bf349b..939a4d0 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -30,7 +30,7 @@ public List getProducts( @RequestParam(defaultValue = "true") boolean asc, @RequestParam(defaultValue = "true") boolean asc2, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "10") int size ){ Set categorySet = category.map(HashSet::new).orElse(null); return service.getFilteredAndSortedProducts( diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index 477e225..16517ca 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -15,7 +15,7 @@ public List findAll(){ } public Optional findById(UUID id) { - return Optional.ofNullable(products) + return Optional.ofNullable(products); } public Product save(Product product) { @@ -38,7 +38,7 @@ public void clear() { public List findByNameOrCategoryOrAvailability( Optional nameFilter, Optional> categoryFilter, - Optional availability, + Optional availability ) { return products.values().stream() .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLoweCase())).orElse(true)) From 773f52b29ac29738500dbd7260dbbd6949d6d297 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:36:40 -0600 Subject: [PATCH 6/7] fix: corrected typo --- .../com/inventory/backend/controller/ProductController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index 939a4d0..56b2819 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -59,7 +59,7 @@ public Product updateProduct(@PathVariable UUID id, @RequestBody @Valid ProductD } // DELETE /products/{id} - @DelteMapping("/{id}") + @DeleteMapping("/{id}") public void deleteProduct(@PathVariable UUID id) { service.deleteProduct(id); } From 1da6dbab9cc8215b1e0ddd8c8f26b0efbe79acf7 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Tue, 17 Jun 2025 11:48:01 -0600 Subject: [PATCH 7/7] fix: corrected typos --- .../com/inventory/backend/controller/ProductController.java | 2 +- .../java/com/inventory/backend/model/InventoryMetrics.java | 2 +- .../com/inventory/backend/repository/ProductRepository.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/inventory/backend/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java index 56b2819..f13502e 100644 --- a/backend/src/main/java/com/inventory/backend/controller/ProductController.java +++ b/backend/src/main/java/com/inventory/backend/controller/ProductController.java @@ -49,7 +49,7 @@ public List getProducts( // POST /products @PostMapping public Product createProduct(@RequestBody @Valid ProductDTO dto) { - return service.createProduct(id, dto); + return service.createProduct(dto); } // PUT /products/{id} diff --git a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java index b3b064b..a4c1f58 100644 --- a/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java +++ b/backend/src/main/java/com/inventory/backend/model/InventoryMetrics.java @@ -23,7 +23,7 @@ public static InventoryMetrics fromProductList(List products) { int totalStock = products.stream().mapToInt(Product::getQuantityInStock).sum(); double totalValue = products.stream() - .maptoDouble(Product::getTotalValue) + .mapToDouble(Product::getTotalValue) .sum(); List inStock = products.stream() diff --git a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java index 16517ca..d6ea2e2 100644 --- a/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/inventory/backend/repository/ProductRepository.java @@ -15,7 +15,7 @@ public List findAll(){ } public Optional findById(UUID id) { - return Optional.ofNullable(products); + return Optional.ofNullable(products.get(id)); } public Product save(Product product) { @@ -41,7 +41,7 @@ public List findByNameOrCategoryOrAvailability( Optional availability ) { return products.values().stream() - .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLoweCase())).orElse(true)) + .filter(p -> nameFilter.map(name -> p.getName().toLowerCase().contains(name.toLowerCase())).orElse(true)) .filter(p -> categoryFilter.map(categories -> categories.contains(p.category())).orElse(true)) .filter(p -> availability.map(avail -> avail == p.isInStock()).orElse(true)) .collect(Collectors.toList());