diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java index 92af9b4..d50e880 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -16,16 +16,16 @@ @Repository public interface CustomerRepository extends JpaRepository { - @Query("SELECT c.id AS customerId, c.name AS customerName, " - + "COALESCE(SUM(o.totalAmount), 0) AS totalSpending " - + "FROM Customer c " - + "LEFT JOIN c.orders o " - + "WITH o.finalStatusTimestamp BETWEEN :startDate AND :endDate " - + "AND o.status = 'DELIVERED' " - + "GROUP BY c.id, c.name " - + "ORDER BY totalSpending DESC") - Page findTopSpenders( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate, - Pageable pageable); + @Query("SELECT c.id AS customerId, c.name AS customerName, " + + "COALESCE(SUM(o.totalAmount), 0) AS totalSpending " + + "FROM Customer c " + + "INNER JOIN c.orders o " + + "WITH o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + + "AND o.status = 'DELIVERED' " + + "GROUP BY c.id, c.name " + + "ORDER BY totalSpending DESC") + Page findTopSpenders( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + Pageable pageable); } diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java index 6759b77..90795ce 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java @@ -1,6 +1,7 @@ package com.Podzilla.analytics.repositories; import java.util.List; +import java.util.UUID; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -14,27 +15,27 @@ @Repository public interface ProductSnapshotRepository - extends JpaRepository { + extends JpaRepository { - @Query("SELECT p.category AS category, " - + "SUM(s.quantity * p.cost) AS totalStockValue " - + "FROM ProductSnapshot s " - + "JOIN s.product p " - + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " - + " FROM ProductSnapshot s2 " - + " WHERE s2.product.id = s.product.id) " - + "GROUP BY p.category") - List getInventoryValueByCategory(); + @Query("SELECT p.category AS category, " + + "SUM(s.quantity * p.cost) AS totalStockValue " + + "FROM ProductSnapshot s " + + "JOIN s.product p " + + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " + + "FROM ProductSnapshot s2 " + + "WHERE s2.product.id = s.product.id) " + + "GROUP BY p.category") + List getInventoryValueByCategory(); - @Query("SELECT p.id AS productId, " - + "p.name AS productName, " - + "s.quantity AS currentQuantity, " - + "p.lowStockThreshold AS threshold " - + "FROM ProductSnapshot s " - + "JOIN s.product p " - + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " - + " FROM ProductSnapshot s2 " - + " WHERE s2.product.id = s.product.id) " - + "AND s.quantity <= p.lowStockThreshold") - Page getLowStockProducts(Pageable pageable); + @Query("SELECT p.id AS productId, " + + "p.name AS productName, " + + "s.quantity AS currentQuantity, " + + "p.lowStockThreshold AS threshold " + + "FROM ProductSnapshot s " + + "JOIN s.product p " + + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " + + "FROM ProductSnapshot s2 " + + "WHERE s2.product.id = s.product.id) " + + "AND s.quantity <= p.lowStockThreshold") + Page getLowStockProducts(Pageable pageable); } diff --git a/src/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java new file mode 100644 index 0000000..a39e7bf --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java @@ -0,0 +1,221 @@ +package com.Podzilla.analytics.controllers; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.hamcrest.Matchers.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.transaction.annotation.Transactional; + +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.RegionRepository; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +class CustomerReportControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private RegionRepository regionRepository; + + @Autowired + private EntityManager entityManager; + + private static final DateTimeFormatter ISO_LOCAL_DATE = DateTimeFormatter.ISO_LOCAL_DATE; + + private Customer customer1; + private Customer customer2; + private Region region1; + private Order order1; + private Order order2; + private Order order3; + + @BeforeEach + void setUp() { + orderRepository.deleteAll(); + customerRepository.deleteAll(); + regionRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + // Create test region + region1 = regionRepository.save(Region.builder() + .city("Sample City") + .state("Sample State") + .country("Sample Country") + .postalCode("12345") + .build()); + + // Create test customers + customer1 = customerRepository.save(Customer.builder() + .id(UUID.randomUUID()) + .name("John Doe") + .build()); + + customer2 = customerRepository.save(Customer.builder() + .id(UUID.randomUUID()) + .name("Jane Smith") + .build()); + + // Create test orders + order1 = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("1000.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(2)) + .status(Order.OrderStatus.DELIVERED) + .customer(customer1) + .region(region1) + .build()); + + order2 = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("500.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(1)) + .status(Order.OrderStatus.DELIVERED) + .customer(customer1) + .region(region1) + .build()); + + order3 = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("2000.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(3)) + .status(Order.OrderStatus.DELIVERED) + .customer(customer2) + .region(region1) + .build()); + + entityManager.flush(); + entityManager.clear(); + } + + @AfterEach + void tearDown() { + orderRepository.deleteAll(); + customerRepository.deleteAll(); + regionRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void contextLoads() { + } + + @Test + void getTopSpenders_ShouldReturnListOfTopSpenders() throws Exception { + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now(); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].customerName").value(customer2.getName())) + .andExpect(jsonPath("$[0].totalSpending").value(closeTo(order3.getTotalAmount().doubleValue(), 0.01))) + .andExpect(jsonPath("$[1].customerName").value(customer1.getName())) + .andExpect(jsonPath("$[1].totalSpending") + .value(closeTo(order1.getTotalAmount().add(order2.getTotalAmount()).doubleValue(), 0.01))); + } + + @Test + void getTopSpenders_ShouldReturnEmptyListWhenNoOrdersInDateRange() throws Exception { + LocalDate startDate = LocalDate.now().plusDays(1); + LocalDate endDate = LocalDate.now().plusDays(2); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + void getTopSpenders_ShouldHandlePagination() throws Exception { + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now(); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "1")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].customerName").value(customer2.getName())) + .andExpect(jsonPath("$[0].totalSpending").value(closeTo(order3.getTotalAmount().doubleValue(), 0.01))); + } + + @Test + void getTopSpenders_ShouldExcludeFailedOrders() throws Exception { + Order failedOrder = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("3000.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(1)) + .status(Order.OrderStatus.DELIVERY_FAILED) + .customer(customer1) + .region(region1) + .build()); + + entityManager.flush(); + entityManager.clear(); + + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now(); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].customerName").value(customer2.getName())) + .andExpect(jsonPath("$[0].totalSpending").value(closeTo(order3.getTotalAmount().doubleValue(), 0.01))) + .andExpect(jsonPath("$[1].customerName").value(customer1.getName())) + .andExpect(jsonPath("$[1].totalSpending") + .value(closeTo(order1.getTotalAmount().add(order2.getTotalAmount()).doubleValue(), 0.01))) + .andExpect(jsonPath("$[1].totalSpending").value(not(closeTo(order1.getTotalAmount() + .add(order2.getTotalAmount()).add(failedOrder.getTotalAmount()).doubleValue(), 0.01)))); + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java new file mode 100644 index 0000000..a4af6b3 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java @@ -0,0 +1,210 @@ +package com.Podzilla.analytics.controllers; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.hamcrest.Matchers.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.transaction.annotation.Transactional; + +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.ProductSnapshot; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.ProductSnapshotRepository; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +class InventoryReportControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private ProductSnapshotRepository productSnapshotRepository; + + @Autowired + private EntityManager entityManager; + + private Product electronicsProduct; + private Product clothingProduct; + + @BeforeEach + void setUp() { + productSnapshotRepository.deleteAll(); + productRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + // Create test products + electronicsProduct = productRepository.save(Product.builder() + .id(UUID.randomUUID()) + .name("Laptop") + .category("Electronics") + .cost(new BigDecimal("1000.00")) + .lowStockThreshold(10) + .build()); + + clothingProduct = productRepository.save(Product.builder() + .id(UUID.randomUUID()) + .name("T-Shirt") + .category("Clothing") + .cost(new BigDecimal("20.00")) + .lowStockThreshold(5) + .build()); + + entityManager.flush(); + entityManager.clear(); + + // Create product snapshots in a separate transaction + ProductSnapshot electronicsSnapshot = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(electronicsProduct) + .quantity(15) + .build(); + + ProductSnapshot clothingSnapshot = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(clothingProduct) + .quantity(3) // Below threshold + .build(); + + productSnapshotRepository.save(electronicsSnapshot); + productSnapshotRepository.save(clothingSnapshot); + entityManager.flush(); + entityManager.clear(); + } + + @AfterEach + void tearDown() { + productSnapshotRepository.deleteAll(); + productRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void contextLoads() { + } + + @Test + void getInventoryValueByCategory_ShouldReturnListOfCategoryValues() throws Exception { + mockMvc.perform(get("/inventory-analytics/value/by-category")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.category == '" + electronicsProduct.getCategory() + + "')].totalStockValue") + .value(hasItem(closeTo(electronicsProduct.getCost() + .multiply(new BigDecimal("15")) + .doubleValue(), 0.01)))) + .andExpect(jsonPath("$[?(@.category == '" + clothingProduct.getCategory() + + "')].totalStockValue") + .value(hasItem(closeTo(clothingProduct.getCost() + .multiply(new BigDecimal("3")) + .doubleValue(), 0.01)))); + } + + @Test + void getLowStockProducts_ShouldReturnOnlyProductsBelowThreshold() throws Exception { + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productName").value(clothingProduct.getName())) + .andExpect(jsonPath("$.content[0].currentQuantity").value(3)) + .andExpect(jsonPath("$.content[0].threshold") + .value(clothingProduct.getLowStockThreshold())); + } + + @Test + void getLowStockProducts_ShouldReturnEmptyListWhenNoProductsBelowThreshold() throws Exception { + // Create a new snapshot with quantity above threshold + ProductSnapshot newSnapshot = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(clothingProduct) + .quantity(clothingProduct.getLowStockThreshold() + 1) + .build(); + + productSnapshotRepository.save(newSnapshot); + entityManager.flush(); + entityManager.clear(); + + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(0))); + } + + @Test + void getLowStockProducts_ShouldHandlePagination() throws Exception { + Product product2 = productRepository.save(Product.builder() + .id(UUID.randomUUID()) + .name("Jeans") + .category(clothingProduct.getCategory()) + .cost(new BigDecimal("50.00")) + .lowStockThreshold(8) + .build()); + + entityManager.flush(); + entityManager.clear(); + + ProductSnapshot snapshot2 = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(product2) + .quantity(5) + .build(); + + productSnapshotRepository.save(snapshot2); + entityManager.flush(); + entityManager.clear(); + + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "0") + .param("size", "1")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productName").value(clothingProduct.getName())) + .andExpect(jsonPath("$.content[0].currentQuantity").value(3)) + .andExpect(jsonPath("$.content[0].threshold") + .value(clothingProduct.getLowStockThreshold())) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.totalPages").value(2)); + + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "1") + .param("size", "1")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productName").value(product2.getName())) + .andExpect(jsonPath("$.content[0].currentQuantity").value(5)) + .andExpect(jsonPath("$.content[0].threshold").value(product2.getLowStockThreshold())) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.totalPages").value(2)); + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java new file mode 100644 index 0000000..ccff387 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java @@ -0,0 +1,165 @@ +package com.Podzilla.analytics.services; + +import com.Podzilla.analytics.api.dtos.customer.CustomersTopSpendersResponse; +import com.Podzilla.analytics.api.projections.customer.CustomersTopSpendersProjection; +import com.Podzilla.analytics.repositories.CustomerRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CustomerReportServiceTest { + + @Mock + private CustomerRepository customerRepository; + + @InjectMocks + private CustomerAnalyticsService customerAnalyticsService; + + private LocalDate testStartDate; + private LocalDate testEndDate; + private LocalDateTime expectedStartDateTime; + private LocalDateTime expectedEndDateTime; + + @BeforeEach + void setUp() { + testStartDate = LocalDate.of(2024, 1, 1); + testEndDate = LocalDate.of(2024, 1, 31); + expectedStartDateTime = testStartDate.atStartOfDay(); + expectedEndDateTime = testEndDate.atTime(LocalTime.MAX); + } + + private CustomersTopSpendersProjection createMockProjection( + UUID customerId, String customerName, BigDecimal totalSpending) { + CustomersTopSpendersProjection mockProjection = Mockito.mock(CustomersTopSpendersProjection.class); + Mockito.lenient().when(mockProjection.getCustomerId()).thenReturn(customerId); + Mockito.lenient().when(mockProjection.getCustomerName()).thenReturn(customerName); + Mockito.lenient().when(mockProjection.getTotalSpending()).thenReturn(totalSpending); + return mockProjection; + } + + @Test + void getTopSpenders_shouldReturnCorrectSpendersForMultipleCustomers() { + // Arrange + UUID customerId1 = UUID.randomUUID(); + UUID customerId2 = UUID.randomUUID(); + CustomersTopSpendersProjection janeData = createMockProjection( + customerId1, "Jane", new BigDecimal("5000.00")); + CustomersTopSpendersProjection johnData = createMockProjection( + customerId2, "John", new BigDecimal("3000.00")); + + Page mockPage = new PageImpl<>( + Arrays.asList(janeData, johnData)); + + when(customerRepository.findTopSpenders( + any(LocalDateTime.class), + any(LocalDateTime.class), + any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + List result = customerAnalyticsService + .getTopSpenders(testStartDate, testEndDate, 0, 10); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + CustomersTopSpendersResponse janeResponse = result.stream() + .filter(r -> r.getCustomerName().equals("Jane")) + .findFirst().orElse(null); + assertNotNull(janeResponse); + assertEquals(customerId1, janeResponse.getCustomerId()); + assertEquals(new BigDecimal("5000.00"), janeResponse.getTotalSpending()); + + CustomersTopSpendersResponse johnResponse = result.stream() + .filter(r -> r.getCustomerName().equals("John")) + .findFirst().orElse(null); + assertNotNull(johnResponse); + assertEquals(customerId2, johnResponse.getCustomerId()); + assertEquals(new BigDecimal("3000.00"), johnResponse.getTotalSpending()); + + // Verify repository method was called with correct arguments + Mockito.verify(customerRepository).findTopSpenders( + expectedStartDateTime, + expectedEndDateTime, + PageRequest.of(0, 10)); + } + + @Test + void getTopSpenders_shouldReturnEmptyListWhenNoData() { + // Arrange + Page emptyPage = new PageImpl<>(Collections.emptyList()); + when(customerRepository.findTopSpenders( + any(LocalDateTime.class), + any(LocalDateTime.class), + any(PageRequest.class))) + .thenReturn(emptyPage); + + // Act + List result = customerAnalyticsService + .getTopSpenders(testStartDate, testEndDate, 0, 10); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(customerRepository).findTopSpenders( + expectedStartDateTime, + expectedEndDateTime, + PageRequest.of(0, 10)); + } + + @Test + void getTopSpenders_shouldHandlePagination() { + // Arrange + UUID customerId1 = UUID.randomUUID(); + CustomersTopSpendersProjection janeData = createMockProjection( + customerId1, "Jane", new BigDecimal("5000.00")); + + Page mockPage = new PageImpl<>( + Collections.singletonList(janeData)); + + when(customerRepository.findTopSpenders( + any(LocalDateTime.class), + any(LocalDateTime.class), + any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + List result = customerAnalyticsService + .getTopSpenders(testStartDate, testEndDate, 0, 1); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Jane", result.get(0).getCustomerName()); + assertEquals(new BigDecimal("5000.00"), result.get(0).getTotalSpending()); + + Mockito.verify(customerRepository).findTopSpenders( + expectedStartDateTime, + expectedEndDateTime, + PageRequest.of(0, 1)); + } +} diff --git a/src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java new file mode 100644 index 0000000..ce60a56 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java @@ -0,0 +1,195 @@ +package com.Podzilla.analytics.services; + +import com.Podzilla.analytics.api.dtos.inventory.InventoryValueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.inventory.LowStockProductResponse; +import com.Podzilla.analytics.api.projections.inventory.InventoryValueByCategoryProjection; +import com.Podzilla.analytics.api.projections.inventory.LowStockProductProjection; +import com.Podzilla.analytics.repositories.ProductSnapshotRepository; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class InventoryReportServiceTest { + + @Mock + private ProductSnapshotRepository inventoryRepo; + + @InjectMocks + private InventoryAnalyticsService inventoryAnalyticsService; + + private InventoryValueByCategoryProjection createMockInventoryProjection( + String category, BigDecimal totalStockValue) { + InventoryValueByCategoryProjection mockProjection = Mockito.mock(InventoryValueByCategoryProjection.class); + Mockito.lenient().when(mockProjection.getCategory()).thenReturn(category); + Mockito.lenient().when(mockProjection.getTotalStockValue()).thenReturn(totalStockValue); + return mockProjection; + } + + private LowStockProductProjection createMockLowStockProjection( + UUID productId, String productName, Long currentQuantity, Long threshold) { + LowStockProductProjection mockProjection = Mockito.mock(LowStockProductProjection.class); + Mockito.lenient().when(mockProjection.getProductId()).thenReturn(productId); + Mockito.lenient().when(mockProjection.getProductName()).thenReturn(productName); + Mockito.lenient().when(mockProjection.getCurrentQuantity()).thenReturn(currentQuantity); + Mockito.lenient().when(mockProjection.getThreshold()).thenReturn(threshold); + return mockProjection; + } + + @Test + void getInventoryValueByCategory_shouldReturnCorrectValuesForMultipleCategories() { + // Arrange + InventoryValueByCategoryProjection electronicsData = createMockInventoryProjection( + "Electronics", new BigDecimal("50000.00")); + InventoryValueByCategoryProjection clothingData = createMockInventoryProjection( + "Clothing", new BigDecimal("20000.00")); + + when(inventoryRepo.getInventoryValueByCategory()) + .thenReturn(Arrays.asList(electronicsData, clothingData)); + + // Act + List result = inventoryAnalyticsService + .getInventoryValueByCategory(); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + InventoryValueByCategoryResponse electronicsResponse = result.stream() + .filter(r -> r.getCategory().equals("Electronics")) + .findFirst().orElse(null); + assertNotNull(electronicsResponse); + assertEquals(new BigDecimal("50000.00"), electronicsResponse.getTotalStockValue()); + + InventoryValueByCategoryResponse clothingResponse = result.stream() + .filter(r -> r.getCategory().equals("Clothing")) + .findFirst().orElse(null); + assertNotNull(clothingResponse); + assertEquals(new BigDecimal("20000.00"), clothingResponse.getTotalStockValue()); + + Mockito.verify(inventoryRepo).getInventoryValueByCategory(); + } + + @Test + void getInventoryValueByCategory_shouldReturnEmptyListWhenNoData() { + // Arrange + when(inventoryRepo.getInventoryValueByCategory()) + .thenReturn(Collections.emptyList()); + + // Act + List result = inventoryAnalyticsService + .getInventoryValueByCategory(); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(inventoryRepo).getInventoryValueByCategory(); + } + + @Test + void getLowStockProducts_shouldReturnCorrectProducts() { + // Arrange + UUID productId1 = UUID.randomUUID(); + UUID productId2 = UUID.randomUUID(); + LowStockProductProjection product1Data = createMockLowStockProjection( + productId1, "Laptop", 5L, 10L); + LowStockProductProjection product2Data = createMockLowStockProjection( + productId2, "Mouse", 2L, 5L); + + Page mockPage = new PageImpl<>( + Arrays.asList(product1Data, product2Data)); + + when(inventoryRepo.getLowStockProducts(any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + Page result = inventoryAnalyticsService + .getLowStockProducts(0, 10); + + // Assert + assertNotNull(result); + assertEquals(2, result.getContent().size()); + + LowStockProductResponse product1Response = result.getContent().stream() + .filter(p -> p.getProductName().equals("Laptop")) + .findFirst().orElse(null); + assertNotNull(product1Response); + assertEquals(productId1, product1Response.getProductId()); + assertEquals(5, product1Response.getCurrentQuantity()); + assertEquals(10, product1Response.getThreshold()); + + LowStockProductResponse product2Response = result.getContent().stream() + .filter(p -> p.getProductName().equals("Mouse")) + .findFirst().orElse(null); + assertNotNull(product2Response); + assertEquals(productId2, product2Response.getProductId()); + assertEquals(2, product2Response.getCurrentQuantity()); + assertEquals(5, product2Response.getThreshold()); + + Mockito.verify(inventoryRepo).getLowStockProducts(PageRequest.of(0, 10)); + } + + @Test + void getLowStockProducts_shouldReturnEmptyPageWhenNoData() { + // Arrange + Page emptyPage = new PageImpl<>(Collections.emptyList()); + when(inventoryRepo.getLowStockProducts(any(PageRequest.class))) + .thenReturn(emptyPage); + + // Act + Page result = inventoryAnalyticsService + .getLowStockProducts(0, 10); + + // Assert + assertNotNull(result); + assertTrue(result.getContent().isEmpty()); + + Mockito.verify(inventoryRepo).getLowStockProducts(PageRequest.of(0, 10)); + } + + @Test + void getLowStockProducts_shouldHandlePagination() { + // Arrange + UUID productId = UUID.randomUUID(); + LowStockProductProjection productData = createMockLowStockProjection( + productId, "Laptop", 5L, 10L); + + Page mockPage = new PageImpl<>( + Collections.singletonList(productData)); + + when(inventoryRepo.getLowStockProducts(any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + Page result = inventoryAnalyticsService + .getLowStockProducts(0, 1); + + // Assert + assertNotNull(result); + assertEquals(1, result.getContent().size()); + assertEquals("Laptop", result.getContent().get(0).getProductName()); + assertEquals(5, result.getContent().get(0).getCurrentQuantity()); + assertEquals(10, result.getContent().get(0).getThreshold()); + + Mockito.verify(inventoryRepo).getLowStockProducts(PageRequest.of(0, 1)); + } +}