From f1dcd10a79b9d98ca9a8a30af61e626e42d7a9c7 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 1 Dec 2025 16:08:42 -0500 Subject: [PATCH] new categories and mapping logic --- .../model/MerchantCategoryMapping.java | 14 +++++ .../goforward/ballistic/model/Product.java | 15 ++++- .../ballistic/model/ProductConfiguration.java | 10 +++ .../MerchantCategoryMappingService.java | 61 ++++++++++++------- .../impl/MerchantFeedImportServiceImpl.java | 51 ++++++---------- 5 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 src/main/java/group/goforward/ballistic/model/ProductConfiguration.java diff --git a/src/main/java/group/goforward/ballistic/model/MerchantCategoryMapping.java b/src/main/java/group/goforward/ballistic/model/MerchantCategoryMapping.java index d90e561..9bb833c 100644 --- a/src/main/java/group/goforward/ballistic/model/MerchantCategoryMapping.java +++ b/src/main/java/group/goforward/ballistic/model/MerchantCategoryMapping.java @@ -3,6 +3,8 @@ package group.goforward.ballistic.model; import jakarta.persistence.*; import java.time.OffsetDateTime; +import group.goforward.ballistic.model.ProductConfiguration; + @Entity @Table( name = "merchant_category_mappings", @@ -28,6 +30,10 @@ public class MerchantCategoryMapping { @Column(name = "mapped_part_role", length = 128) private String mappedPartRole; // e.g. "upper-receiver", "barrel" + @Column(name = "mapped_configuration") + @Enumerated(EnumType.STRING) + private ProductConfiguration mappedConfiguration; + @Column(name = "created_at", nullable = false) private OffsetDateTime createdAt = OffsetDateTime.now(); @@ -73,6 +79,14 @@ public class MerchantCategoryMapping { this.mappedPartRole = mappedPartRole; } + public ProductConfiguration getMappedConfiguration() { + return mappedConfiguration; + } + + public void setMappedConfiguration(ProductConfiguration mappedConfiguration) { + this.mappedConfiguration = mappedConfiguration; + } + public OffsetDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/group/goforward/ballistic/model/Product.java b/src/main/java/group/goforward/ballistic/model/Product.java index 0b0540e..785f928 100644 --- a/src/main/java/group/goforward/ballistic/model/Product.java +++ b/src/main/java/group/goforward/ballistic/model/Product.java @@ -4,6 +4,8 @@ import jakarta.persistence.*; import java.time.Instant; import java.util.UUID; +import group.goforward.ballistic.model.ProductConfiguration; + @Entity @Table(name = "products") public class Product { @@ -38,6 +40,10 @@ public class Product { @Column(name = "part_role") private String partRole; + @Column(name = "configuration") + @Enumerated(EnumType.STRING) + private ProductConfiguration configuration; + @Column(name = "short_description") private String shortDescription; @@ -223,4 +229,11 @@ public class Product { this.platformLocked = platformLocked; } -} \ No newline at end of file + public ProductConfiguration getConfiguration() { + return configuration; + } + + public void setConfiguration(ProductConfiguration configuration) { + this.configuration = configuration; + } +} diff --git a/src/main/java/group/goforward/ballistic/model/ProductConfiguration.java b/src/main/java/group/goforward/ballistic/model/ProductConfiguration.java new file mode 100644 index 0000000..7bda4e9 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/model/ProductConfiguration.java @@ -0,0 +1,10 @@ +package group.goforward.ballistic.model; + +public enum ProductConfiguration { + STRIPPED, // bare receiver / component + ASSEMBLED, // built up but not fully complete + BARRELED, // upper + barrel + gas system, no BCG/CH + COMPLETE, // full assembly ready to run + KIT, // collection of parts (LPK, trigger kits, etc.) + OTHER // fallback / unknown +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/services/MerchantCategoryMappingService.java b/src/main/java/group/goforward/ballistic/services/MerchantCategoryMappingService.java index c93d162..06e808c 100644 --- a/src/main/java/group/goforward/ballistic/services/MerchantCategoryMappingService.java +++ b/src/main/java/group/goforward/ballistic/services/MerchantCategoryMappingService.java @@ -2,6 +2,7 @@ package group.goforward.ballistic.services; import group.goforward.ballistic.model.Merchant; import group.goforward.ballistic.model.MerchantCategoryMapping; +import group.goforward.ballistic.model.ProductConfiguration; import group.goforward.ballistic.repos.MerchantCategoryMappingRepository; import jakarta.transaction.Transactional; import java.util.List; @@ -22,41 +23,44 @@ public class MerchantCategoryMappingService { } /** - * Resolve a partRole for a given raw category. - * If not found, create a row with null mappedPartRole and return null (so importer can skip). + * Resolve (or create) a mapping row for this merchant + raw category. + * - If it exists, returns it (with whatever mappedPartRole / mappedConfiguration are set). + * - If it doesn't exist, creates a placeholder row with null mappings and returns it. + * + * The importer can then: + * - skip rows where mappedPartRole is still null + * - use mappedConfiguration if present */ @Transactional - public String resolvePartRole(Merchant merchant, String rawCategory) { + public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) { if (rawCategory == null || rawCategory.isBlank()) { return null; } String trimmed = rawCategory.trim(); - Optional 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; + return mappingRepository + .findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed) + .orElseGet(() -> { + MerchantCategoryMapping mapping = new MerchantCategoryMapping(); + mapping.setMerchant(merchant); + mapping.setRawCategory(trimmed); + mapping.setMappedPartRole(null); + mapping.setMappedConfiguration(null); + return mappingRepository.save(mapping); + }); } /** * Upsert mapping (admin UI). */ @Transactional - public MerchantCategoryMapping upsertMapping(Merchant merchant, String rawCategory, String mappedPartRole) { + public MerchantCategoryMapping upsertMapping( + Merchant merchant, + String rawCategory, + String mappedPartRole, + ProductConfiguration mappedConfiguration + ) { String trimmed = rawCategory.trim(); MerchantCategoryMapping mapping = mappingRepository @@ -72,6 +76,21 @@ public class MerchantCategoryMappingService { (mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim() ); + mapping.setMappedConfiguration(mappedConfiguration); + return mappingRepository.save(mapping); } + /** + * Backwards-compatible overload for existing callers (e.g. controller) + * that don’t care about productConfiguration yet. + */ + @Transactional + public MerchantCategoryMapping upsertMapping( + Merchant merchant, + String rawCategory, + String mappedPartRole + ) { + // Delegate to the new method with `null` configuration + return upsertMapping(merchant, rawCategory, mappedPartRole, null); + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/services/impl/MerchantFeedImportServiceImpl.java b/src/main/java/group/goforward/ballistic/services/impl/MerchantFeedImportServiceImpl.java index 5c34c2f..c56f021 100644 --- a/src/main/java/group/goforward/ballistic/services/impl/MerchantFeedImportServiceImpl.java +++ b/src/main/java/group/goforward/ballistic/services/impl/MerchantFeedImportServiceImpl.java @@ -22,6 +22,7 @@ import group.goforward.ballistic.repos.BrandRepository; import group.goforward.ballistic.repos.MerchantRepository; import group.goforward.ballistic.repos.ProductRepository; import group.goforward.ballistic.services.MerchantCategoryMappingService; +import group.goforward.ballistic.model.MerchantCategoryMapping; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import group.goforward.ballistic.repos.ProductOfferRepository; @@ -190,11 +191,28 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService String rawCategoryKey = buildRawCategoryKey(row); p.setRawCategoryKey(rawCategoryKey); - // ---------- PART ROLE ---------- - String partRole = resolvePartRole(merchant, row); + // ---------- PART ROLE (via category mapping, with keyword fallback) ---------- + String partRole = null; + + if (rawCategoryKey != null) { + // Ask the mapping service for (or to create) a mapping row + MerchantCategoryMapping mapping = + merchantCategoryMappingService.resolveMapping(merchant, rawCategoryKey); + + if (mapping != null && mapping.getMappedPartRole() != null && !mapping.getMappedPartRole().isBlank()) { + partRole = mapping.getMappedPartRole().trim(); + } + } + + // Fallback: keyword-based inference if we still don't have a mapped partRole + if (partRole == null || partRole.isBlank()) { + partRole = inferPartRole(row); + } + if (partRole == null || partRole.isBlank()) { partRole = "unknown"; } + p.setPartRole(partRole); } private void upsertOfferFromRow(Product product, Merchant merchant, MerchantFeedRow row) { @@ -263,35 +281,6 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService productOfferRepository.save(offer); } - private String resolvePartRole(Merchant merchant, MerchantFeedRow row) { - // Build a merchant-specific raw category key like "Department > Category > SubCategory" - String rawCategoryKey = buildRawCategoryKey(row); - - if (rawCategoryKey != null) { - // 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; - } - } - - // Fallback: keyword-based inference - String keywordRole = inferPartRole(row); - if (keywordRole != null && !keywordRole.isBlank()) { - return keywordRole; - } - - // Last resort: log as unmapped and return null - System.out.println("IMPORT !!! UNMAPPED CATEGORY for merchant=" + merchant.getName() - + ", rawCategoryKey='" + rawCategoryKey + "'" - + ", sku=" + row.sku() - + ", productName=" + row.productName()); - - return null; - } // --------------------------------------------------------------------- // Feed reading + brand resolution