diff --git a/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java b/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java index 622a87a..7b9a4f4 100644 --- a/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java +++ b/src/main/java/group/goforward/ballistic/imports/MerchantFeedImportServiceImpl.java @@ -1,6 +1,17 @@ package group.goforward.ballistic.imports; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.io.Reader; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; import group.goforward.ballistic.model.Brand; import group.goforward.ballistic.model.Merchant; @@ -34,66 +45,37 @@ 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 (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")); + // Read all rows from the merchant feed + List rows = readFeedRowsForMerchant(merchant); + System.out.println("IMPORT >>> read " + rows.size() + " rows for merchant=" + merchant.getName()); - // TODO: replace this with real feed parsing: - // List rows = feedClient.fetch(merchant); - // rows.forEach(row -> upsertProduct(merchant, brand, row)); - MerchantFeedRow row = new MerchantFeedRow( - "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 - ); + for (MerchantFeedRow row : rows) { + // Resolve brand from the row (fallback to "Aero Precision" or whatever you want as default) + Brand brand = resolveBrand(row); + Product p = upsertProduct(merchant, brand, row); - Product p = upsertProduct(merchant, brand, row); - - System.out.println("IMPORT >>> upserted product id=" + p.getId() - + ", name=" + p.getName() - + ", slug=" + p.getSlug() - + ", platform=" + p.getPlatform() - + ", partRole=" + p.getPartRole() - + ", merchant=" + merchant.getName()); + System.out.println("IMPORT >>> upserted product id=" + p.getId() + + ", name=" + p.getName() + + ", slug=" + p.getSlug() + + ", platform=" + p.getPlatform() + + ", partRole=" + p.getPartRole() + + ", merchant=" + merchant.getName()); + } } - /** - * Upsert logic: - * - Try Brand+MPN, then Brand+UPC (for now using sku as a stand-in) - * - If found, update fields but keep existing slug - * - If not found, create a new Product and generate a unique slug - */ + // --------------------------------------------------------------------- + // Upsert logic + // --------------------------------------------------------------------- + private Product upsertProduct(Merchant merchant, Brand brand, MerchantFeedRow row) { System.out.println("IMPORT >>> upsertProduct brand=" + brand.getName() + ", sku=" + row.sku() + ", productName=" + row.productName()); String mpn = trimOrNull(row.manufacturerId()); - String upc = trimOrNull(row.sku()); // later: real UPC column + String upc = trimOrNull(row.sku()); // placeholder until real UPC field - java.util.List candidates = java.util.Collections.emptyList(); + List candidates = Collections.emptyList(); if (mpn != null) { candidates = productRepository.findAllByBrandAndMpn(brand, mpn); @@ -118,16 +100,10 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService } updateProductFromRow(p, row, isNew); - return productRepository.save(p); } - /** - * Shared mapping logic from feed row -> Product entity. - * If isNew = true, we generate a slug. Otherwise we leave the slug alone. - */ private void updateProductFromRow(Product p, MerchantFeedRow row, boolean isNew) { - // ---------- NAME ---------- String name = coalesce( trimOrNull(row.productName()), @@ -158,7 +134,6 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService slug = "product-" + System.currentTimeMillis(); } - // Ensure slug is unique by appending a numeric suffix if needed String uniqueSlug = generateUniqueSlug(slug); p.setSlug(uniqueSlug); } @@ -176,15 +151,13 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService 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 (or map later). + // UPC placeholder p.setUpc(null); // ---------- PLATFORM ---------- @@ -199,7 +172,100 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService p.setPartRole(partRole); } - // --- Helpers ---------------------------------------------------------- + // --------------------------------------------------------------------- + // Feed reading + brand resolution + // --------------------------------------------------------------------- + + private List readFeedRowsForMerchant(Merchant merchant) { + String rawFeedUrl = merchant.getFeedUrl(); + if (rawFeedUrl == null || rawFeedUrl.isBlank()) { + throw new IllegalStateException("Merchant " + merchant.getName() + " has no feed_url configured"); + } + + String feedUrl = rawFeedUrl.trim(); + System.out.println("IMPORT >>> reading feed for merchant=" + merchant.getName() + " from: " + feedUrl); + + List rows = new ArrayList<>(); + + try (Reader reader = (feedUrl.startsWith("http://") || feedUrl.startsWith("https://")) + ? new InputStreamReader(new URL(feedUrl).openStream(), StandardCharsets.UTF_8) + : java.nio.file.Files.newBufferedReader(java.nio.file.Paths.get(feedUrl), StandardCharsets.UTF_8); + CSVParser parser = CSVFormat.DEFAULT + .withFirstRecordAsHeader() + .withIgnoreSurroundingSpaces() + .withTrim() + .parse(reader)) { + + for (CSVRecord rec : parser) { + MerchantFeedRow row = new MerchantFeedRow( + rec.get("SKU"), + rec.get("Manufacturer Id"), + rec.get("Brand Name"), + rec.get("Product Name"), + rec.get("Long Description"), + rec.get("Short Description"), + rec.get("Department"), + rec.get("Category"), + rec.get("SubCategory"), + rec.get("Thumb URL"), + rec.get("Image URL"), + rec.get("Buy Link"), + rec.get("Keywords"), + rec.get("Reviews"), + parseBigDecimal(rec.get("Retail Price")), + parseBigDecimal(rec.get("Sale Price")), + rec.get("Brand Page Link"), + rec.get("Brand Logo Image"), + rec.get("Product Page View Tracking"), + rec.get("Variants XML"), + rec.get("Medium Image URL"), + rec.get("Product Content Widget"), + rec.get("Google Categorization"), + rec.get("Item Based Commission") + ); + + rows.add(row); + } + } catch (Exception ex) { + throw new RuntimeException("Failed to read feed for merchant " + + merchant.getName() + " from " + feedUrl, ex); + } + + System.out.println("IMPORT >>> parsed " + rows.size() + " rows for merchant=" + merchant.getName()); + return rows; + } + + private Brand resolveBrand(MerchantFeedRow row) { + String rawBrand = trimOrNull(row.brandName()); + final String brandName = (rawBrand != null) ? rawBrand : "Aero Precision"; + + return brandRepository.findByNameIgnoreCase(brandName) + .orElseGet(() -> { + Brand b = new Brand(); + b.setName(brandName); + return brandRepository.save(b); + }); + } + + private String getCol(String[] cols, int index) { + return (index >= 0 && index < cols.length) ? cols[index] : null; + } + + private BigDecimal parseBigDecimal(String raw) { + if (raw == null) return null; + String trimmed = raw.trim(); + if (trimmed.isEmpty()) return null; + try { + return new BigDecimal(trimmed); + } catch (NumberFormatException ex) { + System.out.println("IMPORT !!! bad BigDecimal value: '" + raw + "', skipping"); + return null; + } + } + + // --------------------------------------------------------------------- + // Misc helpers + // --------------------------------------------------------------------- private String trimOrNull(String value) { if (value == null) return null; @@ -236,7 +302,6 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService 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"; } @@ -261,6 +326,9 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService 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"; }