This commit is contained in:
2025-11-30 06:39:07 -05:00
parent 140b75621f
commit 52c49c7238

View File

@@ -1,6 +1,17 @@
package group.goforward.ballistic.imports; package group.goforward.ballistic.imports;
import java.math.BigDecimal; 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.Brand;
import group.goforward.ballistic.model.Merchant; import group.goforward.ballistic.model.Merchant;
@@ -34,66 +45,37 @@ 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 (Aero Precision for merchant 4). // Read all rows from the merchant feed
// Later we can switch to row.brandName() + auto-create brands. List<MerchantFeedRow> rows = readFeedRowsForMerchant(merchant);
Brand brand = brandRepository.findByNameIgnoreCase("Aero Precision") System.out.println("IMPORT >>> read " + rows.size() + " rows for merchant=" + merchant.getName());
.orElseThrow(() -> new IllegalStateException("Brand 'Aero Precision' not found"));
// TODO: replace this with real feed parsing: for (MerchantFeedRow row : rows) {
// List<MerchantFeedRow> rows = feedClient.fetch(merchant); // Resolve brand from the row (fallback to "Aero Precision" or whatever you want as default)
// rows.forEach(row -> upsertProduct(merchant, brand, row)); Brand brand = resolveBrand(row);
MerchantFeedRow row = new MerchantFeedRow( Product p = upsertProduct(merchant, brand, row);
"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 = upsertProduct(merchant, brand, row); System.out.println("IMPORT >>> upserted product id=" + p.getId()
+ ", name=" + p.getName()
System.out.println("IMPORT >>> upserted product id=" + p.getId() + ", slug=" + p.getSlug()
+ ", name=" + p.getName() + ", platform=" + p.getPlatform()
+ ", slug=" + p.getSlug() + ", partRole=" + p.getPartRole()
+ ", platform=" + p.getPlatform() + ", merchant=" + merchant.getName());
+ ", partRole=" + p.getPartRole() }
+ ", merchant=" + merchant.getName());
} }
/** // ---------------------------------------------------------------------
* Upsert logic: // 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
*/
private Product upsertProduct(Merchant merchant, Brand brand, MerchantFeedRow row) { private Product upsertProduct(Merchant merchant, Brand brand, MerchantFeedRow row) {
System.out.println("IMPORT >>> upsertProduct brand=" + brand.getName() System.out.println("IMPORT >>> upsertProduct brand=" + brand.getName()
+ ", sku=" + row.sku() + ", sku=" + row.sku()
+ ", productName=" + row.productName()); + ", productName=" + row.productName());
String mpn = trimOrNull(row.manufacturerId()); 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<Product> candidates = java.util.Collections.emptyList(); List<Product> candidates = Collections.emptyList();
if (mpn != null) { if (mpn != null) {
candidates = productRepository.findAllByBrandAndMpn(brand, mpn); candidates = productRepository.findAllByBrandAndMpn(brand, mpn);
@@ -118,16 +100,10 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
} }
updateProductFromRow(p, row, isNew); updateProductFromRow(p, row, isNew);
return productRepository.save(p); 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) { private void updateProductFromRow(Product p, MerchantFeedRow row, boolean isNew) {
// ---------- NAME ---------- // ---------- NAME ----------
String name = coalesce( String name = coalesce(
trimOrNull(row.productName()), trimOrNull(row.productName()),
@@ -158,7 +134,6 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
slug = "product-" + System.currentTimeMillis(); slug = "product-" + System.currentTimeMillis();
} }
// Ensure slug is unique by appending a numeric suffix if needed
String uniqueSlug = generateUniqueSlug(slug); String uniqueSlug = generateUniqueSlug(slug);
p.setSlug(uniqueSlug); p.setSlug(uniqueSlug);
} }
@@ -176,15 +151,13 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
p.setMainImageUrl(mainImage); p.setMainImageUrl(mainImage);
// ---------- IDENTIFIERS ---------- // ---------- IDENTIFIERS ----------
// AvantLink "Manufacturer Id" is a good fit for MPN.
String mpn = coalesce( String mpn = coalesce(
trimOrNull(row.manufacturerId()), trimOrNull(row.manufacturerId()),
trimOrNull(row.sku()) trimOrNull(row.sku())
); );
p.setMpn(mpn); p.setMpn(mpn);
// Feed doesnt give us UPC in the header you showed. // UPC placeholder
// Well leave UPC null for now (or map later).
p.setUpc(null); p.setUpc(null);
// ---------- PLATFORM ---------- // ---------- PLATFORM ----------
@@ -199,7 +172,100 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
p.setPartRole(partRole); p.setPartRole(partRole);
} }
// --- Helpers ---------------------------------------------------------- // ---------------------------------------------------------------------
// 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) { private String trimOrNull(String value) {
if (value == null) return null; 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("ar-10") || lower.contains("ar10")) return "AR-10";
if (lower.contains("ak-47") || lower.contains("ak47")) return "AK-47"; if (lower.contains("ak-47") || lower.contains("ak47")) return "AK-47";
// Default: treat Aero as AR-15 universe for now
return "AR-15"; return "AR-15";
} }
@@ -261,6 +326,9 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
if (lower.contains("lower")) { if (lower.contains("lower")) {
return "lower-receiver"; return "lower-receiver";
} }
if (lower.contains("magazine") || lower.contains("mag")) {
return "magazine";
}
if (lower.contains("stock") || lower.contains("buttstock")) { if (lower.contains("stock") || lower.contains("buttstock")) {
return "stock"; return "stock";
} }