mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-06 11:06:45 -05:00
90ish percent working. data flows, offers are create and pricings is pulling.
This commit is contained in:
@@ -26,9 +26,12 @@ public class CorsConfig {
|
||||
"http://localhost:4201",
|
||||
"http://localhost:8070",
|
||||
"https://localhost:8070",
|
||||
"http://localhost:8080",
|
||||
"https://localhost:8080",
|
||||
"http://localhost:3000",
|
||||
"https://localhost:3000",
|
||||
"http://192.168.11.210:8070",
|
||||
"https://192.168.11.210:8070",
|
||||
"http://localhost:4200",
|
||||
"http://citysites.gofwd.group",
|
||||
"https://citysites.gofwd.group",
|
||||
"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);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ import group.goforward.ballistic.repos.MerchantCategoryMapRepository;
|
||||
import group.goforward.ballistic.model.MerchantCategoryMap;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import group.goforward.ballistic.repos.ProductOfferRepository;
|
||||
import group.goforward.ballistic.model.ProductOffer;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@@ -32,15 +36,18 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
private final BrandRepository brandRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||
private final ProductOfferRepository productOfferRepository;
|
||||
|
||||
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
||||
BrandRepository brandRepository,
|
||||
ProductRepository productRepository,
|
||||
MerchantCategoryMapRepository merchantCategoryMapRepository) {
|
||||
MerchantCategoryMapRepository merchantCategoryMapRepository,
|
||||
ProductOfferRepository productOfferRepository) {
|
||||
this.merchantRepository = merchantRepository;
|
||||
this.brandRepository = brandRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||
this.productOfferRepository = productOfferRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,7 +112,14 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
}
|
||||
|
||||
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, Merchant merchant, MerchantFeedRow row, boolean isNew) {
|
||||
@@ -180,6 +194,62 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
}
|
||||
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"
|
||||
|
||||
@@ -13,9 +13,9 @@ import java.util.UUID;
|
||||
@Table(name = "product_offers")
|
||||
public class ProductOffer {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private UUID id;
|
||||
private Integer id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||
@@ -60,11 +60,11 @@ public class ProductOffer {
|
||||
@Column(name = "first_seen_at", nullable = false)
|
||||
private OffsetDateTime firstSeenAt;
|
||||
|
||||
public UUID getId() {
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@@ -164,4 +164,14 @@ public class ProductOffer {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package group.goforward.ballistic.repos;
|
||||
|
||||
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 java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ProductOfferRepository extends JpaRepository<ProductOffer, UUID> {
|
||||
Optional<ProductOffer> findByMerchantAndAvantlinkProductId(Merchant merchant, String avantlinkProductId);
|
||||
List<ProductOffer> findByProductAndInStockTrueOrderByPriceAsc(Product product);
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
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.UUID;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
|
||||
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> 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,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