From 87b3c4bff87eed664b4058701bf3ceb647563faf Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 30 Nov 2025 05:18:07 -0500 Subject: [PATCH] slug handling changes --- .../MerchantFeedImportServiceImpl.java | 197 ++++++++++++++---- .../ballistic/repos/ProductRepository.java | 3 + 2 files changed, 165 insertions(+), 35 deletions(-) diff --git a/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java b/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java index 97a8622..a4643d0 100644 --- a/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java +++ b/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java @@ -1,4 +1,6 @@ package group.goforward.ballistic.imports; +import java.math.BigDecimal; +import java.util.Optional; import group.goforward.ballistic.model.Brand; import group.goforward.ballistic.model.Merchant; @@ -9,7 +11,6 @@ import group.goforward.ballistic.repos.ProductRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - @Service @Transactional public class MerchantFeedImportServiceImpl implements MerchantFeedImportService { @@ -33,25 +34,48 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService Merchant merchant = merchantRepository.findById(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") - .orElseThrow(() -> new IllegalStateException("Brand 'Aero Precision' not found")); + .orElseThrow(() -> new IllegalStateException("Brand 'Aero Precision' not found")); - // Fake a single row – we’ll swap this for real CSV parsing once the plumbing works + // TODO: replace this with real feed parsing: + // List rows = feedClient.fetch(merchant); + // rows.forEach(row -> upsertProduct(merchant, row)); MerchantFeedRow row = new MerchantFeedRow( - "TEST-SKU-001", - "APPG100002", - brand.getName(), - "Test Product From Import", - null, null, null, null, null, - null, null, null, null, null, - null, null, - null, null, null, null, null, null, null, null - ); + "TEST-SKU-001", + "APPG100002", + brand.getName(), + "Test Product From Import", + "This is a long description from AvantLink.", + "Short description from AvantLink.", + "Rifles", + "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); + System.out.println("IMPORT >>> created product id=" + p.getId() + ", name=" + p.getName() + + ", slug=" + p.getSlug() + + ", platform=" + p.getPlatform() + + ", partRole=" + p.getPartRole() + ", merchant=" + merchant.getName()); } @@ -63,23 +87,24 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService Product p = new Product(); p.setBrand(brand); - String name = row.productName(); - if (name == null || name.isBlank()) { - name = row.sku(); - } - if (name == null || name.isBlank()) { + // ---------- NAME ---------- + String name = coalesce( + trimOrNull(row.productName()), + trimOrNull(row.shortDescription()), + trimOrNull(row.longDescription()), + trimOrNull(row.sku()) + ); + if (name == null) { name = "Unknown Product"; } - - // Set required fields: name and slug p.setName(name); - // Generate a simple slug from the name (fallback to SKU if needed) - String baseForSlug = name; - if (baseForSlug == null || baseForSlug.isBlank()) { - baseForSlug = row.sku(); - } - if (baseForSlug == null || baseForSlug.isBlank()) { + // ---------- SLUG ---------- + String baseForSlug = coalesce( + trimOrNull(name), + trimOrNull(row.sku()) + ); + if (baseForSlug == null) { baseForSlug = "product-" + System.currentTimeMillis(); } @@ -87,22 +112,124 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService .toLowerCase() .replaceAll("[^a-z0-9]+", "-") .replaceAll("(^-|-$)", ""); - if (slug.isBlank()) { 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()) { - p.setPlatform("AR-15"); - } - - if (p.getPartRole() == null || p.getPartRole().isBlank()) { - p.setPartRole("unknown"); - } + // ---------- DESCRIPTIONS ---------- + 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 doesn’t give us UPC in the header you showed. + // We’ll 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); 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"; + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/repos/ProductRepository.java b/src/main/java/group/goforward/ballistic/repos/ProductRepository.java index b91bed1..f438159 100644 --- a/src/main/java/group/goforward/ballistic/repos/ProductRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/ProductRepository.java @@ -14,4 +14,7 @@ public interface ProductRepository extends JpaRepository { Optional findByBrandAndMpn(Brand brand, String mpn); Optional findByBrandAndUpc(Brand brand, String upc); + + boolean existsBySlug(String slug); + } \ No newline at end of file