mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-06 02:56: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.MerchantRepository;
|
||||
import group.goforward.ballistic.repos.ProductRepository;
|
||||
import group.goforward.ballistic.repos.MerchantCategoryMapRepository;
|
||||
import group.goforward.ballistic.model.MerchantCategoryMap;
|
||||
import group.goforward.ballistic.service.MerchantCategoryMappingService;
|
||||
import group.goforward.ballistic.service.MerchantCategoryMappingService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import group.goforward.ballistic.repos.ProductOfferRepository;
|
||||
@@ -35,18 +35,18 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final BrandRepository brandRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||
private final MerchantCategoryMappingService merchantCategoryMappingService;
|
||||
private final ProductOfferRepository productOfferRepository;
|
||||
|
||||
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
||||
BrandRepository brandRepository,
|
||||
ProductRepository productRepository,
|
||||
MerchantCategoryMapRepository merchantCategoryMapRepository,
|
||||
MerchantCategoryMappingService merchantCategoryMappingService,
|
||||
ProductOfferRepository productOfferRepository) {
|
||||
this.merchantRepository = merchantRepository;
|
||||
this.brandRepository = brandRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||
this.merchantCategoryMappingService = merchantCategoryMappingService;
|
||||
this.productOfferRepository = productOfferRepository;
|
||||
}
|
||||
|
||||
@@ -180,8 +180,10 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
p.setUpc(null);
|
||||
|
||||
// ---------- PLATFORM ----------
|
||||
String platform = inferPlatform(row);
|
||||
p.setPlatform(platform != null ? platform : "AR-15");
|
||||
if (isNew || !Boolean.TRUE.equals(p.getPlatformLocked())) {
|
||||
String platform = inferPlatform(row);
|
||||
p.setPlatform(platform != null ? platform : "AR-15");
|
||||
}
|
||||
|
||||
// ---------- RAW CATEGORY KEY (for debugging / analytics) ----------
|
||||
String rawCategoryKey = buildRawCategoryKey(row);
|
||||
@@ -203,51 +205,60 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
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);
|
||||
|
||||
|
||||
// Idempotent upsert: look for an existing offer for this merchant + AvantLink product id
|
||||
ProductOffer offer = productOfferRepository
|
||||
.findByMerchantIdAndAvantlinkProductId(merchant.getId(), avantlinkProductId)
|
||||
.orElseGet(ProductOffer::new);
|
||||
|
||||
// If this is a brand‑new offer, initialize key fields
|
||||
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
|
||||
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) {
|
||||
|
||||
// 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;
|
||||
originalPrice = (retail != null ? retail : sale);
|
||||
} else {
|
||||
effectivePrice = retail;
|
||||
originalPrice = retail;
|
||||
// Otherwise fall back to retail or whatever is present
|
||||
effectivePrice = (retail != null ? retail : sale);
|
||||
originalPrice = (retail != null ? retail : sale);
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
// Update "last seen" on every import pass
|
||||
offer.setLastSeenAt(OffsetDateTime.now());
|
||||
|
||||
productOfferRepository.save(offer);
|
||||
}
|
||||
|
||||
@@ -256,15 +267,13 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
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;
|
||||
}
|
||||
// Delegate to the mapping service, which will:
|
||||
// - Look up an existing mapping
|
||||
// - If none exists, create a placeholder row with null mappedPartRole
|
||||
// - Return the mapped partRole, or null if not yet mapped
|
||||
String mapped = merchantCategoryMappingService.resolvePartRole(merchant, rawCategoryKey);
|
||||
if (mapped != null && !mapped.isBlank()) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +283,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
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()
|
||||
+ ", rawCategoryKey='" + rawCategoryKey + "'"
|
||||
+ ", 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")
|
||||
private String rawCategoryKey;
|
||||
|
||||
@Column(name = "platform_locked", nullable = false)
|
||||
private Boolean platformLocked = false;
|
||||
|
||||
|
||||
|
||||
// --- lifecycle hooks ---
|
||||
|
||||
@PrePersist
|
||||
@@ -209,4 +214,13 @@ public class Product {
|
||||
public void setDeletedAt(Instant 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.List;
|
||||
import java.util.Optional;
|
||||
|
||||
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
|
||||
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