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/controller/ProductController.java b/backend/src/main/java/com/inventory/backend/controller/ProductController.java
new file mode 100644
index 0000000..f13502e
--- /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(dto);
+ }
+
+ // PUT /products/{id}
+ @PutMapping("/{id}")
+ public Product updateProduct(@PathVariable UUID id, @RequestBody @Valid ProductDTO dto) {
+ return service.updateProduct(id,dto);
+ }
+
+ // DELETE /products/{id}
+ @DeleteMapping("/{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
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..a4c1f58
--- /dev/null
+++ 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/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
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..d6ea2e2
--- /dev/null
+++ 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.get(id));
+ }
+
+ 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.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());
+ }
+}
\ 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