Compare commits

..

4 Commits

Author SHA1 Message Date
52c49c7238 no idea? 2025-11-30 06:39:07 -05:00
140b75621f running import from csv off Avant. Needs some clean up on category matching. 2025-11-30 06:38:51 -05:00
f539c64588 small changes. still working 2025-11-30 05:40:59 -05:00
87b3c4bff8 slug handling changes 2025-11-30 05:22:08 -05:00
3 changed files with 301 additions and 61 deletions

View File

@@ -25,7 +25,7 @@
</developer>
<developer>
<name>Sean Strawsburg</name>
<email>don@goforward.group</email>
<email>sean@goforward.group</email>
<organization>Forward Group, LLC</organization>
</developer>
</developers>
@@ -53,7 +53,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
@@ -87,6 +86,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.11.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -1,5 +1,18 @@
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;
import group.goforward.ballistic.model.Product;
@@ -9,7 +22,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,76 +45,297 @@ 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.
Brand brand = brandRepository.findByNameIgnoreCase("Aero Precision")
.orElseThrow(() -> new IllegalStateException("Brand 'Aero Precision' not found"));
// Read all rows from the merchant feed
List<MerchantFeedRow> rows = readFeedRowsForMerchant(merchant);
System.out.println("IMPORT >>> read " + rows.size() + " rows for merchant=" + merchant.getName());
// Fake a single row well swap this for real CSV parsing once the plumbing works
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
);
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 = createProduct(brand, row);
System.out.println("IMPORT >>> created product id=" + p.getId()
+ ", name=" + p.getName()
+ ", 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());
}
}
private Product createProduct(Brand brand, MerchantFeedRow row) {
System.out.println("IMPORT >>> createProduct brand=" + brand.getName()
// ---------------------------------------------------------------------
// 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());
Product p = new Product();
p.setBrand(brand);
String mpn = trimOrNull(row.manufacturerId());
String upc = trimOrNull(row.sku()); // placeholder until real UPC field
String name = row.productName();
if (name == null || name.isBlank()) {
name = row.sku();
List<Product> candidates = Collections.emptyList();
if (mpn != null) {
candidates = productRepository.findAllByBrandAndMpn(brand, mpn);
}
if (name == null || name.isBlank()) {
name = "Unknown Product";
if ((candidates == null || candidates.isEmpty()) && upc != null) {
candidates = productRepository.findAllByBrandAndUpc(brand, upc);
}
// Set required fields: name and slug
p.setName(name);
Product p;
boolean isNew = (candidates == null || candidates.isEmpty());
// 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()) {
baseForSlug = "product-" + System.currentTimeMillis();
if (isNew) {
p = new Product();
p.setBrand(brand);
} else {
if (candidates.size() > 1) {
System.out.println("IMPORT !!! WARNING: multiple existing products found for brand="
+ brand.getName() + ", mpn=" + mpn + ", upc=" + upc
+ ". Using the first match (id=" + candidates.get(0).getId() + ")");
}
p = candidates.get(0);
}
String slug = baseForSlug
.toLowerCase()
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("(^-|-$)", "");
if (slug.isBlank()) {
slug = "product-" + System.currentTimeMillis();
}
p.setSlug(slug);
if (p.getPlatform() == null || p.getPlatform().isBlank()) {
p.setPlatform("AR-15");
}
if (p.getPartRole() == null || p.getPartRole().isBlank()) {
p.setPartRole("unknown");
}
updateProductFromRow(p, row, isNew);
return productRepository.save(p);
}
private void updateProductFromRow(Product p, MerchantFeedRow row, boolean isNew) {
// ---------- NAME ----------
String name = coalesce(
trimOrNull(row.productName()),
trimOrNull(row.shortDescription()),
trimOrNull(row.longDescription()),
trimOrNull(row.sku())
);
if (name == null) {
name = "Unknown Product";
}
p.setName(name);
// ---------- SLUG ----------
if (isNew || p.getSlug() == null || p.getSlug().isBlank()) {
String baseForSlug = coalesce(
trimOrNull(name),
trimOrNull(row.sku())
);
if (baseForSlug == null) {
baseForSlug = "product-" + System.currentTimeMillis();
}
String slug = baseForSlug
.toLowerCase()
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("(^-|-$)", "");
if (slug.isBlank()) {
slug = "product-" + System.currentTimeMillis();
}
String uniqueSlug = generateUniqueSlug(slug);
p.setSlug(uniqueSlug);
}
// ---------- 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 ----------
String mpn = coalesce(
trimOrNull(row.manufacturerId()),
trimOrNull(row.sku())
);
p.setMpn(mpn);
// UPC placeholder
p.setUpc(null);
// ---------- PLATFORM ----------
String platform = inferPlatform(row);
p.setPlatform(platform != null ? platform : "AR-15");
// ---------- PART ROLE ----------
String partRole = inferPartRole(row);
if (partRole == null || partRole.isBlank()) {
partRole = "unknown";
}
p.setPartRole(partRole);
}
// ---------------------------------------------------------------------
// Feed reading + brand resolution
// ---------------------------------------------------------------------
private List<MerchantFeedRow> 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<MerchantFeedRow> 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;
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";
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("magazine") || lower.contains("mag")) {
return "magazine";
}
if (lower.contains("stock") || lower.contains("buttstock")) {
return "stock";
}
if (lower.contains("grip")) {
return "grip";
}
return "unknown";
}
}

View File

@@ -6,12 +6,15 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Integer> {
Optional<Product> findByUuid(UUID uuid);
Optional<Product> findByBrandAndMpn(Brand brand, String mpn);
boolean existsBySlug(String slug);
Optional<Product> findByBrandAndUpc(Brand brand, String upc);
List<Product> findAllByBrandAndMpn(Brand brand, String mpn);
List<Product> findAllByBrandAndUpc(Brand brand, String upc);
}