mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
reworked the importers category mapping. whew.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package group.goforward.battlbuilder.classification;
|
||||
|
||||
import group.goforward.battlbuilder.model.PartRoleRule;
|
||||
import group.goforward.battlbuilder.repos.PartRoleRuleRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Component
|
||||
public class PartRoleResolver {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PartRoleResolver.class);
|
||||
|
||||
private final PartRoleRuleRepository repo;
|
||||
|
||||
private final List<CompiledRule> rules = new ArrayList<>();
|
||||
|
||||
public PartRoleResolver(PartRoleRuleRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void load() {
|
||||
rules.clear();
|
||||
|
||||
List<PartRoleRule> active = repo.findAllByActiveTrueOrderByPriorityDescIdAsc();
|
||||
for (PartRoleRule r : active) {
|
||||
try {
|
||||
rules.add(new CompiledRule(
|
||||
r.getId(),
|
||||
r.getTargetPlatform(),
|
||||
Pattern.compile(r.getNameRegex(), Pattern.CASE_INSENSITIVE),
|
||||
normalizeRole(r.getTargetPartRole())
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.warn("Skipping invalid part role rule id={} regex={} err={}",
|
||||
r.getId(), r.getNameRegex(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Loaded {} part role rules", rules.size());
|
||||
}
|
||||
|
||||
public String resolve(String platform, String productName, String rawCategoryKey) {
|
||||
String p = normalizePlatform(platform);
|
||||
|
||||
// we match primarily on productName; optionally also include rawCategoryKey in the text blob
|
||||
String text = (productName == null ? "" : productName) +
|
||||
" " +
|
||||
(rawCategoryKey == null ? "" : rawCategoryKey);
|
||||
|
||||
for (CompiledRule r : rules) {
|
||||
if (!r.appliesToPlatform(p)) continue;
|
||||
if (r.pattern.matcher(text).find()) {
|
||||
return r.targetPartRole; // already normalized
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeRole(String role) {
|
||||
if (role == null) return null;
|
||||
String t = role.trim();
|
||||
if (t.isEmpty()) return null;
|
||||
return t.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static String normalizePlatform(String platform) {
|
||||
if (platform == null) return null;
|
||||
String t = platform.trim();
|
||||
return t.isEmpty() ? null : t.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private record CompiledRule(Long id, String targetPlatform, Pattern pattern, String targetPartRole) {
|
||||
boolean appliesToPlatform(String platform) {
|
||||
if (targetPlatform == null || targetPlatform.isBlank()) return true;
|
||||
if (platform == null) return false;
|
||||
return targetPlatform.trim().equalsIgnoreCase(platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,25 @@ package group.goforward.battlbuilder.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.hibernate.annotations.OnDelete;
|
||||
import org.hibernate.annotations.OnDeleteAction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* merchant_category_mappings is intentionally limited to:
|
||||
* - merchant
|
||||
* - raw_category
|
||||
* - mapped_part_role
|
||||
*
|
||||
* It does NOT determine platform or confidence.
|
||||
* Platform is inferred at classification time by feed/rules.
|
||||
*/
|
||||
|
||||
@Entity
|
||||
@Table(name = "merchant_category_map")
|
||||
@Table(name = "merchant_category_mappings")
|
||||
public class MerchantCategoryMap {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
@@ -28,147 +36,44 @@ public class MerchantCategoryMap {
|
||||
@Column(name = "raw_category", nullable = false, length = Integer.MAX_VALUE)
|
||||
private String rawCategory;
|
||||
|
||||
@Column(name = "canonical_part_role", length = Integer.MAX_VALUE)
|
||||
private String canonicalPartRole;
|
||||
@Column(name = "mapped_part_role", length = Integer.MAX_VALUE)
|
||||
private String mappedPartRole;
|
||||
|
||||
@Column(name = "confidence", precision = 5, scale = 2)
|
||||
private BigDecimal confidence;
|
||||
|
||||
@Column(name = "notes", length = Integer.MAX_VALUE)
|
||||
private String notes;
|
||||
@Column(name = "mapped_configuration", length = Integer.MAX_VALUE)
|
||||
private String mappedConfiguration;
|
||||
|
||||
@NotNull
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
@NotNull
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private OffsetDateTime updatedAt;
|
||||
|
||||
@Size(max = 255)
|
||||
@Column(name = "canonical_category")
|
||||
private String canonicalCategory;
|
||||
|
||||
@NotNull
|
||||
@ColumnDefault("true")
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Boolean enabled = false;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "platform", length = 100)
|
||||
private String platform;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "part_role", length = 100)
|
||||
private String partRole;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private OffsetDateTime deletedAt;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
public Integer getId() { return id; }
|
||||
public void setId(Integer id) { this.id = id; }
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
public Merchant getMerchant() { return merchant; }
|
||||
public void setMerchant(Merchant merchant) { this.merchant = merchant; }
|
||||
|
||||
public Merchant getMerchant() {
|
||||
return merchant;
|
||||
}
|
||||
public String getRawCategory() { return rawCategory; }
|
||||
public void setRawCategory(String rawCategory) { this.rawCategory = rawCategory; }
|
||||
|
||||
public void setMerchant(Merchant merchant) {
|
||||
this.merchant = merchant;
|
||||
}
|
||||
public String getMappedPartRole() { return mappedPartRole; }
|
||||
public void setMappedPartRole(String mappedPartRole) { this.mappedPartRole = mappedPartRole; }
|
||||
|
||||
public String getRawCategory() {
|
||||
return rawCategory;
|
||||
}
|
||||
public String getMappedConfiguration() { return mappedConfiguration; }
|
||||
public void setMappedConfiguration(String mappedConfiguration) { this.mappedConfiguration = mappedConfiguration; }
|
||||
|
||||
public void setRawCategory(String rawCategory) {
|
||||
this.rawCategory = rawCategory;
|
||||
}
|
||||
public OffsetDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public String getCanonicalPartRole() {
|
||||
return canonicalPartRole;
|
||||
}
|
||||
|
||||
public void setCanonicalPartRole(String canonicalPartRole) {
|
||||
this.canonicalPartRole = canonicalPartRole;
|
||||
}
|
||||
|
||||
public BigDecimal getConfidence() {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
public void setConfidence(BigDecimal confidence) {
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public String getCanonicalCategory() {
|
||||
return canonicalCategory;
|
||||
}
|
||||
|
||||
public void setCanonicalCategory(String canonicalCategory) {
|
||||
this.canonicalCategory = canonicalCategory;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
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 OffsetDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(OffsetDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public OffsetDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(OffsetDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package group.goforward.battlbuilder.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "part_role_rules")
|
||||
public class PartRoleRule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean active = true;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int priority = 0;
|
||||
|
||||
@Column(name = "target_platform")
|
||||
private String targetPlatform; // nullable = applies to all
|
||||
|
||||
@Column(name = "name_regex", nullable = false)
|
||||
private String nameRegex;
|
||||
|
||||
@Column(name = "target_part_role", nullable = false)
|
||||
private String targetPartRole;
|
||||
|
||||
@Column
|
||||
private String notes;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt = Instant.now();
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
// getters/setters
|
||||
|
||||
public Long getId() { return id; }
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public int getPriority() { return priority; }
|
||||
public void setPriority(int priority) { this.priority = priority; }
|
||||
|
||||
public String getTargetPlatform() { return targetPlatform; }
|
||||
public void setTargetPlatform(String targetPlatform) { this.targetPlatform = targetPlatform; }
|
||||
|
||||
public String getNameRegex() { return nameRegex; }
|
||||
public void setNameRegex(String nameRegex) { this.nameRegex = nameRegex; }
|
||||
|
||||
public String getTargetPartRole() { return targetPartRole; }
|
||||
public void setTargetPartRole(String targetPartRole) { this.targetPartRole = targetPartRole; }
|
||||
|
||||
public String getNotes() { return notes; }
|
||||
public void setNotes(String notes) { this.notes = notes; }
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import java.util.List;
|
||||
@Repository
|
||||
public interface MerchantCategoryMapRepository extends JpaRepository<MerchantCategoryMap, Integer> {
|
||||
|
||||
List<MerchantCategoryMap> findAllByMerchantIdAndRawCategoryAndEnabledTrue(
|
||||
List<MerchantCategoryMap> findAllByMerchant_IdAndRawCategoryAndDeletedAtIsNull(
|
||||
Integer merchantId,
|
||||
String rawCategory
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package group.goforward.battlbuilder.repos;
|
||||
|
||||
import group.goforward.battlbuilder.model.PartRoleRule;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PartRoleRuleRepository extends JpaRepository<PartRoleRule, Long> {
|
||||
List<PartRoleRule> findAllByActiveTrueOrderByPriorityDescIdAsc();
|
||||
}
|
||||
@@ -201,4 +201,14 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
|
||||
@Param("merchantId") Integer merchantId,
|
||||
@Param("status") ImportStatus status
|
||||
);
|
||||
|
||||
@Query(value = """
|
||||
select distinct p.*
|
||||
from products p
|
||||
join product_offers po on po.product_id = p.id
|
||||
where po.merchant_id = :merchantId
|
||||
and p.import_status = 'PENDING_MAPPING'
|
||||
and p.deleted_at is null
|
||||
""", nativeQuery = true)
|
||||
List<Product> findPendingMappingByMerchantId(@Param("merchantId") Integer merchantId);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package group.goforward.battlbuilder.services;
|
||||
|
||||
public interface ReclassificationService {
|
||||
int reclassifyPendingForMerchant(Integer merchantId);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package group.goforward.battlbuilder.services.impl;
|
||||
|
||||
import group.goforward.battlbuilder.classification.PartRoleResolver;
|
||||
import group.goforward.battlbuilder.imports.MerchantFeedRow;
|
||||
import group.goforward.battlbuilder.model.Merchant;
|
||||
import group.goforward.battlbuilder.model.MerchantCategoryMap;
|
||||
@@ -7,7 +8,6 @@ import group.goforward.battlbuilder.repos.MerchantCategoryMapRepository;
|
||||
import group.goforward.battlbuilder.services.CategoryClassificationService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
@@ -16,84 +16,57 @@ import java.util.Optional;
|
||||
public class CategoryClassificationServiceImpl implements CategoryClassificationService {
|
||||
|
||||
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||
private final PartRoleResolver partRoleResolver;
|
||||
|
||||
public CategoryClassificationServiceImpl(MerchantCategoryMapRepository merchantCategoryMapRepository) {
|
||||
public CategoryClassificationServiceImpl(
|
||||
MerchantCategoryMapRepository merchantCategoryMapRepository,
|
||||
PartRoleResolver partRoleResolver
|
||||
) {
|
||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||
this.partRoleResolver = partRoleResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result classify(Merchant merchant, MerchantFeedRow row) {
|
||||
String rawCategoryKey = buildRawCategoryKey(row);
|
||||
|
||||
// 1) Platform from mapping (if present), else infer
|
||||
String platform = resolvePlatformFromMapping(merchant, rawCategoryKey)
|
||||
.orElseGet(() -> inferPlatform(row));
|
||||
if (platform == null) {
|
||||
platform = "AR-15";
|
||||
}
|
||||
// Platform is inferred from feed/rules; mapping table does not store platform.
|
||||
String platform = inferPlatform(row);
|
||||
if (platform == null) platform = "AR-15";
|
||||
|
||||
// 2) Part role from mapping (if present), else infer
|
||||
String partRole = resolvePartRoleFromMapping(merchant, rawCategoryKey, platform)
|
||||
.orElseGet(() -> inferPartRole(row));
|
||||
final String platformFinal = platform;
|
||||
final String rawCategoryKeyFinal = rawCategoryKey;
|
||||
|
||||
if (partRole == null || partRole.isBlank()) {
|
||||
partRole = "UNKNOWN";
|
||||
} else {
|
||||
partRole = partRole.trim();
|
||||
}
|
||||
// Part role from mapping (if present), else rules, else infer
|
||||
String partRole = resolvePartRoleFromMapping(merchant, rawCategoryKeyFinal)
|
||||
.orElseGet(() -> {
|
||||
String resolved = partRoleResolver.resolve(platformFinal, row.productName(), rawCategoryKeyFinal);
|
||||
return resolved != null ? resolved : inferPartRole(row);
|
||||
});
|
||||
|
||||
return new Result(platform, partRole, rawCategoryKey);
|
||||
partRole = normalizePartRole(partRole);
|
||||
|
||||
return new Result(platformFinal, partRole, rawCategoryKeyFinal);
|
||||
}
|
||||
|
||||
private Optional<String> resolvePlatformFromMapping(Merchant merchant, String rawCategoryKey) {
|
||||
if (rawCategoryKey == null) return Optional.empty();
|
||||
private Optional<String> resolvePartRoleFromMapping(Merchant merchant, String rawCategoryKey) {
|
||||
if (merchant == null || rawCategoryKey == null) return Optional.empty();
|
||||
|
||||
List<MerchantCategoryMap> mappings =
|
||||
merchantCategoryMapRepository.findAllByMerchantIdAndRawCategoryAndEnabledTrue(
|
||||
merchantCategoryMapRepository.findAllByMerchant_IdAndRawCategoryAndDeletedAtIsNull(
|
||||
merchant.getId(), rawCategoryKey
|
||||
);
|
||||
|
||||
return mappings.stream()
|
||||
.sorted(Comparator
|
||||
.comparing((MerchantCategoryMap m) -> m.getPlatform() == null) // exact platform last
|
||||
.thenComparing(MerchantCategoryMap::getConfidence, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(MerchantCategoryMap::getId))
|
||||
.map(MerchantCategoryMap::getPlatform)
|
||||
.filter(p -> p != null && !p.isBlank())
|
||||
.map(m -> m.getMappedPartRole())
|
||||
.filter(r -> r != null && !r.isBlank())
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private Optional<String> resolvePartRoleFromMapping(Merchant merchant,
|
||||
String rawCategoryKey,
|
||||
String platform) {
|
||||
if (rawCategoryKey == null) return Optional.empty();
|
||||
|
||||
List<MerchantCategoryMap> mappings =
|
||||
merchantCategoryMapRepository.findAllByMerchantIdAndRawCategoryAndEnabledTrue(
|
||||
merchant.getId(), rawCategoryKey
|
||||
);
|
||||
|
||||
return mappings.stream()
|
||||
.filter(m -> m.getPartRole() != null && !m.getPartRole().isBlank())
|
||||
// prefer explicit platform, but allow null platform
|
||||
.sorted(Comparator
|
||||
.comparing((MerchantCategoryMap m) -> !platformEquals(m.getPlatform(), platform))
|
||||
.thenComparing(MerchantCategoryMap::getConfidence, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(MerchantCategoryMap::getId))
|
||||
.map(MerchantCategoryMap::getPartRole)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private boolean platformEquals(String a, String b) {
|
||||
if (a == null || b == null) return false;
|
||||
return a.equalsIgnoreCase(b);
|
||||
}
|
||||
|
||||
// You can reuse logic from MerchantFeedImportServiceImpl, but I’ll inline equivalents here
|
||||
private String buildRawCategoryKey(MerchantFeedRow row) {
|
||||
String dept = trimOrNull(row.department());
|
||||
String cat = trimOrNull(row.category());
|
||||
String sub = trimOrNull(row.subCategory());
|
||||
String cat = trimOrNull(row.category());
|
||||
String sub = trimOrNull(row.subCategory());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (dept != null) sb.append(dept);
|
||||
@@ -105,6 +78,7 @@ public class CategoryClassificationServiceImpl implements CategoryClassification
|
||||
if (!sb.isEmpty()) sb.append(" > ");
|
||||
sb.append(sub);
|
||||
}
|
||||
|
||||
String result = sb.toString();
|
||||
return result.isBlank() ? null : result;
|
||||
}
|
||||
@@ -121,6 +95,7 @@ public class CategoryClassificationServiceImpl implements CategoryClassification
|
||||
if (lower.contains("ar-10") || lower.contains("ar10")) return "AR-10";
|
||||
if (lower.contains("ak-47") || lower.contains("ak47")) return "AK-47";
|
||||
|
||||
// default
|
||||
return "AR-15";
|
||||
}
|
||||
|
||||
@@ -133,31 +108,24 @@ public class CategoryClassificationServiceImpl implements CategoryClassification
|
||||
|
||||
String lower = cat.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (lower.contains("handguard") || lower.contains("rail")) {
|
||||
return "handguard";
|
||||
}
|
||||
if (lower.contains("barrel")) {
|
||||
return "barrel";
|
||||
}
|
||||
if (lower.contains("upper")) {
|
||||
return "upper-receiver";
|
||||
}
|
||||
if (lower.contains("lower")) {
|
||||
return "lower-receiver";
|
||||
}
|
||||
if (lower.contains("magazine") || lower.contains("mag")) {
|
||||
return "magazine";
|
||||
}
|
||||
if (lower.contains("stock") || lower.contains("buttstock")) {
|
||||
return "stock";
|
||||
}
|
||||
if (lower.contains("grip")) {
|
||||
return "grip";
|
||||
}
|
||||
if (lower.contains("handguard") || lower.contains("rail")) return "handguard";
|
||||
if (lower.contains("barrel")) return "barrel";
|
||||
if (lower.contains("upper")) return "upper-receiver";
|
||||
if (lower.contains("lower")) return "lower-receiver";
|
||||
if (lower.contains("magazine") || lower.contains("mag")) return "magazine";
|
||||
if (lower.contains("stock") || lower.contains("buttstock")) return "stock";
|
||||
if (lower.contains("grip")) return "grip";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private String normalizePartRole(String partRole) {
|
||||
if (partRole == null) return "unknown";
|
||||
String t = partRole.trim().toLowerCase(Locale.ROOT)
|
||||
.replace('_', '-');
|
||||
return t.isBlank() ? "unknown" : t;
|
||||
}
|
||||
|
||||
private String trimOrNull(String v) {
|
||||
if (v == null) return null;
|
||||
String t = v.trim();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package group.goforward.battlbuilder.services.impl;
|
||||
|
||||
import group.goforward.battlbuilder.model.ImportStatus;
|
||||
import group.goforward.battlbuilder.model.Merchant;
|
||||
import group.goforward.battlbuilder.model.MerchantCategoryMap;
|
||||
import group.goforward.battlbuilder.model.Product;
|
||||
import group.goforward.battlbuilder.repos.MerchantCategoryMapRepository;
|
||||
import group.goforward.battlbuilder.repos.MerchantRepository;
|
||||
import group.goforward.battlbuilder.repos.ProductRepository;
|
||||
import group.goforward.battlbuilder.services.ReclassificationService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class ReclassificationServiceImpl implements ReclassificationService {
|
||||
|
||||
private final ProductRepository productRepository;
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||
|
||||
public ReclassificationServiceImpl(
|
||||
ProductRepository productRepository,
|
||||
MerchantRepository merchantRepository,
|
||||
MerchantCategoryMapRepository merchantCategoryMapRepository
|
||||
) {
|
||||
this.productRepository = productRepository;
|
||||
this.merchantRepository = merchantRepository;
|
||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int reclassifyPendingForMerchant(Integer merchantId) {
|
||||
// validate merchant exists (helps avoid silent failures)
|
||||
Merchant merchant = merchantRepository.findById(merchantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + merchantId));
|
||||
|
||||
// products that are pending for THIS merchant (via offers join in repo)
|
||||
List<Product> pending = productRepository.findPendingMappingByMerchantId(merchantId);
|
||||
|
||||
int updated = 0;
|
||||
|
||||
for (Product p : pending) {
|
||||
// IMPORTANT: this assumes Product has rawCategoryKey stored (your DB does).
|
||||
// If your getter name differs, change this line accordingly.
|
||||
String rawCategoryKey = p.getRawCategoryKey();
|
||||
if (rawCategoryKey == null || rawCategoryKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<String> mappedRole = resolveMappedPartRole(merchant.getId(), rawCategoryKey);
|
||||
if (mappedRole.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String normalized = normalizePartRole(mappedRole.get());
|
||||
if ("unknown".equals(normalized)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
p.setPartRole(normalized);
|
||||
p.setImportStatus(ImportStatus.MAPPED);
|
||||
updated++;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private Optional<String> resolveMappedPartRole(Integer merchantId, String rawCategoryKey) {
|
||||
// NOTE: MerchantCategoryMap has a ManyToOne `merchant`, so we query via merchant.id traversal.
|
||||
List<MerchantCategoryMap> mappings =
|
||||
merchantCategoryMapRepository.findAllByMerchant_IdAndRawCategoryAndDeletedAtIsNull(
|
||||
merchantId, rawCategoryKey
|
||||
);
|
||||
|
||||
return mappings.stream()
|
||||
.map(MerchantCategoryMap::getMappedPartRole)
|
||||
.filter(v -> v != null && !v.isBlank())
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private String normalizePartRole(String partRole) {
|
||||
if (partRole == null) return "unknown";
|
||||
String t = partRole.trim().toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
return t.isBlank() ? "unknown" : t;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user