Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@
@Repository
public interface CustomerRepository extends JpaRepository<Customer, UUID> {

@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<CustomersTopSpendersProjection> 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 "
Copy link

Copilot AI May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom query for top spenders lacks an ORDER BY clause, which can lead to nondeterministic result ordering; add ORDER BY COALESCE(SUM(o.totalAmount), 0) DESC to ensure consistent sorting by total spending.

Copilot uses AI. Check for mistakes.
+ "WITH o.finalStatusTimestamp BETWEEN :startDate AND :endDate "
+ "AND o.status = 'DELIVERED' "
+ "GROUP BY c.id, c.name "
+ "ORDER BY totalSpending DESC")
Page<CustomersTopSpendersProjection> findTopSpenders(
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,27 +15,27 @@

@Repository
public interface ProductSnapshotRepository
extends JpaRepository<ProductSnapshot, Long> {
extends JpaRepository<ProductSnapshot, UUID> {

@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<InventoryValueByCategoryProjection> 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<InventoryValueByCategoryProjection> 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<LowStockProductProjection> 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<LowStockProductProjection> getLowStockProducts(Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -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))));
}
}
Loading