Compare commits

...

2 Commits

23 changed files with 307 additions and 208 deletions

View File

@@ -7,7 +7,9 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
//@ComponentScan(basePackages = "group.goforward.ballistic")
@ComponentScan("group.goforward.ballistic.controllers")
@ComponentScan("group.goforward.ballistic.repos")
@ComponentScan("group.goforward.ballistic.services")
@EntityScan(basePackages = "group.goforward.ballistic.model")
@EnableJpaRepositories(basePackages = "group.goforward.ballistic.repos")
public class BallisticApplication {

View File

@@ -1 +1,13 @@
/**
* Provides the classes necessary for the Spring Configurations for the ballistic -Builder application.
* This package includes Configurations for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
*
* @since 1.0
* @author Don Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.configuration;

View File

@@ -1,6 +1,6 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.imports.MerchantFeedImportService;
import group.goforward.ballistic.services.MerchantFeedImportService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

View File

@@ -3,7 +3,7 @@ package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import group.goforward.ballistic.repos.MerchantRepository;
import group.goforward.ballistic.service.MerchantCategoryMappingService;
import group.goforward.ballistic.services.MerchantCategoryMappingService;
import group.goforward.ballistic.web.dto.MerchantCategoryMappingDto;
import group.goforward.ballistic.web.dto.UpsertMerchantCategoryMappingRequest;
import java.util.List;

View File

@@ -1,7 +1,7 @@
package group.goforward.ballistic.controllers;
import group.goforward.ballistic.model.Psa;
import group.goforward.ballistic.service.PsaService;
import group.goforward.ballistic.services.PsaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

View File

@@ -3,7 +3,7 @@ package group.goforward.ballistic.controllers;
import group.goforward.ballistic.ApiResponse;
import group.goforward.ballistic.model.State;
import group.goforward.ballistic.repos.StateRepository;
import group.goforward.ballistic.service.StatesService;
import group.goforward.ballistic.services.StatesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

View File

@@ -0,0 +1 @@
package group.goforward.ballistic.controllers;

View File

@@ -0,0 +1 @@
package group.goforward.ballistic.imports.dto;

View File

@@ -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;
}

View File

@@ -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;
}
public ProductConfiguration getConfiguration() {
return configuration;
}
public void setConfiguration(ProductConfiguration configuration) {
this.configuration = configuration;
}
}

View File

@@ -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
}

View File

@@ -0,0 +1 @@
package group.goforward.ballistic.model;

View File

@@ -0,0 +1,13 @@
/**
* Provides the classes necessary for the Spring Repository for the ballistic -Builder application.
* This package includes Repository for Spring-Boot application
*
*
* <p>The main entry point for managing the inventory is the
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
*
* @since 1.0
* @author Sean Strawsburg
* @version 1.1
*/
package group.goforward.ballistic.repos;

View File

@@ -1,77 +0,0 @@
package group.goforward.ballistic.service;
import group.goforward.ballistic.model.Merchant;
import group.goforward.ballistic.model.MerchantCategoryMapping;
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class MerchantCategoryMappingService {
private final MerchantCategoryMappingRepository mappingRepository;
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
this.mappingRepository = mappingRepository;
}
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
}
/**
* Resolve a partRole for a given raw category.
* If not found, create a row with null mappedPartRole and return null (so importer can skip).
*/
@Transactional
public String resolvePartRole(Merchant merchant, String rawCategory) {
if (rawCategory == null || rawCategory.isBlank()) {
return null;
}
String trimmed = rawCategory.trim();
Optional<MerchantCategoryMapping> 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;
}
/**
* Upsert mapping (admin UI).
*/
@Transactional
public MerchantCategoryMapping upsertMapping(Merchant merchant, String rawCategory, String mappedPartRole) {
String trimmed = rawCategory.trim();
MerchantCategoryMapping mapping = mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping m = new MerchantCategoryMapping();
m.setMerchant(merchant);
m.setRawCategory(trimmed);
return m;
});
mapping.setMappedPartRole(
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
);
return mappingRepository.save(mapping);
}
}

View File

@@ -0,0 +1,96 @@
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;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class MerchantCategoryMappingService {
private final MerchantCategoryMappingRepository mappingRepository;
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
this.mappingRepository = mappingRepository;
}
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
}
/**
* 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 MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
if (rawCategory == null || rawCategory.isBlank()) {
return null;
}
String trimmed = rawCategory.trim();
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,
ProductConfiguration mappedConfiguration
) {
String trimmed = rawCategory.trim();
MerchantCategoryMapping mapping = mappingRepository
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
.orElseGet(() -> {
MerchantCategoryMapping m = new MerchantCategoryMapping();
m.setMerchant(merchant);
m.setRawCategory(trimmed);
return m;
});
mapping.setMappedPartRole(
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
);
mapping.setMappedConfiguration(mappedConfiguration);
return mappingRepository.save(mapping);
}
/**
* Backwards-compatible overload for existing callers (e.g. controller)
* that dont 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);
}
}

View File

@@ -1,4 +1,4 @@
package group.goforward.ballistic.imports;
package group.goforward.ballistic.services;
public interface MerchantFeedImportService {

View File

@@ -1,4 +1,4 @@
package group.goforward.ballistic.service;
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.Psa;
import group.goforward.ballistic.repos.PsaRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -9,7 +9,7 @@ import java.util.Optional;
import java.util.UUID;
@Service
public class PsaService {
public class PsaService implements group.goforward.ballistic.services.impl.PsaService {
private final PsaRepository psaRepository;
@@ -18,18 +18,22 @@ public class PsaService {
this.psaRepository = psaRepository;
}
@Override
public List<Psa> findAll() {
return psaRepository.findAll();
}
@Override
public Optional<Psa> findById(UUID id) {
return psaRepository.findById(id);
}
@Override
public Psa save(Psa psa) {
return psaRepository.save(psa);
}
@Override
public void deleteById(UUID id) {
psaRepository.deleteById(id);
}

View File

@@ -1,4 +1,4 @@
package group.goforward.ballistic.service;
package group.goforward.ballistic.services;
import group.goforward.ballistic.model.State;

View File

@@ -1,4 +1,4 @@
package group.goforward.ballistic.imports;
package group.goforward.ballistic.services.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
@@ -9,6 +9,8 @@ import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import group.goforward.ballistic.imports.MerchantFeedRow;
import group.goforward.ballistic.services.MerchantFeedImportService;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
@@ -19,8 +21,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.service.MerchantCategoryMappingService;
import group.goforward.ballistic.service.MerchantCategoryMappingService;
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;
@@ -189,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) {
@@ -262,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

View File

@@ -0,0 +1,17 @@
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.Psa;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface PsaService {
List<Psa> findAll();
Optional<Psa> findById(UUID id);
Psa save(Psa psa);
void deleteById(UUID id);
}

View File

@@ -1,9 +1,9 @@
package group.goforward.ballistic.service.impl;
package group.goforward.ballistic.services.impl;
import group.goforward.ballistic.model.State;
import group.goforward.ballistic.repos.StateRepository;
import group.goforward.ballistic.service.StatesService;
import group.goforward.ballistic.services.StatesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

View File

@@ -0,0 +1 @@
package group.goforward.ballistic.services.impl;

View File

@@ -0,0 +1 @@
package group.goforward.ballistic.services;