mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-05 18:46:44 -05:00
Compare commits
2 Commits
52c49c7238
...
855f1c23c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 855f1c23c9 | |||
| 2ef96939f4 |
@@ -26,9 +26,12 @@ public class CorsConfig {
|
|||||||
"http://localhost:4201",
|
"http://localhost:4201",
|
||||||
"http://localhost:8070",
|
"http://localhost:8070",
|
||||||
"https://localhost:8070",
|
"https://localhost:8070",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"https://localhost:8080",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://localhost:3000",
|
||||||
"http://192.168.11.210:8070",
|
"http://192.168.11.210:8070",
|
||||||
"https://192.168.11.210:8070",
|
"https://192.168.11.210:8070",
|
||||||
"http://localhost:4200",
|
|
||||||
"http://citysites.gofwd.group",
|
"http://citysites.gofwd.group",
|
||||||
"https://citysites.gofwd.group",
|
"https://citysites.gofwd.group",
|
||||||
"http://citysites.gofwd.group:8070",
|
"http://citysites.gofwd.group:8070",
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package group.goforward.ballistic.controllers;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Product;
|
||||||
|
import group.goforward.ballistic.model.ProductOffer;
|
||||||
|
import group.goforward.ballistic.repos.ProductOfferRepository;
|
||||||
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.ProductSummaryDto;
|
||||||
|
import group.goforward.ballistic.web.mapper.ProductMapper;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/products")
|
||||||
|
@CrossOrigin
|
||||||
|
public class ProductController {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
private final ProductOfferRepository productOfferRepository;
|
||||||
|
|
||||||
|
public ProductController(
|
||||||
|
ProductRepository productRepository,
|
||||||
|
ProductOfferRepository productOfferRepository
|
||||||
|
) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
this.productOfferRepository = productOfferRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/gunbuilder")
|
||||||
|
public List<ProductSummaryDto> getGunbuilderProducts(
|
||||||
|
@RequestParam(defaultValue = "AR-15") String platform,
|
||||||
|
@RequestParam(required = false, name = "partRoles") List<String> partRoles
|
||||||
|
) {
|
||||||
|
// 1) Load products
|
||||||
|
List<Product> products;
|
||||||
|
if (partRoles == null || partRoles.isEmpty()) {
|
||||||
|
products = productRepository.findByPlatform(platform);
|
||||||
|
} else {
|
||||||
|
products = productRepository.findByPlatformAndPartRoleIn(platform, partRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (products.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Load offers for these product IDs (Integer IDs)
|
||||||
|
List<Integer> productIds = products.stream()
|
||||||
|
.map(Product::getId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<ProductOffer> allOffers =
|
||||||
|
productOfferRepository.findByProductIdIn(productIds);
|
||||||
|
|
||||||
|
Map<Integer, List<ProductOffer>> offersByProductId = allOffers.stream()
|
||||||
|
.collect(Collectors.groupingBy(o -> o.getProduct().getId()));
|
||||||
|
|
||||||
|
// 3) Map to DTOs with price and buyUrl
|
||||||
|
return products.stream()
|
||||||
|
.map(p -> {
|
||||||
|
List<ProductOffer> offersForProduct =
|
||||||
|
offersByProductId.getOrDefault(p.getId(), Collections.emptyList());
|
||||||
|
|
||||||
|
ProductOffer bestOffer = pickBestOffer(offersForProduct);
|
||||||
|
|
||||||
|
BigDecimal price = bestOffer != null ? bestOffer.getEffectivePrice() : null;
|
||||||
|
String buyUrl = bestOffer != null ? bestOffer.getBuyUrl() : null;
|
||||||
|
|
||||||
|
return ProductMapper.toSummary(p, price, buyUrl);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductOffer pickBestOffer(List<ProductOffer> offers) {
|
||||||
|
if (offers == null || offers.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right now: lowest price wins, regardless of stock (we set inStock=true on import anyway)
|
||||||
|
return offers.stream()
|
||||||
|
.filter(o -> o.getEffectivePrice() != null)
|
||||||
|
.min(Comparator.comparing(ProductOffer::getEffectivePrice))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,14 @@ import group.goforward.ballistic.model.Product;
|
|||||||
import group.goforward.ballistic.repos.BrandRepository;
|
import group.goforward.ballistic.repos.BrandRepository;
|
||||||
import group.goforward.ballistic.repos.MerchantRepository;
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
import group.goforward.ballistic.repos.ProductRepository;
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import group.goforward.ballistic.repos.MerchantCategoryMapRepository;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMap;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import group.goforward.ballistic.repos.ProductOfferRepository;
|
||||||
|
import group.goforward.ballistic.model.ProductOffer;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -29,13 +35,19 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
private final MerchantRepository merchantRepository;
|
private final MerchantRepository merchantRepository;
|
||||||
private final BrandRepository brandRepository;
|
private final BrandRepository brandRepository;
|
||||||
private final ProductRepository productRepository;
|
private final ProductRepository productRepository;
|
||||||
|
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||||
|
private final ProductOfferRepository productOfferRepository;
|
||||||
|
|
||||||
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
||||||
BrandRepository brandRepository,
|
BrandRepository brandRepository,
|
||||||
ProductRepository productRepository) {
|
ProductRepository productRepository,
|
||||||
|
MerchantCategoryMapRepository merchantCategoryMapRepository,
|
||||||
|
ProductOfferRepository productOfferRepository) {
|
||||||
this.merchantRepository = merchantRepository;
|
this.merchantRepository = merchantRepository;
|
||||||
this.brandRepository = brandRepository;
|
this.brandRepository = brandRepository;
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
|
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||||
|
this.productOfferRepository = productOfferRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -99,11 +111,18 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
p = candidates.get(0);
|
p = candidates.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProductFromRow(p, row, isNew);
|
updateProductFromRow(p, merchant, row, isNew);
|
||||||
return productRepository.save(p);
|
|
||||||
|
// Save the product first
|
||||||
|
Product saved = productRepository.save(p);
|
||||||
|
|
||||||
|
// Then upsert the offer for this row
|
||||||
|
upsertOfferFromRow(saved, merchant, row);
|
||||||
|
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateProductFromRow(Product p, MerchantFeedRow row, boolean isNew) {
|
private void updateProductFromRow(Product p, Merchant merchant, MerchantFeedRow row, boolean isNew) {
|
||||||
// ---------- NAME ----------
|
// ---------- NAME ----------
|
||||||
String name = coalesce(
|
String name = coalesce(
|
||||||
trimOrNull(row.productName()),
|
trimOrNull(row.productName()),
|
||||||
@@ -164,13 +183,105 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
String platform = inferPlatform(row);
|
String platform = inferPlatform(row);
|
||||||
p.setPlatform(platform != null ? platform : "AR-15");
|
p.setPlatform(platform != null ? platform : "AR-15");
|
||||||
|
|
||||||
|
// ---------- RAW CATEGORY KEY (for debugging / analytics) ----------
|
||||||
|
String rawCategoryKey = buildRawCategoryKey(row);
|
||||||
|
p.setRawCategoryKey(rawCategoryKey);
|
||||||
|
|
||||||
// ---------- PART ROLE ----------
|
// ---------- PART ROLE ----------
|
||||||
String partRole = inferPartRole(row);
|
String partRole = resolvePartRole(merchant, row);
|
||||||
if (partRole == null || partRole.isBlank()) {
|
if (partRole == null || partRole.isBlank()) {
|
||||||
partRole = "unknown";
|
partRole = "unknown";
|
||||||
}
|
}
|
||||||
p.setPartRole(partRole);
|
p.setPartRole(partRole);
|
||||||
}
|
}
|
||||||
|
private void upsertOfferFromRow(Product product, Merchant merchant, MerchantFeedRow row) {
|
||||||
|
// For now, we’ll use SKU as the "avantlinkProductId" placeholder.
|
||||||
|
// If/when you have a real AvantLink product_id in the feed, switch to that.
|
||||||
|
String avantlinkProductId = trimOrNull(row.sku());
|
||||||
|
if (avantlinkProductId == null) {
|
||||||
|
// If there's truly no SKU, bail out – we can't match this offer reliably.
|
||||||
|
System.out.println("IMPORT !!! skipping offer: no SKU for product id=" + product.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple approach: always create a new offer row.
|
||||||
|
// (If you want idempotent imports later, we can add a repository finder
|
||||||
|
// like findByProductAndMerchantAndAvantlinkProductId(...) and reuse the row.)
|
||||||
|
ProductOffer offer = new ProductOffer();
|
||||||
|
offer.setProduct(product);
|
||||||
|
offer.setMerchant(merchant);
|
||||||
|
offer.setAvantlinkProductId(avantlinkProductId);
|
||||||
|
|
||||||
|
// Identifiers
|
||||||
|
offer.setSku(trimOrNull(row.sku()));
|
||||||
|
// No real UPC in this feed yet – leave null for now
|
||||||
|
offer.setUpc(null);
|
||||||
|
|
||||||
|
// Buy URL
|
||||||
|
offer.setBuyUrl(trimOrNull(row.buyLink()));
|
||||||
|
|
||||||
|
// Prices from feed
|
||||||
|
BigDecimal retail = row.retailPrice(); // parsed as BigDecimal in readFeedRowsForMerchant
|
||||||
|
BigDecimal sale = row.salePrice();
|
||||||
|
|
||||||
|
BigDecimal effectivePrice;
|
||||||
|
BigDecimal originalPrice;
|
||||||
|
|
||||||
|
if (sale != null) {
|
||||||
|
effectivePrice = sale;
|
||||||
|
originalPrice = (retail != null ? retail : sale);
|
||||||
|
} else {
|
||||||
|
effectivePrice = retail;
|
||||||
|
originalPrice = retail;
|
||||||
|
}
|
||||||
|
|
||||||
|
offer.setPrice(effectivePrice);
|
||||||
|
offer.setOriginalPrice(originalPrice);
|
||||||
|
|
||||||
|
// Currency + stock
|
||||||
|
offer.setCurrency("USD");
|
||||||
|
// We don't have a real stock flag in this CSV, so assume in-stock for now
|
||||||
|
offer.setInStock(Boolean.TRUE);
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
offer.setLastSeenAt(now);
|
||||||
|
offer.setFirstSeenAt(now); // first import: treat now as first seen
|
||||||
|
|
||||||
|
productOfferRepository.save(offer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePartRole(Merchant merchant, MerchantFeedRow row) {
|
||||||
|
// Build a merchant-specific raw category key like "Department > Category > SubCategory"
|
||||||
|
String rawCategoryKey = buildRawCategoryKey(row);
|
||||||
|
|
||||||
|
if (rawCategoryKey != null) {
|
||||||
|
MerchantCategoryMap mapping = merchantCategoryMapRepository
|
||||||
|
.findByMerchantAndRawCategoryIgnoreCase(merchant, rawCategoryKey)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (mapping != null && mapping.isEnabled()) {
|
||||||
|
String mappedPartRole = trimOrNull(mapping.getPartRole());
|
||||||
|
if (mappedPartRole != null && !mappedPartRole.isBlank()) {
|
||||||
|
return mappedPartRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: keyword-based inference
|
||||||
|
String keywordRole = inferPartRole(row);
|
||||||
|
if (keywordRole != null && !keywordRole.isBlank()) {
|
||||||
|
return keywordRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: log as unmapped and return null/unknown
|
||||||
|
System.out.println("IMPORT !!! UNMAPPED CATEGORY for merchant=" + merchant.getName()
|
||||||
|
+ ", rawCategoryKey='" + rawCategoryKey + "'"
|
||||||
|
+ ", sku=" + row.sku()
|
||||||
|
+ ", productName=" + row.productName());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// Feed reading + brand resolution
|
// Feed reading + brand resolution
|
||||||
@@ -293,6 +404,23 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildRawCategoryKey(MerchantFeedRow row) {
|
||||||
|
String dept = trimOrNull(row.department());
|
||||||
|
String cat = trimOrNull(row.category());
|
||||||
|
String sub = trimOrNull(row.subCategory());
|
||||||
|
|
||||||
|
java.util.List<String> parts = new java.util.ArrayList<>();
|
||||||
|
if (dept != null) parts.add(dept);
|
||||||
|
if (cat != null) parts.add(cat);
|
||||||
|
if (sub != null) parts.add(sub);
|
||||||
|
|
||||||
|
if (parts.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.join(" > ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
private String inferPlatform(MerchantFeedRow row) {
|
private String inferPlatform(MerchantFeedRow row) {
|
||||||
String department = coalesce(trimOrNull(row.department()), trimOrNull(row.category()));
|
String department = coalesce(trimOrNull(row.department()), trimOrNull(row.category()));
|
||||||
if (department == null) return null;
|
if (department == null) return null;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package group.goforward.ballistic.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "merchant_category_map")
|
||||||
|
public class MerchantCategoryMap {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "merchant_id", nullable = false)
|
||||||
|
private Merchant merchant;
|
||||||
|
|
||||||
|
@Column(name = "raw_category", nullable = false, length = 255)
|
||||||
|
private String rawCategory;
|
||||||
|
|
||||||
|
// NEW FIELDS
|
||||||
|
@Column(name = "platform")
|
||||||
|
private String platform; // e.g. "AR-15", "AR-10"
|
||||||
|
|
||||||
|
@Column(name = "part_role")
|
||||||
|
private String partRole; // e.g. "barrel", "handguard"
|
||||||
|
|
||||||
|
@Column(name = "canonical_category")
|
||||||
|
private String canonicalCategory; // e.g. "Rifle Barrels"
|
||||||
|
|
||||||
|
@Column(name = "enabled", nullable = false)
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
// --- getters & setters ---
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Merchant getMerchant() {
|
||||||
|
return merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMerchant(Merchant merchant) {
|
||||||
|
this.merchant = merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawCategory() {
|
||||||
|
return rawCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRawCategory(String rawCategory) {
|
||||||
|
this.rawCategory = rawCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlatform() {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatform(String platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPartRole() {
|
||||||
|
return partRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPartRole(String partRole) {
|
||||||
|
this.partRole = partRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCanonicalCategory() {
|
||||||
|
return canonicalCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCanonicalCategory(String canonicalCategory) {
|
||||||
|
this.canonicalCategory = canonicalCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,9 @@ public class Product {
|
|||||||
|
|
||||||
@Column(name = "deleted_at")
|
@Column(name = "deleted_at")
|
||||||
private Instant deletedAt;
|
private Instant deletedAt;
|
||||||
|
|
||||||
|
@Column(name = "raw_category_key")
|
||||||
|
private String rawCategoryKey;
|
||||||
|
|
||||||
// --- lifecycle hooks ---
|
// --- lifecycle hooks ---
|
||||||
|
|
||||||
@@ -77,6 +80,14 @@ public class Product {
|
|||||||
updatedAt = Instant.now();
|
updatedAt = Instant.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRawCategoryKey() {
|
||||||
|
return rawCategoryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRawCategoryKey(String rawCategoryKey) {
|
||||||
|
this.rawCategoryKey = rawCategoryKey;
|
||||||
|
}
|
||||||
|
|
||||||
// --- getters & setters ---
|
// --- getters & setters ---
|
||||||
|
|
||||||
public Integer getId() {
|
public Integer getId() {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import java.util.UUID;
|
|||||||
@Table(name = "product_offers")
|
@Table(name = "product_offers")
|
||||||
public class ProductOffer {
|
public class ProductOffer {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "id", nullable = false)
|
@Column(name = "id", nullable = false)
|
||||||
private UUID id;
|
private Integer id;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||||
@@ -60,11 +60,11 @@ public class ProductOffer {
|
|||||||
@Column(name = "first_seen_at", nullable = false)
|
@Column(name = "first_seen_at", nullable = false)
|
||||||
private OffsetDateTime firstSeenAt;
|
private OffsetDateTime firstSeenAt;
|
||||||
|
|
||||||
public UUID getId() {
|
public Integer getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setId(UUID id) {
|
public void setId(Integer id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,4 +164,14 @@ public class ProductOffer {
|
|||||||
this.firstSeenAt = firstSeenAt;
|
this.firstSeenAt = firstSeenAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getEffectivePrice() {
|
||||||
|
// Prefer a true sale price when it's lower than the original
|
||||||
|
if (price != null && originalPrice != null && price.compareTo(originalPrice) < 0) {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use whatever is available
|
||||||
|
return price != null ? price : originalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package group.goforward.ballistic.repos;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMap;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface MerchantCategoryMapRepository extends JpaRepository<MerchantCategoryMap, Integer> {
|
||||||
|
|
||||||
|
Optional<MerchantCategoryMap> findByMerchantAndRawCategoryIgnoreCase(Merchant merchant, String rawCategory);
|
||||||
|
}
|
||||||
@@ -3,5 +3,9 @@ package group.goforward.ballistic.repos;
|
|||||||
import group.goforward.ballistic.model.Merchant;
|
import group.goforward.ballistic.model.Merchant;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface MerchantRepository extends JpaRepository<Merchant, Integer> {
|
public interface MerchantRepository extends JpaRepository<Merchant, Integer> {
|
||||||
|
|
||||||
|
Optional<Merchant> findByNameIgnoreCase(String name);
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
package group.goforward.ballistic.repos;
|
package group.goforward.ballistic.repos;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.ProductOffer;
|
import group.goforward.ballistic.model.ProductOffer;
|
||||||
import group.goforward.ballistic.model.Product;
|
|
||||||
import group.goforward.ballistic.model.Merchant;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public interface ProductOfferRepository extends JpaRepository<ProductOffer, UUID> {
|
import java.util.Collection;
|
||||||
Optional<ProductOffer> findByMerchantAndAvantlinkProductId(Merchant merchant, String avantlinkProductId);
|
import java.util.List;
|
||||||
List<ProductOffer> findByProductAndInStockTrueOrderByPriceAsc(Product product);
|
|
||||||
|
public interface ProductOfferRepository extends JpaRepository<ProductOffer, Integer> {
|
||||||
|
|
||||||
|
List<ProductOffer> findByProductIdIn(Collection<Integer> productIds);
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public interface ProductRepository extends JpaRepository<Product, Integer> {
|
public interface ProductRepository extends JpaRepository<Product, Integer> {
|
||||||
|
|
||||||
@@ -17,4 +18,10 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
|
|||||||
List<Product> findAllByBrandAndMpn(Brand brand, String mpn);
|
List<Product> findAllByBrandAndMpn(Brand brand, String mpn);
|
||||||
|
|
||||||
List<Product> findAllByBrandAndUpc(Brand brand, String upc);
|
List<Product> findAllByBrandAndUpc(Brand brand, String upc);
|
||||||
|
|
||||||
|
// All products for a given platform (e.g. "AR-15")
|
||||||
|
List<Product> findByPlatform(String platform);
|
||||||
|
|
||||||
|
// Products filtered by platform + part roles (e.g. upper-receiver, barrel, etc.)
|
||||||
|
List<Product> findByPlatformAndPartRoleIn(String platform, Collection<String> partRoles);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package group.goforward.ballistic.seed;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMap;
|
||||||
|
import group.goforward.ballistic.repos.MerchantCategoryMapRepository;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MerchantCategoryMapSeeder {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CommandLineRunner seedMerchantCategoryMaps(MerchantRepository merchantRepository,
|
||||||
|
MerchantCategoryMapRepository mapRepository) {
|
||||||
|
return args -> {
|
||||||
|
// --- Guard: only seed if table is (mostly) empty ---
|
||||||
|
long existing = mapRepository.count();
|
||||||
|
if (existing > 0) {
|
||||||
|
System.out.println("CategoryMapSeeder: found " + existing + " existing mappings, skipping seeding.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("CategoryMapSeeder: seeding initial MerchantCategoryMap rows...");
|
||||||
|
|
||||||
|
// Adjust merchant names if they differ in your DB
|
||||||
|
seedAeroPrecision(merchantRepository, mapRepository);
|
||||||
|
seedBrownells(merchantRepository, mapRepository);
|
||||||
|
seedPSA(merchantRepository, mapRepository);
|
||||||
|
|
||||||
|
System.out.println("CategoryMapSeeder: seeding complete.");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// AERO PRECISION
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void seedAeroPrecision(MerchantRepository merchantRepository,
|
||||||
|
MerchantCategoryMapRepository mapRepository) {
|
||||||
|
|
||||||
|
merchantRepository.findByNameIgnoreCase("Aero Precision").ifPresent(merchant -> {
|
||||||
|
|
||||||
|
// Keys come from Department | Category | SubCategory combos
|
||||||
|
upsert(merchant, "Charging Handles",
|
||||||
|
"AR-15", "charging-handle", "Charging Handles", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Shop All Barrels",
|
||||||
|
null, "barrel", "Rifle Barrels", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Lower Parts Kits",
|
||||||
|
"AR-15", "lower-parts-kit", "Lower Parts Kits", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Handguards",
|
||||||
|
"AR-15", "handguard", "Handguards & Rails", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Upper Receivers",
|
||||||
|
"AR-15", "upper-receiver", "Upper Receivers", true, mapRepository);
|
||||||
|
|
||||||
|
// Platform-only hints (let your existing heuristics decide part_role)
|
||||||
|
upsert(merchant, ".308 Winchester",
|
||||||
|
"AR-10", null, "AR-10 / .308 Parts", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "6.5 Creedmoor",
|
||||||
|
"AR-10", null, "6.5 Creedmoor Parts", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "5.56 Nato / .223 Wylde",
|
||||||
|
"AR-15", null, "5.56 / .223 Wylde Parts", true, mapRepository);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// BROWNELLS
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void seedBrownells(MerchantRepository merchantRepository,
|
||||||
|
MerchantCategoryMapRepository mapRepository) {
|
||||||
|
|
||||||
|
merchantRepository.findByNameIgnoreCase("Brownells").ifPresent(merchant -> {
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Receiver Parts | Receivers",
|
||||||
|
null, "receiver", "Rifle Receivers", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Barrel Parts | Rifle Barrels",
|
||||||
|
null, "barrel", "Rifle Barrels", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Stock Parts | Rifle Stocks",
|
||||||
|
null, "stock", "Rifle Stocks", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Muzzle Devices | Compensators & Muzzle Brakes",
|
||||||
|
null, "muzzle-device", "Muzzle Devices", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Trigger Parts | Triggers",
|
||||||
|
null, "trigger", "Triggers", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Receiver Parts | Magazine Parts",
|
||||||
|
null, "magazine", "Magazine & Mag Parts", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Sights | Front Sights",
|
||||||
|
null, "sight", "Iron Sights", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Sights | Rear Sights",
|
||||||
|
null, "sight", "Iron Sights", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Receiver Parts | Buffer Tube Parts",
|
||||||
|
null, "buffer-tube", "Buffer Tubes & Parts", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "Rifle Parts | Stock Parts | Buttstocks",
|
||||||
|
null, "stock", "Buttstocks", true, mapRepository);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// PALMETTO STATE ARMORY (PSA)
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void seedPSA(MerchantRepository merchantRepository,
|
||||||
|
MerchantCategoryMapRepository mapRepository) {
|
||||||
|
|
||||||
|
merchantRepository.findByNameIgnoreCase("Palmetto State Armory").ifPresent(merchant -> {
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Upper Parts | Stripped Uppers",
|
||||||
|
"AR-15", "upper-receiver", "AR-15 Stripped Uppers", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Upper Parts | Complete Uppers",
|
||||||
|
"AR-15", "complete-upper", "AR-15 Complete Uppers", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Barrel Parts | Barrels",
|
||||||
|
"AR-15", "barrel", "AR-15 Barrels", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Lower Parts | Stripped Lowers",
|
||||||
|
"AR-15", "lower-receiver", "AR-15 Stripped Lowers", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Handguard Parts | Handguards",
|
||||||
|
"AR-15", "handguard", "AR-15 Handguards", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Bolt Carrier Groups | Bolt Carrier Groups",
|
||||||
|
"AR-15", "bcg", "AR-15 BCGs", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Trigger Parts | Triggers",
|
||||||
|
"AR-15", "trigger", "AR-15 Triggers", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Stock Parts | Stocks",
|
||||||
|
"AR-15", "stock", "AR-15 Stocks", true, mapRepository);
|
||||||
|
|
||||||
|
upsert(merchant, "AR-15 Parts | Muzzle Devices | Muzzle Devices",
|
||||||
|
"AR-15", "muzzle-device", "AR-15 Muzzle Devices", true, mapRepository);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Helper
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void upsert(Merchant merchant,
|
||||||
|
String rawCategory,
|
||||||
|
String platform,
|
||||||
|
String partRole,
|
||||||
|
String canonicalCategory,
|
||||||
|
boolean enabled,
|
||||||
|
MerchantCategoryMapRepository mapRepository) {
|
||||||
|
|
||||||
|
MerchantCategoryMap map = mapRepository
|
||||||
|
.findByMerchantAndRawCategoryIgnoreCase(merchant, rawCategory)
|
||||||
|
.orElseGet(MerchantCategoryMap::new);
|
||||||
|
|
||||||
|
map.setMerchant(merchant);
|
||||||
|
map.setRawCategory(rawCategory);
|
||||||
|
|
||||||
|
// These fields are optional – null means “let heuristics or defaults handle it”
|
||||||
|
map.setPlatform(platform);
|
||||||
|
map.setPartRole(partRole);
|
||||||
|
map.setCanonicalCategory(canonicalCategory);
|
||||||
|
map.setEnabled(enabled);
|
||||||
|
|
||||||
|
mapRepository.save(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public class ProductSummaryDto {
|
||||||
|
|
||||||
|
private String id; // product UUID as string
|
||||||
|
private String name;
|
||||||
|
private String brand;
|
||||||
|
private String platform;
|
||||||
|
private String partRole;
|
||||||
|
private String categoryKey;
|
||||||
|
private BigDecimal price;
|
||||||
|
private String buyUrl;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrand() {
|
||||||
|
return brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrand(String brand) {
|
||||||
|
this.brand = brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlatform() {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatform(String platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPartRole() {
|
||||||
|
return partRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPartRole(String partRole) {
|
||||||
|
this.partRole = partRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCategoryKey() {
|
||||||
|
return categoryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCategoryKey(String categoryKey) {
|
||||||
|
this.categoryKey = categoryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getPrice() {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrice(BigDecimal price) {
|
||||||
|
this.price = price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBuyUrl() {
|
||||||
|
return buyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuyUrl(String buyUrl) {
|
||||||
|
this.buyUrl = buyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package group.goforward.ballistic.web.mapper;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Product;
|
||||||
|
import group.goforward.ballistic.web.dto.ProductSummaryDto;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public class ProductMapper {
|
||||||
|
|
||||||
|
public static ProductSummaryDto toSummary(Product product, BigDecimal price, String buyUrl) {
|
||||||
|
ProductSummaryDto dto = new ProductSummaryDto();
|
||||||
|
|
||||||
|
// Product ID -> String
|
||||||
|
dto.setId(String.valueOf(product.getId()));
|
||||||
|
|
||||||
|
dto.setName(product.getName());
|
||||||
|
dto.setBrand(product.getBrand() != null ? product.getBrand().getName() : null);
|
||||||
|
dto.setPlatform(product.getPlatform());
|
||||||
|
dto.setPartRole(product.getPartRole());
|
||||||
|
|
||||||
|
// Use rawCategoryKey from the Product entity
|
||||||
|
dto.setCategoryKey(product.getRawCategoryKey());
|
||||||
|
|
||||||
|
// Price + buy URL from offers
|
||||||
|
dto.setPrice(price);
|
||||||
|
dto.setBuyUrl(buyUrl);
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user