mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-06 02:56:44 -05:00
category mapping work. app runs still :D
This commit is contained in:
@@ -19,6 +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 org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -29,13 +31,16 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final BrandRepository brandRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||
|
||||
public MerchantFeedImportServiceImpl(MerchantRepository merchantRepository,
|
||||
BrandRepository brandRepository,
|
||||
ProductRepository productRepository) {
|
||||
ProductRepository productRepository,
|
||||
MerchantCategoryMapRepository merchantCategoryMapRepository) {
|
||||
this.merchantRepository = merchantRepository;
|
||||
this.brandRepository = brandRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,11 +104,11 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
p = candidates.get(0);
|
||||
}
|
||||
|
||||
updateProductFromRow(p, row, isNew);
|
||||
updateProductFromRow(p, merchant, row, isNew);
|
||||
return productRepository.save(p);
|
||||
}
|
||||
|
||||
private void updateProductFromRow(Product p, MerchantFeedRow row, boolean isNew) {
|
||||
private void updateProductFromRow(Product p, Merchant merchant, MerchantFeedRow row, boolean isNew) {
|
||||
// ---------- NAME ----------
|
||||
String name = coalesce(
|
||||
trimOrNull(row.productName()),
|
||||
@@ -164,14 +169,50 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
String platform = inferPlatform(row);
|
||||
p.setPlatform(platform != null ? platform : "AR-15");
|
||||
|
||||
// ---------- RAW CATEGORY KEY (for debugging / analytics) ----------
|
||||
String rawCategoryKey = buildRawCategoryKey(row);
|
||||
p.setRawCategoryKey(rawCategoryKey);
|
||||
|
||||
// ---------- PART ROLE ----------
|
||||
String partRole = inferPartRole(row);
|
||||
String partRole = resolvePartRole(merchant, row);
|
||||
if (partRole == null || partRole.isBlank()) {
|
||||
partRole = "unknown";
|
||||
}
|
||||
p.setPartRole(partRole);
|
||||
}
|
||||
|
||||
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
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -293,6 +334,23 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
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) {
|
||||
String department = coalesce(trimOrNull(row.department()), trimOrNull(row.category()));
|
||||
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")
|
||||
private Instant deletedAt;
|
||||
|
||||
@Column(name = "raw_category_key")
|
||||
private String rawCategoryKey;
|
||||
|
||||
// --- lifecycle hooks ---
|
||||
|
||||
@@ -77,6 +80,14 @@ public class Product {
|
||||
updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
public String getRawCategoryKey() {
|
||||
return rawCategoryKey;
|
||||
}
|
||||
|
||||
public void setRawCategoryKey(String rawCategoryKey) {
|
||||
this.rawCategoryKey = rawCategoryKey;
|
||||
}
|
||||
|
||||
// --- getters & setters ---
|
||||
|
||||
public Integer getId() {
|
||||
|
||||
@@ -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 org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface MerchantRepository extends JpaRepository<Merchant, Integer> {
|
||||
|
||||
Optional<Merchant> findByNameIgnoreCase(String name);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user