mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-05 18:46:44 -05:00
allow for category mapping from GB UI. Add platform locked flag to products.
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
package group.goforward.ballistic.controllers;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/merchants")
|
||||||
|
@CrossOrigin // adjust later if you want
|
||||||
|
public class AdminMerchantController {
|
||||||
|
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
|
||||||
|
public AdminMerchantController(MerchantRepository merchantRepository) {
|
||||||
|
this.merchantRepository = merchantRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<Merchant> getMerchants() {
|
||||||
|
// If you want a DTO here, you can wrap it, but this is fine for internal admin
|
||||||
|
return merchantRepository.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package group.goforward.ballistic.controllers;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
|
import group.goforward.ballistic.service.MerchantCategoryMappingService;
|
||||||
|
import group.goforward.ballistic.web.dto.MerchantCategoryMappingDto;
|
||||||
|
import group.goforward.ballistic.web.dto.UpsertMerchantCategoryMappingRequest;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/merchant-category-mappings")
|
||||||
|
@CrossOrigin
|
||||||
|
public class MerchantCategoryMappingController {
|
||||||
|
|
||||||
|
private final MerchantCategoryMappingService mappingService;
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
|
||||||
|
public MerchantCategoryMappingController(
|
||||||
|
MerchantCategoryMappingService mappingService,
|
||||||
|
MerchantRepository merchantRepository
|
||||||
|
) {
|
||||||
|
this.mappingService = mappingService;
|
||||||
|
this.merchantRepository = merchantRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<MerchantCategoryMappingDto> listMappings(
|
||||||
|
@RequestParam("merchantId") Integer merchantId
|
||||||
|
) {
|
||||||
|
List<MerchantCategoryMapping> mappings = mappingService.findByMerchant(merchantId);
|
||||||
|
return mappings.stream()
|
||||||
|
.map(this::toDto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public MerchantCategoryMappingDto upsertMapping(
|
||||||
|
@RequestBody UpsertMerchantCategoryMappingRequest request
|
||||||
|
) {
|
||||||
|
Merchant merchant = merchantRepository
|
||||||
|
.findById(request.getMerchantId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + request.getMerchantId()));
|
||||||
|
|
||||||
|
MerchantCategoryMapping mapping = mappingService.upsertMapping(
|
||||||
|
merchant,
|
||||||
|
request.getRawCategory(),
|
||||||
|
request.getMappedPartRole()
|
||||||
|
);
|
||||||
|
|
||||||
|
return toDto(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MerchantCategoryMappingDto toDto(MerchantCategoryMapping mapping) {
|
||||||
|
MerchantCategoryMappingDto dto = new MerchantCategoryMappingDto();
|
||||||
|
dto.setId(mapping.getId());
|
||||||
|
dto.setMerchantId(mapping.getMerchant().getId());
|
||||||
|
dto.setMerchantName(mapping.getMerchant().getName());
|
||||||
|
dto.setRawCategory(mapping.getRawCategory());
|
||||||
|
dto.setMappedPartRole(mapping.getMappedPartRole());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ 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.service.MerchantCategoryMappingService;
|
||||||
import group.goforward.ballistic.model.MerchantCategoryMap;
|
import group.goforward.ballistic.service.MerchantCategoryMappingService;
|
||||||
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.repos.ProductOfferRepository;
|
||||||
@@ -35,18 +35,18 @@ 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 MerchantCategoryMappingService merchantCategoryMappingService;
|
||||||
private final ProductOfferRepository productOfferRepository;
|
private final ProductOfferRepository productOfferRepository;
|
||||||
|
|
||||||
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
||||||
BrandRepository brandRepository,
|
BrandRepository brandRepository,
|
||||||
ProductRepository productRepository,
|
ProductRepository productRepository,
|
||||||
MerchantCategoryMapRepository merchantCategoryMapRepository,
|
MerchantCategoryMappingService merchantCategoryMappingService,
|
||||||
ProductOfferRepository productOfferRepository) {
|
ProductOfferRepository productOfferRepository) {
|
||||||
this.merchantRepository = merchantRepository;
|
this.merchantRepository = merchantRepository;
|
||||||
this.brandRepository = brandRepository;
|
this.brandRepository = brandRepository;
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
this.merchantCategoryMappingService = merchantCategoryMappingService;
|
||||||
this.productOfferRepository = productOfferRepository;
|
this.productOfferRepository = productOfferRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,8 +180,10 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
p.setUpc(null);
|
p.setUpc(null);
|
||||||
|
|
||||||
// ---------- PLATFORM ----------
|
// ---------- PLATFORM ----------
|
||||||
String platform = inferPlatform(row);
|
if (isNew || !Boolean.TRUE.equals(p.getPlatformLocked())) {
|
||||||
p.setPlatform(platform != null ? platform : "AR-15");
|
String platform = inferPlatform(row);
|
||||||
|
p.setPlatform(platform != null ? platform : "AR-15");
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- RAW CATEGORY KEY (for debugging / analytics) ----------
|
// ---------- RAW CATEGORY KEY (for debugging / analytics) ----------
|
||||||
String rawCategoryKey = buildRawCategoryKey(row);
|
String rawCategoryKey = buildRawCategoryKey(row);
|
||||||
@@ -204,13 +206,22 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple approach: always create a new offer row.
|
// Idempotent upsert: look for an existing offer for this merchant + AvantLink product id
|
||||||
// (If you want idempotent imports later, we can add a repository finder
|
ProductOffer offer = productOfferRepository
|
||||||
// like findByProductAndMerchantAndAvantlinkProductId(...) and reuse the row.)
|
.findByMerchantIdAndAvantlinkProductId(merchant.getId(), avantlinkProductId)
|
||||||
ProductOffer offer = new ProductOffer();
|
.orElseGet(ProductOffer::new);
|
||||||
offer.setProduct(product);
|
|
||||||
offer.setMerchant(merchant);
|
// If this is a brand‑new offer, initialize key fields
|
||||||
offer.setAvantlinkProductId(avantlinkProductId);
|
if (offer.getId() == null) {
|
||||||
|
offer.setMerchant(merchant);
|
||||||
|
offer.setProduct(product);
|
||||||
|
offer.setAvantlinkProductId(avantlinkProductId);
|
||||||
|
offer.setFirstSeenAt(OffsetDateTime.now());
|
||||||
|
} else {
|
||||||
|
// Make sure associations stay in sync if anything changed
|
||||||
|
offer.setMerchant(merchant);
|
||||||
|
offer.setProduct(product);
|
||||||
|
}
|
||||||
|
|
||||||
// Identifiers
|
// Identifiers
|
||||||
offer.setSku(trimOrNull(row.sku()));
|
offer.setSku(trimOrNull(row.sku()));
|
||||||
@@ -227,12 +238,14 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
BigDecimal effectivePrice;
|
BigDecimal effectivePrice;
|
||||||
BigDecimal originalPrice;
|
BigDecimal originalPrice;
|
||||||
|
|
||||||
if (sale != null) {
|
// Prefer sale price if it exists and is less than or equal to retail
|
||||||
|
if (sale != null && (retail == null || sale.compareTo(retail) <= 0)) {
|
||||||
effectivePrice = sale;
|
effectivePrice = sale;
|
||||||
originalPrice = (retail != null ? retail : sale);
|
originalPrice = (retail != null ? retail : sale);
|
||||||
} else {
|
} else {
|
||||||
effectivePrice = retail;
|
// Otherwise fall back to retail or whatever is present
|
||||||
originalPrice = retail;
|
effectivePrice = (retail != null ? retail : sale);
|
||||||
|
originalPrice = (retail != null ? retail : sale);
|
||||||
}
|
}
|
||||||
|
|
||||||
offer.setPrice(effectivePrice);
|
offer.setPrice(effectivePrice);
|
||||||
@@ -243,10 +256,8 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
// We don't have a real stock flag in this CSV, so assume in-stock for now
|
// We don't have a real stock flag in this CSV, so assume in-stock for now
|
||||||
offer.setInStock(Boolean.TRUE);
|
offer.setInStock(Boolean.TRUE);
|
||||||
|
|
||||||
// Timestamps
|
// Update "last seen" on every import pass
|
||||||
OffsetDateTime now = OffsetDateTime.now();
|
offer.setLastSeenAt(OffsetDateTime.now());
|
||||||
offer.setLastSeenAt(now);
|
|
||||||
offer.setFirstSeenAt(now); // first import: treat now as first seen
|
|
||||||
|
|
||||||
productOfferRepository.save(offer);
|
productOfferRepository.save(offer);
|
||||||
}
|
}
|
||||||
@@ -256,15 +267,13 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
String rawCategoryKey = buildRawCategoryKey(row);
|
String rawCategoryKey = buildRawCategoryKey(row);
|
||||||
|
|
||||||
if (rawCategoryKey != null) {
|
if (rawCategoryKey != null) {
|
||||||
MerchantCategoryMap mapping = merchantCategoryMapRepository
|
// Delegate to the mapping service, which will:
|
||||||
.findByMerchantAndRawCategoryIgnoreCase(merchant, rawCategoryKey)
|
// - Look up an existing mapping
|
||||||
.orElse(null);
|
// - If none exists, create a placeholder row with null mappedPartRole
|
||||||
|
// - Return the mapped partRole, or null if not yet mapped
|
||||||
if (mapping != null && mapping.isEnabled()) {
|
String mapped = merchantCategoryMappingService.resolvePartRole(merchant, rawCategoryKey);
|
||||||
String mappedPartRole = trimOrNull(mapping.getPartRole());
|
if (mapped != null && !mapped.isBlank()) {
|
||||||
if (mappedPartRole != null && !mappedPartRole.isBlank()) {
|
return mapped;
|
||||||
return mappedPartRole;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +283,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
return keywordRole;
|
return keywordRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: log as unmapped and return null/unknown
|
// Last resort: log as unmapped and return null
|
||||||
System.out.println("IMPORT !!! UNMAPPED CATEGORY for merchant=" + merchant.getName()
|
System.out.println("IMPORT !!! UNMAPPED CATEGORY for merchant=" + merchant.getName()
|
||||||
+ ", rawCategoryKey='" + rawCategoryKey + "'"
|
+ ", rawCategoryKey='" + rawCategoryKey + "'"
|
||||||
+ ", sku=" + row.sku()
|
+ ", sku=" + row.sku()
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package group.goforward.ballistic.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "merchant_category_mappings",
|
||||||
|
uniqueConstraints = @UniqueConstraint(
|
||||||
|
name = "uq_merchant_category",
|
||||||
|
columnNames = { "merchant_id", "raw_category" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
public class MerchantCategoryMapping {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY) // SERIAL
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
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 = 512)
|
||||||
|
private String rawCategory;
|
||||||
|
|
||||||
|
@Column(name = "mapped_part_role", length = 128)
|
||||||
|
private String mappedPartRole; // e.g. "upper-receiver", "barrel"
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void onUpdate() {
|
||||||
|
this.updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters & setters
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = 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 getMappedPartRole() {
|
||||||
|
return mappedPartRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMappedPartRole(String mappedPartRole) {
|
||||||
|
this.mappedPartRole = mappedPartRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,11 @@ public class Product {
|
|||||||
@Column(name = "raw_category_key")
|
@Column(name = "raw_category_key")
|
||||||
private String rawCategoryKey;
|
private String rawCategoryKey;
|
||||||
|
|
||||||
|
@Column(name = "platform_locked", nullable = false)
|
||||||
|
private Boolean platformLocked = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- lifecycle hooks ---
|
// --- lifecycle hooks ---
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
@@ -209,4 +214,13 @@ public class Product {
|
|||||||
public void setDeletedAt(Instant deletedAt) {
|
public void setDeletedAt(Instant deletedAt) {
|
||||||
this.deletedAt = deletedAt;
|
this.deletedAt = deletedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getPlatformLocked() {
|
||||||
|
return platformLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatformLocked(Boolean platformLocked) {
|
||||||
|
this.platformLocked = platformLocked;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package group.goforward.ballistic.repos;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface MerchantCategoryMappingRepository
|
||||||
|
extends JpaRepository<MerchantCategoryMapping, Integer> {
|
||||||
|
|
||||||
|
Optional<MerchantCategoryMapping> findByMerchantIdAndRawCategoryIgnoreCase(
|
||||||
|
Integer merchantId,
|
||||||
|
String rawCategory
|
||||||
|
);
|
||||||
|
|
||||||
|
List<MerchantCategoryMapping> findByMerchantIdOrderByRawCategoryAsc(Integer merchantId);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface ProductOfferRepository extends JpaRepository<ProductOffer, Integer> {
|
public interface ProductOfferRepository extends JpaRepository<ProductOffer, Integer> {
|
||||||
|
|
||||||
@@ -12,4 +13,10 @@ public interface ProductOfferRepository extends JpaRepository<ProductOffer, Inte
|
|||||||
|
|
||||||
// Used by the /api/products/gunbuilder endpoint
|
// Used by the /api/products/gunbuilder endpoint
|
||||||
List<ProductOffer> findByProductIdIn(Collection<Integer> productIds);
|
List<ProductOffer> findByProductIdIn(Collection<Integer> productIds);
|
||||||
|
|
||||||
|
// Unique offer lookup for importer upsert
|
||||||
|
Optional<ProductOffer> findByMerchantIdAndAvantlinkProductId(
|
||||||
|
Integer merchantId,
|
||||||
|
String avantlinkProductId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
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,77 @@
|
|||||||
|
package group.goforward.ballistic.service;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
|
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MerchantCategoryMappingService {
|
||||||
|
|
||||||
|
private final MerchantCategoryMappingRepository mappingRepository;
|
||||||
|
|
||||||
|
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
|
||||||
|
this.mappingRepository = mappingRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
|
||||||
|
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a partRole for a given raw category.
|
||||||
|
* If not found, create a row with null mappedPartRole and return null (so importer can skip).
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public String resolvePartRole(Merchant merchant, String rawCategory) {
|
||||||
|
if (rawCategory == null || rawCategory.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = rawCategory.trim();
|
||||||
|
|
||||||
|
Optional<MerchantCategoryMapping> existingOpt =
|
||||||
|
mappingRepository.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed);
|
||||||
|
|
||||||
|
if (existingOpt.isPresent()) {
|
||||||
|
return existingOpt.get().getMappedPartRole();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create placeholder row
|
||||||
|
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
|
||||||
|
mapping.setMerchant(merchant);
|
||||||
|
mapping.setRawCategory(trimmed);
|
||||||
|
mapping.setMappedPartRole(null);
|
||||||
|
|
||||||
|
mappingRepository.save(mapping);
|
||||||
|
|
||||||
|
// No mapping yet → importer should skip this product
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert mapping (admin UI).
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public MerchantCategoryMapping upsertMapping(Merchant merchant, String rawCategory, String mappedPartRole) {
|
||||||
|
String trimmed = rawCategory.trim();
|
||||||
|
|
||||||
|
MerchantCategoryMapping mapping = mappingRepository
|
||||||
|
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
MerchantCategoryMapping m = new MerchantCategoryMapping();
|
||||||
|
m.setMerchant(merchant);
|
||||||
|
m.setRawCategory(trimmed);
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
|
||||||
|
mapping.setMappedPartRole(
|
||||||
|
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
return mappingRepository.save(mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
public class MerchantCategoryMappingDto {
|
||||||
|
|
||||||
|
private Integer id;
|
||||||
|
private Integer merchantId;
|
||||||
|
private String merchantName;
|
||||||
|
private String rawCategory;
|
||||||
|
private String mappedPartRole;
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMerchantId() {
|
||||||
|
return merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMerchantId(Integer merchantId) {
|
||||||
|
this.merchantId = merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMerchantName() {
|
||||||
|
return merchantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMerchantName(String merchantName) {
|
||||||
|
this.merchantName = merchantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawCategory() {
|
||||||
|
return rawCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRawCategory(String rawCategory) {
|
||||||
|
this.rawCategory = rawCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMappedPartRole() {
|
||||||
|
return mappedPartRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMappedPartRole(String mappedPartRole) {
|
||||||
|
this.mappedPartRole = mappedPartRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
public class UpsertMerchantCategoryMappingRequest {
|
||||||
|
|
||||||
|
private Integer merchantId;
|
||||||
|
private String rawCategory;
|
||||||
|
private String mappedPartRole; // can be null to "unmap"
|
||||||
|
|
||||||
|
public Integer getMerchantId() {
|
||||||
|
return merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMerchantId(Integer merchantId) {
|
||||||
|
this.merchantId = merchantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawCategory() {
|
||||||
|
return rawCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRawCategory(String rawCategory) {
|
||||||
|
this.rawCategory = rawCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMappedPartRole() {
|
||||||
|
return mappedPartRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMappedPartRole(String mappedPartRole) {
|
||||||
|
this.mappedPartRole = mappedPartRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user