mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-06 02:56:44 -05:00
Compare commits
4 Commits
4c0a3bd12d
...
52c49c7238
| Author | SHA1 | Date | |
|---|---|---|---|
| 52c49c7238 | |||
| 140b75621f | |||
| f539c64588 | |||
| 87b3c4bff8 |
8
pom.xml
8
pom.xml
@@ -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>
|
||||
|
||||
@@ -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 – we’ll 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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user