mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
added server side price sorting endpoints
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package group.goforward.battlbuilder.catalog.query;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProductWithBestPrice {
|
||||
Long getId();
|
||||
UUID getUuid();
|
||||
String getName();
|
||||
String getSlug();
|
||||
String getPlatform();
|
||||
String getPartRole();
|
||||
|
||||
BigDecimal getBestPrice(); // derived
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package group.goforward.battlbuilder.controllers;
|
||||
import group.goforward.battlbuilder.services.ProductQueryService;
|
||||
import group.goforward.battlbuilder.web.dto.ProductOfferDto;
|
||||
import group.goforward.battlbuilder.web.dto.ProductSummaryDto;
|
||||
import group.goforward.battlbuilder.web.dto.catalog.ProductSort;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -23,17 +24,26 @@ public class ProductV1Controller {
|
||||
this.productQueryService = productQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Product list endpoint
|
||||
* Example:
|
||||
* /api/v1/products?platform=AR-15&partRoles=upper-receiver&priceSort=price_asc&page=0&size=50
|
||||
*
|
||||
* NOTE: do NOT use `sort=` here — Spring reserves it for Pageable sorting.
|
||||
*/
|
||||
@GetMapping
|
||||
@Cacheable(
|
||||
value = "gunbuilderProductsV1",
|
||||
key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles.toString()) + '::' + #pageable.pageNumber + '::' + #pageable.pageSize"
|
||||
key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles) + '::' + #priceSort + '::' + #pageable.pageNumber + '::' + #pageable.pageSize"
|
||||
)
|
||||
public Page<ProductSummaryDto> getProducts(
|
||||
@RequestParam(defaultValue = "AR-15") String platform,
|
||||
@RequestParam(required = false, name = "partRoles") List<String> partRoles,
|
||||
@RequestParam(name = "priceSort", defaultValue = "price_asc") String priceSort,
|
||||
@PageableDefault(size = 50) Pageable pageable
|
||||
) {
|
||||
return productQueryService.getProductsPage(platform, partRoles, pageable);
|
||||
ProductSort sortEnum = ProductSort.from(priceSort);
|
||||
return productQueryService.getProductsPage(platform, partRoles, pageable, sortEnum);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/offers")
|
||||
|
||||
@@ -3,6 +3,7 @@ package group.goforward.battlbuilder.repos;
|
||||
import group.goforward.battlbuilder.model.ImportStatus;
|
||||
import group.goforward.battlbuilder.model.Brand;
|
||||
import group.goforward.battlbuilder.model.Product;
|
||||
import group.goforward.battlbuilder.catalog.query.ProductWithBestPrice;
|
||||
|
||||
import group.goforward.battlbuilder.repos.projections.CatalogRow;
|
||||
import group.goforward.battlbuilder.web.dto.catalog.CatalogOptionRow;
|
||||
@@ -625,6 +626,28 @@ ORDER BY productCount DESC
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
// -------------------------------------------------
|
||||
// Best Price
|
||||
// -------------------------------------------------
|
||||
@Query("""
|
||||
select
|
||||
p.id as id,
|
||||
p.uuid as uuid,
|
||||
p.name as name,
|
||||
p.slug as slug,
|
||||
p.platform as platform,
|
||||
p.partRole as partRole,
|
||||
min(o.price) as bestPrice
|
||||
from Product p
|
||||
join ProductOffer o on o.product.id = p.id
|
||||
where (:platform is null or p.platform = :platform)
|
||||
and o.inStock = true
|
||||
group by p.id, p.uuid, p.name, p.slug, p.platform, p.partRole
|
||||
""")
|
||||
Page<ProductWithBestPrice> findProductsWithBestPriceInStock(
|
||||
@Param("platform") String platform,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ package group.goforward.battlbuilder.services;
|
||||
|
||||
import group.goforward.battlbuilder.web.dto.ProductOfferDto;
|
||||
import group.goforward.battlbuilder.web.dto.ProductSummaryDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import group.goforward.battlbuilder.web.dto.catalog.ProductSort;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ProductQueryService {
|
||||
|
||||
List<ProductSummaryDto> getProducts(String platform, List<String> partRoles);
|
||||
@@ -16,5 +16,10 @@ public interface ProductQueryService {
|
||||
|
||||
ProductSummaryDto getProductById(Integer productId);
|
||||
|
||||
Page<ProductSummaryDto> getProductsPage(String platform, List<String> partRoles, Pageable pageable);
|
||||
Page<ProductSummaryDto> getProductsPage(
|
||||
String platform,
|
||||
List<String> partRoles,
|
||||
Pageable pageable,
|
||||
ProductSort sort
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import group.goforward.battlbuilder.services.ProductQueryService;
|
||||
import group.goforward.battlbuilder.web.dto.ProductOfferDto;
|
||||
import group.goforward.battlbuilder.web.dto.ProductSummaryDto;
|
||||
import group.goforward.battlbuilder.web.mapper.ProductMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import group.goforward.battlbuilder.web.dto.catalog.ProductSort;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
@@ -16,6 +16,8 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Collections;
|
||||
@@ -79,24 +81,37 @@ public class ProductQueryServiceImpl implements ProductQueryService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ProductSummaryDto> getProductsPage(String platform, List<String> partRoles, Pageable pageable) {
|
||||
public Page<ProductSummaryDto> getProductsPage(
|
||||
String platform,
|
||||
List<String> partRoles,
|
||||
Pageable pageable,
|
||||
ProductSort sort
|
||||
) {
|
||||
final boolean allPlatforms = platform != null && platform.equalsIgnoreCase("ALL");
|
||||
|
||||
// IMPORTANT: ignore Pageable sorting (because we are doing our own "best price" logic)
|
||||
// If the client accidentally passes ?sort=..., Spring Data will try ordering by a Product field.
|
||||
// We'll strip it to be safe.
|
||||
Pageable safePageable = pageable;
|
||||
if (pageable != null && pageable.getSort() != null && pageable.getSort().isSorted()) {
|
||||
safePageable = Pageable.ofSize(pageable.getPageSize()).withPage(pageable.getPageNumber());
|
||||
}
|
||||
|
||||
Page<Product> productPage;
|
||||
|
||||
if (partRoles == null || partRoles.isEmpty()) {
|
||||
productPage = allPlatforms
|
||||
? productRepository.findAllWithBrand(pageable)
|
||||
: productRepository.findByPlatformWithBrand(platform, pageable);
|
||||
? productRepository.findAllWithBrand(safePageable)
|
||||
: productRepository.findByPlatformWithBrand(platform, safePageable);
|
||||
} else {
|
||||
productPage = allPlatforms
|
||||
? productRepository.findByPartRoleInWithBrand(partRoles, pageable)
|
||||
: productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles, pageable);
|
||||
? productRepository.findByPartRoleInWithBrand(partRoles, safePageable)
|
||||
: productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles, safePageable);
|
||||
}
|
||||
|
||||
List<Product> products = productPage.getContent();
|
||||
if (products.isEmpty()) {
|
||||
return Page.empty(pageable);
|
||||
return Page.empty(safePageable);
|
||||
}
|
||||
|
||||
List<Integer> productIds = products.stream().map(Product::getId).toList();
|
||||
@@ -108,6 +123,7 @@ public class ProductQueryServiceImpl implements ProductQueryService {
|
||||
.filter(o -> o.getProduct() != null && o.getProduct().getId() != null)
|
||||
.collect(Collectors.groupingBy(o -> o.getProduct().getId()));
|
||||
|
||||
// Build DTOs (same as before)
|
||||
List<ProductSummaryDto> dtos = products.stream()
|
||||
.map(p -> {
|
||||
List<ProductOffer> offersForProduct =
|
||||
@@ -122,7 +138,17 @@ public class ProductQueryServiceImpl implements ProductQueryService {
|
||||
})
|
||||
.toList();
|
||||
|
||||
return new PageImpl<>(dtos, pageable, productPage.getTotalElements());
|
||||
// Phase 3 "server-side sort by price" (within the page for now)
|
||||
Comparator<ProductSummaryDto> byPriceNullLast =
|
||||
Comparator.comparing(ProductSummaryDto::getPrice, Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
|
||||
if (sort == ProductSort.PRICE_DESC) {
|
||||
dtos = dtos.stream().sorted(byPriceNullLast.reversed()).toList();
|
||||
} else {
|
||||
dtos = dtos.stream().sorted(byPriceNullLast).toList();
|
||||
}
|
||||
|
||||
return new PageImpl<>(dtos, safePageable, productPage.getTotalElements());
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package group.goforward.battlbuilder.web.dto.catalog;
|
||||
|
||||
public enum ProductSort {
|
||||
PRICE_ASC,
|
||||
PRICE_DESC;
|
||||
|
||||
public static ProductSort from(String raw) {
|
||||
if (raw == null) return PRICE_ASC;
|
||||
String s = raw.trim().toLowerCase();
|
||||
return switch (s) {
|
||||
case "price_desc", "price-desc", "desc" -> PRICE_DESC;
|
||||
default -> PRICE_ASC;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user