slug handling changes

This commit is contained in:
2025-11-30 05:18:07 -05:00
parent 4c0a3bd12d
commit 87b3c4bff8
2 changed files with 165 additions and 35 deletions

View File

@@ -1,4 +1,6 @@
package group.goforward.ballistic.imports; package group.goforward.ballistic.imports;
import java.math.BigDecimal;
import java.util.Optional;
import group.goforward.ballistic.model.Brand; import group.goforward.ballistic.model.Brand;
import group.goforward.ballistic.model.Merchant; import group.goforward.ballistic.model.Merchant;
@@ -9,7 +11,6 @@ import group.goforward.ballistic.repos.ProductRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@Transactional @Transactional
public class MerchantFeedImportServiceImpl implements MerchantFeedImportService { public class MerchantFeedImportServiceImpl implements MerchantFeedImportService {
@@ -33,25 +34,48 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
Merchant merchant = merchantRepository.findById(merchantId) Merchant merchant = merchantRepository.findById(merchantId)
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + merchantId)); .orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + merchantId));
// For now, just pick a brand to prove inserts work. // For now, just pick a brand to prove inserts work (Aero Precision for merchant 4).
// Later we can switch to row.brandName() + auto-create brands.
Brand brand = brandRepository.findByNameIgnoreCase("Aero Precision") Brand brand = brandRepository.findByNameIgnoreCase("Aero Precision")
.orElseThrow(() -> new IllegalStateException("Brand 'Aero Precision' not found")); .orElseThrow(() -> new IllegalStateException("Brand 'Aero Precision' not found"));
// Fake a single row well swap this for real CSV parsing once the plumbing works // TODO: replace this with real feed parsing:
// List<MerchantFeedRow> rows = feedClient.fetch(merchant);
// rows.forEach(row -> upsertProduct(merchant, row));
MerchantFeedRow row = new MerchantFeedRow( MerchantFeedRow row = new MerchantFeedRow(
"TEST-SKU-001", "TEST-SKU-001",
"APPG100002", "APPG100002",
brand.getName(), brand.getName(),
"Test Product From Import", "Test Product From Import",
null, null, null, null, null, "This is a long description from AvantLink.",
null, null, null, null, null, "Short description from AvantLink.",
null, null, "Rifles",
null, null, null, null, null, null, null, null "AR-15 Parts",
); "Handguards & Rails",
"https://example.com/thumb.jpg",
"https://example.com/image.jpg",
"https://example.com/buy-link",
"ar-15, handguard, aero",
null,
new BigDecimal("199.99"), // retailPrice
new BigDecimal("149.99"), // salePrice
null,
null,
null,
null,
"https://example.com/medium.jpg",
null,
null,
null
);
Product p = createProduct(brand, row); Product p = createProduct(brand, row);
System.out.println("IMPORT >>> created product id=" + p.getId() System.out.println("IMPORT >>> created product id=" + p.getId()
+ ", name=" + p.getName() + ", name=" + p.getName()
+ ", slug=" + p.getSlug()
+ ", platform=" + p.getPlatform()
+ ", partRole=" + p.getPartRole()
+ ", merchant=" + merchant.getName()); + ", merchant=" + merchant.getName());
} }
@@ -63,23 +87,24 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
Product p = new Product(); Product p = new Product();
p.setBrand(brand); p.setBrand(brand);
String name = row.productName(); // ---------- NAME ----------
if (name == null || name.isBlank()) { String name = coalesce(
name = row.sku(); trimOrNull(row.productName()),
} trimOrNull(row.shortDescription()),
if (name == null || name.isBlank()) { trimOrNull(row.longDescription()),
trimOrNull(row.sku())
);
if (name == null) {
name = "Unknown Product"; name = "Unknown Product";
} }
// Set required fields: name and slug
p.setName(name); p.setName(name);
// Generate a simple slug from the name (fallback to SKU if needed) // ---------- SLUG ----------
String baseForSlug = name; String baseForSlug = coalesce(
if (baseForSlug == null || baseForSlug.isBlank()) { trimOrNull(name),
baseForSlug = row.sku(); trimOrNull(row.sku())
} );
if (baseForSlug == null || baseForSlug.isBlank()) { if (baseForSlug == null) {
baseForSlug = "product-" + System.currentTimeMillis(); baseForSlug = "product-" + System.currentTimeMillis();
} }
@@ -87,22 +112,124 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
.toLowerCase() .toLowerCase()
.replaceAll("[^a-z0-9]+", "-") .replaceAll("[^a-z0-9]+", "-")
.replaceAll("(^-|-$)", ""); .replaceAll("(^-|-$)", "");
if (slug.isBlank()) { if (slug.isBlank()) {
slug = "product-" + System.currentTimeMillis(); slug = "product-" + System.currentTimeMillis();
} }
p.setSlug(slug); // Ensure slug is unique by appending a numeric suffix if needed
String uniqueSlug = generateUniqueSlug(slug);
p.setSlug(uniqueSlug);
if (p.getPlatform() == null || p.getPlatform().isBlank()) { // ---------- DESCRIPTIONS ----------
p.setPlatform("AR-15"); p.setShortDescription(trimOrNull(row.shortDescription()));
p.setDescription(trimOrNull(row.longDescription()));
// ---------- IMAGE ----------
String mainImage = coalesce(
trimOrNull(row.imageUrl()),
trimOrNull(row.mediumImageUrl()),
trimOrNull(row.thumbUrl())
);
p.setMainImageUrl(mainImage);
// ---------- IDENTIFIERS ----------
// AvantLink "Manufacturer Id" is a good fit for MPN.
String mpn = coalesce(
trimOrNull(row.manufacturerId()),
trimOrNull(row.sku())
);
p.setMpn(mpn);
// Feed doesnt give us UPC in the header you showed.
// Well leave UPC null for now.
p.setUpc(null);
// ---------- PLATFORM ----------
// For now, hard-code to AR-15 to satisfy not-null constraint.
// Later we can infer from row.category()/row.department().
String platform = inferPlatform(row);
p.setPlatform(platform != null ? platform : "AR-15");
// ---------- PART ROLE ----------
// We can do a tiny heuristic off category/subcategory.
String partRole = inferPartRole(row);
if (partRole == null || partRole.isBlank()) {
partRole = "unknown";
} }
p.setPartRole(partRole);
if (p.getPartRole() == null || p.getPartRole().isBlank()) {
p.setPartRole("unknown");
}
return productRepository.save(p); return productRepository.save(p);
} }
// --- Helpers ----------------------------------------------------------
private String trimOrNull(String value) {
if (value == null) return null;
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String coalesce(String... values) {
if (values == null) return null;
for (String v : values) {
if (v != null && !v.isBlank()) {
return v;
}
}
return null;
}
private String generateUniqueSlug(String baseSlug) {
String candidate = baseSlug;
int suffix = 1;
while (productRepository.existsBySlug(candidate)) {
candidate = baseSlug + "-" + suffix;
suffix++;
}
return candidate;
}
private String inferPlatform(MerchantFeedRow row) {
String department = coalesce(trimOrNull(row.department()), trimOrNull(row.category()));
if (department == null) return null;
String lower = department.toLowerCase();
if (lower.contains("ar-15") || lower.contains("ar15")) return "AR-15";
if (lower.contains("ar-10") || lower.contains("ar10")) return "AR-10";
if (lower.contains("ak-47") || lower.contains("ak47")) return "AK-47";
// Default: treat Aero as AR-15 universe for now
return "AR-15";
}
private String inferPartRole(MerchantFeedRow row) {
String cat = coalesce(
trimOrNull(row.subCategory()),
trimOrNull(row.category())
);
if (cat == null) return null;
String lower = cat.toLowerCase();
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("stock") || lower.contains("buttstock")) {
return "stock";
}
if (lower.contains("grip")) {
return "grip";
}
return "unknown";
}
} }

View File

@@ -14,4 +14,7 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
Optional<Product> findByBrandAndMpn(Brand brand, String mpn); Optional<Product> findByBrandAndMpn(Brand brand, String mpn);
Optional<Product> findByBrandAndUpc(Brand brand, String upc); Optional<Product> findByBrandAndUpc(Brand brand, String upc);
boolean existsBySlug(String slug);
} }