mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-05 18:46:44 -05:00
new categories and mapping logic
This commit is contained in:
@@ -3,6 +3,8 @@ package group.goforward.ballistic.model;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ProductConfiguration;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "merchant_category_mappings",
|
name = "merchant_category_mappings",
|
||||||
@@ -28,6 +30,10 @@ public class MerchantCategoryMapping {
|
|||||||
@Column(name = "mapped_part_role", length = 128)
|
@Column(name = "mapped_part_role", length = 128)
|
||||||
private String mappedPartRole; // e.g. "upper-receiver", "barrel"
|
private String mappedPartRole; // e.g. "upper-receiver", "barrel"
|
||||||
|
|
||||||
|
@Column(name = "mapped_configuration")
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private ProductConfiguration mappedConfiguration;
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private OffsetDateTime createdAt = OffsetDateTime.now();
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
@@ -73,6 +79,14 @@ public class MerchantCategoryMapping {
|
|||||||
this.mappedPartRole = mappedPartRole;
|
this.mappedPartRole = mappedPartRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProductConfiguration getMappedConfiguration() {
|
||||||
|
return mappedConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMappedConfiguration(ProductConfiguration mappedConfiguration) {
|
||||||
|
this.mappedConfiguration = mappedConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
public OffsetDateTime getCreatedAt() {
|
public OffsetDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import jakarta.persistence.*;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ProductConfiguration;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "products")
|
@Table(name = "products")
|
||||||
public class Product {
|
public class Product {
|
||||||
@@ -38,6 +40,10 @@ public class Product {
|
|||||||
@Column(name = "part_role")
|
@Column(name = "part_role")
|
||||||
private String partRole;
|
private String partRole;
|
||||||
|
|
||||||
|
@Column(name = "configuration")
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private ProductConfiguration configuration;
|
||||||
|
|
||||||
@Column(name = "short_description")
|
@Column(name = "short_description")
|
||||||
private String shortDescription;
|
private String shortDescription;
|
||||||
|
|
||||||
@@ -223,4 +229,11 @@ public class Product {
|
|||||||
this.platformLocked = platformLocked;
|
this.platformLocked = platformLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProductConfiguration getConfiguration() {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfiguration(ProductConfiguration configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package group.goforward.ballistic.services;
|
|||||||
|
|
||||||
import group.goforward.ballistic.model.Merchant;
|
import group.goforward.ballistic.model.Merchant;
|
||||||
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
|
import group.goforward.ballistic.model.ProductConfiguration;
|
||||||
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,41 +23,44 @@ public class MerchantCategoryMappingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a partRole for a given raw category.
|
* Resolve (or create) a mapping row for this merchant + raw category.
|
||||||
* If not found, create a row with null mappedPartRole and return null (so importer can skip).
|
* - 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
|
@Transactional
|
||||||
public String resolvePartRole(Merchant merchant, String rawCategory) {
|
public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
|
||||||
if (rawCategory == null || rawCategory.isBlank()) {
|
if (rawCategory == null || rawCategory.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String trimmed = rawCategory.trim();
|
String trimmed = rawCategory.trim();
|
||||||
|
|
||||||
Optional<MerchantCategoryMapping> existingOpt =
|
return mappingRepository
|
||||||
mappingRepository.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed);
|
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
|
||||||
|
.orElseGet(() -> {
|
||||||
if (existingOpt.isPresent()) {
|
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
|
||||||
return existingOpt.get().getMappedPartRole();
|
mapping.setMerchant(merchant);
|
||||||
}
|
mapping.setRawCategory(trimmed);
|
||||||
|
mapping.setMappedPartRole(null);
|
||||||
// Create placeholder row
|
mapping.setMappedConfiguration(null);
|
||||||
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
|
return mappingRepository.save(mapping);
|
||||||
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).
|
* Upsert mapping (admin UI).
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@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();
|
String trimmed = rawCategory.trim();
|
||||||
|
|
||||||
MerchantCategoryMapping mapping = mappingRepository
|
MerchantCategoryMapping mapping = mappingRepository
|
||||||
@@ -72,6 +76,21 @@ public class MerchantCategoryMappingService {
|
|||||||
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
|
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mapping.setMappedConfiguration(mappedConfiguration);
|
||||||
|
|
||||||
return mappingRepository.save(mapping);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ 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.services.MerchantCategoryMappingService;
|
import group.goforward.ballistic.services.MerchantCategoryMappingService;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
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;
|
||||||
@@ -190,11 +191,28 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
String rawCategoryKey = buildRawCategoryKey(row);
|
String rawCategoryKey = buildRawCategoryKey(row);
|
||||||
p.setRawCategoryKey(rawCategoryKey);
|
p.setRawCategoryKey(rawCategoryKey);
|
||||||
|
|
||||||
// ---------- PART ROLE ----------
|
// ---------- PART ROLE (via category mapping, with keyword fallback) ----------
|
||||||
String partRole = resolvePartRole(merchant, row);
|
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()) {
|
if (partRole == null || partRole.isBlank()) {
|
||||||
partRole = "unknown";
|
partRole = "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
p.setPartRole(partRole);
|
p.setPartRole(partRole);
|
||||||
}
|
}
|
||||||
private void upsertOfferFromRow(Product product, Merchant merchant, MerchantFeedRow row) {
|
private void upsertOfferFromRow(Product product, Merchant merchant, MerchantFeedRow row) {
|
||||||
@@ -263,35 +281,6 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
productOfferRepository.save(offer);
|
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
|
// Feed reading + brand resolution
|
||||||
|
|||||||
Reference in New Issue
Block a user