mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-21 01:01:05 -05:00
added product description formatter, v1controller now includes all data needed for details page
This commit is contained in:
@@ -5,10 +5,14 @@ import group.goforward.battlbuilder.model.ProductOffer;
|
|||||||
import group.goforward.battlbuilder.repos.ProductOfferRepository;
|
import group.goforward.battlbuilder.repos.ProductOfferRepository;
|
||||||
import group.goforward.battlbuilder.repos.ProductRepository;
|
import group.goforward.battlbuilder.repos.ProductRepository;
|
||||||
import group.goforward.battlbuilder.web.dto.ProductDto;
|
import group.goforward.battlbuilder.web.dto.ProductDto;
|
||||||
|
import group.goforward.battlbuilder.web.dto.ProductOfferDto;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -30,11 +34,7 @@ public class ProductV1Controller {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List products (v1 summary contract)
|
* List products (v1 summary contract)
|
||||||
*
|
* Keep this lightweight for grids/lists.
|
||||||
* Examples:
|
|
||||||
* - GET /api/v1/products?platform=AR-15
|
|
||||||
* - GET /api/v1/products?platform=AR-15&partRoles=complete-upper&partRoles=upper-receiver
|
|
||||||
* - GET /api/v1/products?platform=ALL
|
|
||||||
*/
|
*/
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<ProductDto> listProducts(
|
public List<ProductDto> listProducts(
|
||||||
@@ -79,31 +79,254 @@ public class ProductV1Controller {
|
|||||||
return products.stream()
|
return products.stream()
|
||||||
.map(p -> {
|
.map(p -> {
|
||||||
ProductOffer best = pickBestOffer(offersByProductId.get(p.getId()));
|
ProductOffer best = pickBestOffer(offersByProductId.get(p.getId()));
|
||||||
return toProductDto(p, best);
|
return toProductDtoSummary(p, best);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Product details (v1 contract)
|
* Product details (v1 contract)
|
||||||
* NOTE: accepts String to avoid MethodArgumentTypeMismatch on /undefined etc.
|
* This endpoint is allowed to be "fat" for the details page.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<ProductDto> getProduct(@PathVariable("id") String id) {
|
public ResponseEntity<ProductDto> getProduct(@PathVariable("id") String id) {
|
||||||
Integer productId = parsePositiveInt(id);
|
Integer productId = parsePositiveInt(id);
|
||||||
if (productId == null) {
|
if (productId == null) return ResponseEntity.badRequest().build();
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<Product> productOpt = productRepository.findById(productId);
|
Optional<Product> productOpt = productRepository.findById(productId);
|
||||||
if (productOpt.isEmpty()) {
|
if (productOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Product product = productOpt.get();
|
||||||
|
|
||||||
|
// Pull offers + merchant (your Hibernate log shows this join is happening)
|
||||||
List<ProductOffer> offers = productOfferRepository.findByProductId(productId);
|
List<ProductOffer> offers = productOfferRepository.findByProductId(productId);
|
||||||
ProductOffer best = pickBestOffer(offers);
|
ProductOffer best = pickBestOffer(offers);
|
||||||
|
|
||||||
return ResponseEntity.ok(toProductDto(productOpt.get(), best));
|
ProductDto dto = toProductDtoDetails(product, best, offers);
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Description normalization (feed -> readable bullets)
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize ugly feed descriptions into readable, bullet-point friendly text.
|
||||||
|
*
|
||||||
|
* Goals:
|
||||||
|
* - Break common section headers onto their own lines (Features, Specs, Includes, etc.)
|
||||||
|
* - Convert "glued" headers like "FeaturesSub-MOA..." into "Features\n- Sub-MOA..."
|
||||||
|
* - Split run-on strings into bullets using separators (; | • \n) and heuristic sentence breaks
|
||||||
|
* - Keep it safe (no HTML parsing, no fancy dependencies)
|
||||||
|
*/
|
||||||
|
private static String normalizeDescription(String raw) {
|
||||||
|
if (raw == null) return null;
|
||||||
|
|
||||||
|
String s = raw.trim();
|
||||||
|
if (s.isEmpty()) return null;
|
||||||
|
|
||||||
|
// Basic cleanup (some feeds stuff literal \u003E etc, but your example is mostly plain text)
|
||||||
|
s = s.replace("\r\n", "\n").replace("\r", "\n");
|
||||||
|
s = s.replace("\t", " ");
|
||||||
|
s = collapseSpaces(s);
|
||||||
|
|
||||||
|
// Fix the classic "FeaturesSub-MOA" glued header issue
|
||||||
|
s = unglueHeaders(s);
|
||||||
|
|
||||||
|
// Add newlines around known headers to create sections
|
||||||
|
s = normalizeSectionHeaders(s);
|
||||||
|
|
||||||
|
// If it already contains line breaks, we'll bullet-ify lines; otherwise split heuristically.
|
||||||
|
s = bulletizeContent(s);
|
||||||
|
|
||||||
|
// Safety trim: keep details readable, not infinite
|
||||||
|
int MAX = 8000;
|
||||||
|
if (s.length() > MAX) {
|
||||||
|
s = s.substring(0, MAX).trim() + "…";
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a short description from the normalized description.
|
||||||
|
* Keeps the page from looking empty when shortDescription is null in feeds.
|
||||||
|
*/
|
||||||
|
private static String deriveShortDescription(String normalizedDescription) {
|
||||||
|
if (normalizedDescription == null || normalizedDescription.isBlank()) return null;
|
||||||
|
|
||||||
|
// Use first non-empty line that isn't a header like "Features"
|
||||||
|
String[] lines = normalizedDescription.split("\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
String t = line.trim();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
if (isHeaderLine(t)) continue;
|
||||||
|
|
||||||
|
// If it's a bullet line, strip leading "- "
|
||||||
|
if (t.startsWith("- ")) t = t.substring(2).trim();
|
||||||
|
|
||||||
|
// Keep it short
|
||||||
|
int MAX = 220;
|
||||||
|
if (t.length() > MAX) t = t.substring(0, MAX).trim() + "…";
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isHeaderLine(String line) {
|
||||||
|
String l = line.toLowerCase(Locale.ROOT).trim();
|
||||||
|
return l.equals("features") ||
|
||||||
|
l.equals("specifications") ||
|
||||||
|
l.equals("specs") ||
|
||||||
|
l.equals("includes") ||
|
||||||
|
l.equals("notes") ||
|
||||||
|
l.equals("overview");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String collapseSpaces(String s) {
|
||||||
|
// Collapse repeating spaces (but keep newlines)
|
||||||
|
return s.replaceAll("[ ]{2,}", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert spaces/newlines when section headers are glued to the next word.
|
||||||
|
* Example: "FeaturesSub-MOA Accuracy" -> "Features\nSub-MOA Accuracy"
|
||||||
|
*/
|
||||||
|
private static String unglueHeaders(String s) {
|
||||||
|
// Known headers that show up glued in feeds
|
||||||
|
String[] headers = new String[] {
|
||||||
|
"Features", "Specifications", "Specs", "Includes", "Overview", "Notes"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String h : headers) {
|
||||||
|
// Header followed immediately by a letter/number (no space) => add newline
|
||||||
|
s = s.replaceAll("(?i)\\b" + h + "(?=[A-Za-z0-9])", h + "\n");
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put headers on their own line and add a blank line before them (except at start).
|
||||||
|
*/
|
||||||
|
private static String normalizeSectionHeaders(String s) {
|
||||||
|
// Make sure headers start on a new line if they appear mid-string
|
||||||
|
s = s.replaceAll("(?i)\\s*(\\bFeatures\\b)\\s*", "\n\nFeatures\n");
|
||||||
|
s = s.replaceAll("(?i)\\s*(\\bSpecifications\\b|\\bSpecs\\b)\\s*", "\n\nSpecifications\n");
|
||||||
|
s = s.replaceAll("(?i)\\s*(\\bIncludes\\b)\\s*", "\n\nIncludes\n");
|
||||||
|
s = s.replaceAll("(?i)\\s*(\\bOverview\\b)\\s*", "\n\nOverview\n");
|
||||||
|
s = s.replaceAll("(?i)\\s*(\\bNotes\\b)\\s*", "\n\nNotes\n");
|
||||||
|
|
||||||
|
// Clean extra blank lines
|
||||||
|
s = s.replaceAll("\\n{3,}", "\n\n");
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert content lines into bullets where it makes sense.
|
||||||
|
* - Splits on common separators (; | •) into bullet lines
|
||||||
|
* - For long single-line descriptions, splits into pseudo-bullets by sentence-ish breaks
|
||||||
|
*/
|
||||||
|
private static String bulletizeContent(String s) {
|
||||||
|
String[] lines = s.split("\n");
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
String t = line.trim();
|
||||||
|
if (t.isEmpty()) {
|
||||||
|
out.append("\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve headers as-is
|
||||||
|
if (isHeaderLine(t)) {
|
||||||
|
out.append(t).append("\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If line already looks like bullets, keep it
|
||||||
|
if (t.startsWith("- ") || t.startsWith("• ")) {
|
||||||
|
out.append(t.startsWith("• ") ? "- " + t.substring(2).trim() : t).append("\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on common separators first
|
||||||
|
List<String> parts = splitOnSeparators(t);
|
||||||
|
|
||||||
|
if (parts.size() == 1) {
|
||||||
|
// Heuristic: split long run-on text into "sentences"
|
||||||
|
parts = splitHeuristic(parts.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bullet the parts
|
||||||
|
if (parts.size() > 1) {
|
||||||
|
for (String p : parts) {
|
||||||
|
String bp = p.trim();
|
||||||
|
if (bp.isEmpty()) continue;
|
||||||
|
out.append("- ").append(bp).append("\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single line: keep as paragraph (no bullet)
|
||||||
|
out.append(t).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup: trim extra blank lines
|
||||||
|
String result = out.toString().replaceAll("\\n{3,}", "\n\n").trim();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitOnSeparators(String line) {
|
||||||
|
// Split on ; | • (common feed separators)
|
||||||
|
// Keep it conservative: don't split on commas (would explode calibers etc.)
|
||||||
|
String normalized = line.replace("•", "|");
|
||||||
|
String[] raw = normalized.split("\\s*[;|]\\s*");
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
for (String r : raw) {
|
||||||
|
String t = r.trim();
|
||||||
|
if (!t.isEmpty()) parts.add(t);
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitHeuristic(String line) {
|
||||||
|
String t = line.trim();
|
||||||
|
if (t.length() < 180) return List.of(t); // short line = keep as paragraph
|
||||||
|
|
||||||
|
// Split on ". " and " - " only when it looks like sentences, not decimals (e.g. 17.5)
|
||||||
|
// This is a heuristic, not perfect.
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
String[] chunks = t.split("(?<!\\d)\\.\\s+"); // avoid splitting 17.5
|
||||||
|
for (String c : chunks) {
|
||||||
|
String s = c.trim();
|
||||||
|
if (s.isEmpty()) continue;
|
||||||
|
// Further split on " - " if it seems list-like
|
||||||
|
if (s.contains(" - ")) {
|
||||||
|
for (String sub : s.split("\\s+-\\s+")) {
|
||||||
|
String ss = sub.trim();
|
||||||
|
if (!ss.isEmpty()) parts.add(ss);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If heuristic didn't help, return original
|
||||||
|
if (parts.size() <= 1) return List.of(t);
|
||||||
|
|
||||||
|
// Cap bullet count so we don't spam 50 bullets
|
||||||
|
int MAX_BULLETS = 18;
|
||||||
|
if (parts.size() > MAX_BULLETS) {
|
||||||
|
parts = parts.subList(0, MAX_BULLETS);
|
||||||
|
parts.add("…");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Integer parsePositiveInt(String raw) {
|
private static Integer parsePositiveInt(String raw) {
|
||||||
@@ -115,17 +338,23 @@ public class ProductV1Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MVP: "best" means lowest effective price.
|
||||||
|
* (You can later enhance this to prefer in-stock, then price, etc.)
|
||||||
|
*/
|
||||||
private ProductOffer pickBestOffer(List<ProductOffer> offers) {
|
private ProductOffer pickBestOffer(List<ProductOffer> offers) {
|
||||||
if (offers == null || offers.isEmpty()) return null;
|
if (offers == null || offers.isEmpty()) return null;
|
||||||
|
|
||||||
// MVP: lowest effective price wins
|
|
||||||
return offers.stream()
|
return offers.stream()
|
||||||
.filter(o -> o.getEffectivePrice() != null)
|
.filter(o -> o.getEffectivePrice() != null)
|
||||||
.min(Comparator.comparing(ProductOffer::getEffectivePrice))
|
.min(Comparator.comparing(ProductOffer::getEffectivePrice))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProductDto toProductDto(Product p, ProductOffer bestOffer) {
|
/**
|
||||||
|
* Summary DTO for list/grid pages (keep small).
|
||||||
|
*/
|
||||||
|
private ProductDto toProductDtoSummary(Product p, ProductOffer bestOffer) {
|
||||||
ProductDto dto = new ProductDto();
|
ProductDto dto = new ProductDto();
|
||||||
|
|
||||||
dto.setId(String.valueOf(p.getId()));
|
dto.setId(String.valueOf(p.getId()));
|
||||||
@@ -135,14 +364,87 @@ public class ProductV1Controller {
|
|||||||
dto.setPartRole(p.getPartRole());
|
dto.setPartRole(p.getPartRole());
|
||||||
dto.setCategoryKey(p.getRawCategoryKey());
|
dto.setCategoryKey(p.getRawCategoryKey());
|
||||||
|
|
||||||
BigDecimal price = bestOffer != null ? bestOffer.getEffectivePrice() : null;
|
dto.setPrice(bestOffer != null ? bestOffer.getEffectivePrice() : null);
|
||||||
dto.setPrice(price);
|
|
||||||
|
|
||||||
dto.setBuyUrl(bestOffer != null ? bestOffer.getBuyUrl() : null);
|
dto.setBuyUrl(bestOffer != null ? bestOffer.getBuyUrl() : null);
|
||||||
|
dto.setInStock(bestOffer != null ? bestOffer.getInStock() : null);
|
||||||
|
|
||||||
// v1: match what the UI expects today
|
// v1 list expects imageUrl
|
||||||
dto.setImageUrl(p.getMainImageUrl());
|
dto.setImageUrl(p.getMainImageUrl());
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details DTO for product page (returns richer fields + offers table)
|
||||||
|
*/
|
||||||
|
private ProductDto toProductDtoDetails(Product p, ProductOffer bestOffer, List<ProductOffer> offers) {
|
||||||
|
ProductDto dto = new ProductDto();
|
||||||
|
|
||||||
|
// --- core identity ---
|
||||||
|
dto.setId(String.valueOf(p.getId()));
|
||||||
|
dto.setName(p.getName());
|
||||||
|
dto.setBrand(p.getBrand() != null ? p.getBrand().getName() : null);
|
||||||
|
dto.setPlatform(p.getPlatform());
|
||||||
|
dto.setPartRole(p.getPartRole());
|
||||||
|
dto.setCategoryKey(p.getRawCategoryKey());
|
||||||
|
|
||||||
|
// --- best offer summary (used for headline price + CTA fallback) ---
|
||||||
|
dto.setPrice(bestOffer != null ? bestOffer.getEffectivePrice() : null);
|
||||||
|
dto.setBuyUrl(bestOffer != null ? bestOffer.getBuyUrl() : null);
|
||||||
|
dto.setInStock(bestOffer != null ? bestOffer.getInStock() : null);
|
||||||
|
|
||||||
|
// --- images ---
|
||||||
|
dto.setImageUrl(p.getMainImageUrl()); // legacy field UI already uses
|
||||||
|
dto.setMainImageUrl(p.getMainImageUrl());
|
||||||
|
dto.setBattlImageUrl(p.getBattlImageUrl());
|
||||||
|
|
||||||
|
// --- richer product fields ---
|
||||||
|
dto.setSlug(p.getSlug());
|
||||||
|
dto.setMpn(p.getMpn());
|
||||||
|
dto.setUpc(p.getUpc());
|
||||||
|
dto.setConfiguration(p.getConfiguration() != null ? p.getConfiguration().name() : null);
|
||||||
|
dto.setPlatformLocked(p.getPlatformLocked());
|
||||||
|
|
||||||
|
// --- description normalizer/formatter
|
||||||
|
String normalized = normalizeDescription(p.getDescription());
|
||||||
|
dto.setDescription(normalized);
|
||||||
|
|
||||||
|
String shortDesc = p.getShortDescription();
|
||||||
|
if (shortDesc == null || shortDesc.isBlank()) {
|
||||||
|
shortDesc = deriveShortDescription(normalized);
|
||||||
|
}
|
||||||
|
dto.setShortDescription(shortDesc);
|
||||||
|
|
||||||
|
// --- offers table ---
|
||||||
|
List<ProductOfferDto> offerDtos = (offers == null ? List.<ProductOffer>of() : offers)
|
||||||
|
.stream()
|
||||||
|
.map(this::toOfferDto)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
dto.setOffers(offerDtos);
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offer -> ProductOfferDto mapper
|
||||||
|
* (This is what powers your offers table in the UI)
|
||||||
|
*/
|
||||||
|
private ProductOfferDto toOfferDto(ProductOffer o) {
|
||||||
|
ProductOfferDto dto = new ProductOfferDto();
|
||||||
|
|
||||||
|
dto.setId(String.valueOf(o.getId()));
|
||||||
|
dto.setMerchantName(
|
||||||
|
o.getMerchant() != null ? o.getMerchant().getName() : null
|
||||||
|
);
|
||||||
|
dto.setPrice(o.getPrice());
|
||||||
|
dto.setOriginalPrice(o.getOriginalPrice());
|
||||||
|
dto.setInStock(Boolean.TRUE.equals(o.getInStock()));
|
||||||
|
dto.setBuyUrl(o.getBuyUrl());
|
||||||
|
|
||||||
|
// IMPORTANT: map lastSeenAt → lastUpdated
|
||||||
|
dto.setLastUpdated(o.getLastSeenAt());
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,13 @@ package group.goforward.battlbuilder.repos;
|
|||||||
import group.goforward.battlbuilder.model.ProductOffer;
|
import group.goforward.battlbuilder.model.ProductOffer;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
public interface ProductOfferRepository extends JpaRepository<ProductOffer, Integer> {
|
public interface ProductOfferRepository extends JpaRepository<ProductOffer, Integer> {
|
||||||
|
|
||||||
List<ProductOffer> findByProductId(Integer productId);
|
List<ProductOffer> findByProductId(Integer productId);
|
||||||
@@ -31,4 +33,14 @@ public interface ProductOfferRepository extends JpaRepository<ProductOffer, Inte
|
|||||||
ORDER BY m.name ASC, p.platform ASC, p.importStatus ASC
|
ORDER BY m.name ASC, p.platform ASC, p.importStatus ASC
|
||||||
""")
|
""")
|
||||||
List<Object[]> countByMerchantPlatformAndStatus();
|
List<Object[]> countByMerchantPlatformAndStatus();
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select po
|
||||||
|
from ProductOffer po
|
||||||
|
join fetch po.merchant
|
||||||
|
where po.product.id = :productId
|
||||||
|
""")
|
||||||
|
List<ProductOffer> findByProductIdWithMerchant(
|
||||||
|
@Param("productId") Integer productId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package group.goforward.battlbuilder.web.dto;
|
package group.goforward.battlbuilder.web.dto;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ProductDto {
|
public class ProductDto {
|
||||||
private String id;
|
private String id;
|
||||||
@@ -8,10 +9,33 @@ public class ProductDto {
|
|||||||
private String brand;
|
private String brand;
|
||||||
private String platform;
|
private String platform;
|
||||||
private String partRole;
|
private String partRole;
|
||||||
|
|
||||||
|
// Categories
|
||||||
private String categoryKey;
|
private String categoryKey;
|
||||||
|
|
||||||
|
// Best offer summary (what list + detail page need)
|
||||||
private BigDecimal price;
|
private BigDecimal price;
|
||||||
private String buyUrl;
|
private String buyUrl;
|
||||||
|
private Boolean inStock;
|
||||||
|
|
||||||
|
// Images (detail page can prefer battlImageUrl, fallback to main/imageUrl)
|
||||||
private String imageUrl;
|
private String imageUrl;
|
||||||
|
private String mainImageUrl;
|
||||||
|
private String battlImageUrl;
|
||||||
|
|
||||||
|
// More “product” fields (this is what makes the details page feel legit)
|
||||||
|
private String slug;
|
||||||
|
private String mpn;
|
||||||
|
private String upc;
|
||||||
|
private String configuration;
|
||||||
|
private Boolean platformLocked;
|
||||||
|
private String shortDescription;
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
// Full offers table
|
||||||
|
private List<ProductOfferDto> offers;
|
||||||
|
|
||||||
|
// ---- getters / setters ----
|
||||||
|
|
||||||
public String getId() { return id; }
|
public String getId() { return id; }
|
||||||
public void setId(String id) { this.id = id; }
|
public void setId(String id) { this.id = id; }
|
||||||
@@ -37,6 +61,39 @@ public class ProductDto {
|
|||||||
public String getBuyUrl() { return buyUrl; }
|
public String getBuyUrl() { return buyUrl; }
|
||||||
public void setBuyUrl(String buyUrl) { this.buyUrl = buyUrl; }
|
public void setBuyUrl(String buyUrl) { this.buyUrl = buyUrl; }
|
||||||
|
|
||||||
|
public Boolean getInStock() { return inStock; }
|
||||||
|
public void setInStock(Boolean inStock) { this.inStock = inStock; }
|
||||||
|
|
||||||
public String getImageUrl() { return imageUrl; }
|
public String getImageUrl() { return imageUrl; }
|
||||||
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
|
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
|
||||||
|
|
||||||
|
public String getMainImageUrl() { return mainImageUrl; }
|
||||||
|
public void setMainImageUrl(String mainImageUrl) { this.mainImageUrl = mainImageUrl; }
|
||||||
|
|
||||||
|
public String getBattlImageUrl() { return battlImageUrl; }
|
||||||
|
public void setBattlImageUrl(String battlImageUrl) { this.battlImageUrl = battlImageUrl; }
|
||||||
|
|
||||||
|
public String getSlug() { return slug; }
|
||||||
|
public void setSlug(String slug) { this.slug = slug; }
|
||||||
|
|
||||||
|
public String getMpn() { return mpn; }
|
||||||
|
public void setMpn(String mpn) { this.mpn = mpn; }
|
||||||
|
|
||||||
|
public String getUpc() { return upc; }
|
||||||
|
public void setUpc(String upc) { this.upc = upc; }
|
||||||
|
|
||||||
|
public String getConfiguration() { return configuration; }
|
||||||
|
public void setConfiguration(String configuration) { this.configuration = configuration; }
|
||||||
|
|
||||||
|
public Boolean getPlatformLocked() { return platformLocked; }
|
||||||
|
public void setPlatformLocked(Boolean platformLocked) { this.platformLocked = platformLocked; }
|
||||||
|
|
||||||
|
public String getShortDescription() { return shortDescription; }
|
||||||
|
public void setShortDescription(String shortDescription) { this.shortDescription = shortDescription; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public List<ProductOfferDto> getOffers() { return offers; }
|
||||||
|
public void setOffers(List<ProductOfferDto> offers) { this.offers = offers; }
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package group.goforward.battlbuilder.web.dto;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ProductOfferDto {
|
public class ProductOfferDto {
|
||||||
private String id;
|
private String id;
|
||||||
|
|||||||
Reference in New Issue
Block a user