mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-21 01:01:05 -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.persistence.*;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
import org.hibernate.annotations.ColumnDefault;
|
|
||||||
import org.hibernate.annotations.OnDelete;
|
import org.hibernate.annotations.OnDelete;
|
||||||
import org.hibernate.annotations.OnDeleteAction;
|
import org.hibernate.annotations.OnDeleteAction;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.OffsetDateTime;
|
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
|
@Entity
|
||||||
@Table(name = "merchant_category_map")
|
@Table(name = "merchant_category_mappings")
|
||||||
public class MerchantCategoryMap {
|
public class MerchantCategoryMap {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "id", nullable = false)
|
@Column(name = "id", nullable = false)
|
||||||
@@ -28,147 +36,44 @@ public class MerchantCategoryMap {
|
|||||||
@Column(name = "raw_category", nullable = false, length = Integer.MAX_VALUE)
|
@Column(name = "raw_category", nullable = false, length = Integer.MAX_VALUE)
|
||||||
private String rawCategory;
|
private String rawCategory;
|
||||||
|
|
||||||
@Column(name = "canonical_part_role", length = Integer.MAX_VALUE)
|
@Column(name = "mapped_part_role", length = Integer.MAX_VALUE)
|
||||||
private String canonicalPartRole;
|
private String mappedPartRole;
|
||||||
|
|
||||||
@Column(name = "confidence", precision = 5, scale = 2)
|
@Column(name = "mapped_configuration", length = Integer.MAX_VALUE)
|
||||||
private BigDecimal confidence;
|
private String mappedConfiguration;
|
||||||
|
|
||||||
@Column(name = "notes", length = Integer.MAX_VALUE)
|
|
||||||
private String notes;
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ColumnDefault("now()")
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private OffsetDateTime createdAt;
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ColumnDefault("now()")
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
@Column(name = "updated_at", nullable = false)
|
||||||
private OffsetDateTime updatedAt;
|
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")
|
@Column(name = "deleted_at")
|
||||||
private OffsetDateTime deletedAt;
|
private OffsetDateTime deletedAt;
|
||||||
|
|
||||||
public Integer getId() {
|
public Integer getId() { return id; }
|
||||||
return id;
|
public void setId(Integer id) { this.id = id; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Integer id) {
|
public Merchant getMerchant() { return merchant; }
|
||||||
this.id = id;
|
public void setMerchant(Merchant merchant) { this.merchant = merchant; }
|
||||||
}
|
|
||||||
|
|
||||||
public Merchant getMerchant() {
|
public String getRawCategory() { return rawCategory; }
|
||||||
return merchant;
|
public void setRawCategory(String rawCategory) { this.rawCategory = rawCategory; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setMerchant(Merchant merchant) {
|
public String getMappedPartRole() { return mappedPartRole; }
|
||||||
this.merchant = merchant;
|
public void setMappedPartRole(String mappedPartRole) { this.mappedPartRole = mappedPartRole; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getRawCategory() {
|
public String getMappedConfiguration() { return mappedConfiguration; }
|
||||||
return rawCategory;
|
public void setMappedConfiguration(String mappedConfiguration) { this.mappedConfiguration = mappedConfiguration; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setRawCategory(String rawCategory) {
|
public OffsetDateTime getCreatedAt() { return createdAt; }
|
||||||
this.rawCategory = rawCategory;
|
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getCanonicalPartRole() {
|
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
||||||
return canonicalPartRole;
|
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
}
|
|
||||||
|
|
||||||
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 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
|
@Repository
|
||||||
public interface MerchantCategoryMapRepository extends JpaRepository<MerchantCategoryMap, Integer> {
|
public interface MerchantCategoryMapRepository extends JpaRepository<MerchantCategoryMap, Integer> {
|
||||||
|
|
||||||
List<MerchantCategoryMap> findAllByMerchantIdAndRawCategoryAndEnabledTrue(
|
List<MerchantCategoryMap> findAllByMerchant_IdAndRawCategoryAndDeletedAtIsNull(
|
||||||
Integer merchantId,
|
Integer merchantId,
|
||||||
String rawCategory
|
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("merchantId") Integer merchantId,
|
||||||
@Param("status") ImportStatus status
|
@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;
|
package group.goforward.battlbuilder.services.impl;
|
||||||
|
|
||||||
|
import group.goforward.battlbuilder.classification.PartRoleResolver;
|
||||||
import group.goforward.battlbuilder.imports.MerchantFeedRow;
|
import group.goforward.battlbuilder.imports.MerchantFeedRow;
|
||||||
import group.goforward.battlbuilder.model.Merchant;
|
import group.goforward.battlbuilder.model.Merchant;
|
||||||
import group.goforward.battlbuilder.model.MerchantCategoryMap;
|
import group.goforward.battlbuilder.model.MerchantCategoryMap;
|
||||||
@@ -7,7 +8,6 @@ import group.goforward.battlbuilder.repos.MerchantCategoryMapRepository;
|
|||||||
import group.goforward.battlbuilder.services.CategoryClassificationService;
|
import group.goforward.battlbuilder.services.CategoryClassificationService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -16,80 +16,53 @@ import java.util.Optional;
|
|||||||
public class CategoryClassificationServiceImpl implements CategoryClassificationService {
|
public class CategoryClassificationServiceImpl implements CategoryClassificationService {
|
||||||
|
|
||||||
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
private final MerchantCategoryMapRepository merchantCategoryMapRepository;
|
||||||
|
private final PartRoleResolver partRoleResolver;
|
||||||
|
|
||||||
public CategoryClassificationServiceImpl(MerchantCategoryMapRepository merchantCategoryMapRepository) {
|
public CategoryClassificationServiceImpl(
|
||||||
|
MerchantCategoryMapRepository merchantCategoryMapRepository,
|
||||||
|
PartRoleResolver partRoleResolver
|
||||||
|
) {
|
||||||
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
this.merchantCategoryMapRepository = merchantCategoryMapRepository;
|
||||||
|
this.partRoleResolver = partRoleResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result classify(Merchant merchant, MerchantFeedRow row) {
|
public Result classify(Merchant merchant, MerchantFeedRow row) {
|
||||||
String rawCategoryKey = buildRawCategoryKey(row);
|
String rawCategoryKey = buildRawCategoryKey(row);
|
||||||
|
|
||||||
// 1) Platform from mapping (if present), else infer
|
// Platform is inferred from feed/rules; mapping table does not store platform.
|
||||||
String platform = resolvePlatformFromMapping(merchant, rawCategoryKey)
|
String platform = inferPlatform(row);
|
||||||
.orElseGet(() -> inferPlatform(row));
|
if (platform == null) platform = "AR-15";
|
||||||
if (platform == null) {
|
|
||||||
platform = "AR-15";
|
final String platformFinal = platform;
|
||||||
|
final String rawCategoryKeyFinal = rawCategoryKey;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
partRole = normalizePartRole(partRole);
|
||||||
|
|
||||||
|
return new Result(platformFinal, partRole, rawCategoryKeyFinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Part role from mapping (if present), else infer
|
private Optional<String> resolvePartRoleFromMapping(Merchant merchant, String rawCategoryKey) {
|
||||||
String partRole = resolvePartRoleFromMapping(merchant, rawCategoryKey, platform)
|
if (merchant == null || rawCategoryKey == null) return Optional.empty();
|
||||||
.orElseGet(() -> inferPartRole(row));
|
|
||||||
|
|
||||||
if (partRole == null || partRole.isBlank()) {
|
|
||||||
partRole = "UNKNOWN";
|
|
||||||
} else {
|
|
||||||
partRole = partRole.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Result(platform, partRole, rawCategoryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> resolvePlatformFromMapping(Merchant merchant, String rawCategoryKey) {
|
|
||||||
if (rawCategoryKey == null) return Optional.empty();
|
|
||||||
|
|
||||||
List<MerchantCategoryMap> mappings =
|
List<MerchantCategoryMap> mappings =
|
||||||
merchantCategoryMapRepository.findAllByMerchantIdAndRawCategoryAndEnabledTrue(
|
merchantCategoryMapRepository.findAllByMerchant_IdAndRawCategoryAndDeletedAtIsNull(
|
||||||
merchant.getId(), rawCategoryKey
|
merchant.getId(), rawCategoryKey
|
||||||
);
|
);
|
||||||
|
|
||||||
return mappings.stream()
|
return mappings.stream()
|
||||||
.sorted(Comparator
|
.map(m -> m.getMappedPartRole())
|
||||||
.comparing((MerchantCategoryMap m) -> m.getPlatform() == null) // exact platform last
|
.filter(r -> r != null && !r.isBlank())
|
||||||
.thenComparing(MerchantCategoryMap::getConfidence, Comparator.nullsLast(Comparator.reverseOrder()))
|
|
||||||
.thenComparing(MerchantCategoryMap::getId))
|
|
||||||
.map(MerchantCategoryMap::getPlatform)
|
|
||||||
.filter(p -> p != null && !p.isBlank())
|
|
||||||
.findFirst();
|
.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) {
|
private String buildRawCategoryKey(MerchantFeedRow row) {
|
||||||
String dept = trimOrNull(row.department());
|
String dept = trimOrNull(row.department());
|
||||||
String cat = trimOrNull(row.category());
|
String cat = trimOrNull(row.category());
|
||||||
@@ -105,6 +78,7 @@ public class CategoryClassificationServiceImpl implements CategoryClassification
|
|||||||
if (!sb.isEmpty()) sb.append(" > ");
|
if (!sb.isEmpty()) sb.append(" > ");
|
||||||
sb.append(sub);
|
sb.append(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
String result = sb.toString();
|
String result = sb.toString();
|
||||||
return result.isBlank() ? null : result;
|
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("ar-10") || lower.contains("ar10")) return "AR-10";
|
||||||
if (lower.contains("ak-47") || lower.contains("ak47")) return "AK-47";
|
if (lower.contains("ak-47") || lower.contains("ak47")) return "AK-47";
|
||||||
|
|
||||||
|
// default
|
||||||
return "AR-15";
|
return "AR-15";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,31 +108,24 @@ public class CategoryClassificationServiceImpl implements CategoryClassification
|
|||||||
|
|
||||||
String lower = cat.toLowerCase(Locale.ROOT);
|
String lower = cat.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
if (lower.contains("handguard") || lower.contains("rail")) {
|
if (lower.contains("handguard") || lower.contains("rail")) return "handguard";
|
||||||
return "handguard";
|
if (lower.contains("barrel")) return "barrel";
|
||||||
}
|
if (lower.contains("upper")) return "upper-receiver";
|
||||||
if (lower.contains("barrel")) {
|
if (lower.contains("lower")) return "lower-receiver";
|
||||||
return "barrel";
|
if (lower.contains("magazine") || lower.contains("mag")) return "magazine";
|
||||||
}
|
if (lower.contains("stock") || lower.contains("buttstock")) return "stock";
|
||||||
if (lower.contains("upper")) {
|
if (lower.contains("grip")) return "grip";
|
||||||
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";
|
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) {
|
private String trimOrNull(String v) {
|
||||||
if (v == null) return null;
|
if (v == null) return null;
|
||||||
String t = v.trim();
|
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