mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-05 18:46:44 -05:00
added java caching and optimized controller queries
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Ballistic Backend
|
# Ballistic Builder ( The Armory?) Backend
|
||||||
### Internal Engine for the Builder Ecosystem
|
### Internal Engine for the Shadow System Armory?
|
||||||
|
|
||||||
The Ballistic Backend is the operational backbone behind the Shadown System Armory and its admin tools. It ingests merchant feeds, normalizes product data, manages categories, synchronizes prices, and powers the compatibility, pricing, and product logic behind the consumer-facing Builder.
|
The Ballistic Backend is the operational backbone behind the Shadown System Armory and its admin tools. It ingests merchant feeds, normalizes product data, manages categories, synchronizes prices, and powers the compatibility, pricing, and product logic behind the consumer-facing Builder.
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableCaching
|
||||||
@ComponentScan("group.goforward.ballistic.controllers")
|
@ComponentScan("group.goforward.ballistic.controllers")
|
||||||
@ComponentScan("group.goforward.ballistic.repos")
|
@ComponentScan("group.goforward.ballistic.repos")
|
||||||
@ComponentScan("group.goforward.ballistic.services")
|
@ComponentScan("group.goforward.ballistic.services")
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package group.goforward.ballistic.configuration;
|
||||||
|
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CacheConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CacheManager cacheManager() {
|
||||||
|
// Simple in-memory cache for dev/local
|
||||||
|
return new ConcurrentMapCacheManager("gunbuilderProducts");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import group.goforward.ballistic.web.dto.ProductOfferDto;
|
|||||||
import group.goforward.ballistic.repos.ProductRepository;
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
import group.goforward.ballistic.web.dto.ProductSummaryDto;
|
import group.goforward.ballistic.web.dto.ProductSummaryDto;
|
||||||
import group.goforward.ballistic.web.mapper.ProductMapper;
|
import group.goforward.ballistic.web.mapper.ProductMapper;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -30,35 +31,54 @@ public class ProductController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/gunbuilder")
|
@GetMapping("/gunbuilder")
|
||||||
|
@Cacheable(
|
||||||
|
value = "gunbuilderProducts",
|
||||||
|
key = "#platform + '::' + (#partRoles == null ? 'ALL' : #partRoles.toString())"
|
||||||
|
)
|
||||||
public List<ProductSummaryDto> getGunbuilderProducts(
|
public List<ProductSummaryDto> getGunbuilderProducts(
|
||||||
@RequestParam(defaultValue = "AR-15") String platform,
|
@RequestParam(defaultValue = "AR-15") String platform,
|
||||||
@RequestParam(required = false, name = "partRoles") List<String> partRoles
|
@RequestParam(required = false, name = "partRoles") List<String> partRoles
|
||||||
) {
|
) {
|
||||||
// 1) Load products
|
long started = System.currentTimeMillis();
|
||||||
|
System.out.println("getGunbuilderProducts: start, platform=" + platform +
|
||||||
|
", partRoles=" + (partRoles == null ? "null" : partRoles));
|
||||||
|
|
||||||
|
// 1) Load products (with brand pre-fetched)
|
||||||
|
long tProductsStart = System.currentTimeMillis();
|
||||||
List<Product> products;
|
List<Product> products;
|
||||||
if (partRoles == null || partRoles.isEmpty()) {
|
if (partRoles == null || partRoles.isEmpty()) {
|
||||||
products = productRepository.findByPlatform(platform);
|
products = productRepository.findByPlatformWithBrand(platform);
|
||||||
} else {
|
} else {
|
||||||
products = productRepository.findByPlatformAndPartRoleIn(platform, partRoles);
|
products = productRepository.findByPlatformAndPartRoleInWithBrand(platform, partRoles);
|
||||||
}
|
}
|
||||||
|
long tProductsEnd = System.currentTimeMillis();
|
||||||
|
System.out.println("getGunbuilderProducts: loaded products: " +
|
||||||
|
products.size() + " in " + (tProductsEnd - tProductsStart) + " ms");
|
||||||
|
|
||||||
if (products.isEmpty()) {
|
if (products.isEmpty()) {
|
||||||
|
long took = System.currentTimeMillis() - started;
|
||||||
|
System.out.println("getGunbuilderProducts: 0 products in " + took + " ms");
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Load offers for these product IDs (Integer IDs)
|
// 2) Load offers for these product IDs
|
||||||
|
long tOffersStart = System.currentTimeMillis();
|
||||||
List<Integer> productIds = products.stream()
|
List<Integer> productIds = products.stream()
|
||||||
.map(Product::getId)
|
.map(Product::getId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<ProductOffer> allOffers =
|
List<ProductOffer> allOffers =
|
||||||
productOfferRepository.findByProductIdIn(productIds);
|
productOfferRepository.findByProductIdIn(productIds);
|
||||||
|
long tOffersEnd = System.currentTimeMillis();
|
||||||
|
System.out.println("getGunbuilderProducts: loaded offers: " +
|
||||||
|
allOffers.size() + " in " + (tOffersEnd - tOffersStart) + " ms");
|
||||||
|
|
||||||
Map<Integer, List<ProductOffer>> offersByProductId = allOffers.stream()
|
Map<Integer, List<ProductOffer>> offersByProductId = allOffers.stream()
|
||||||
.collect(Collectors.groupingBy(o -> o.getProduct().getId()));
|
.collect(Collectors.groupingBy(o -> o.getProduct().getId()));
|
||||||
|
|
||||||
// 3) Map to DTOs with price and buyUrl
|
// 3) Map to DTOs with price and buyUrl
|
||||||
return products.stream()
|
long tMapStart = System.currentTimeMillis();
|
||||||
|
List<ProductSummaryDto> result = products.stream()
|
||||||
.map(p -> {
|
.map(p -> {
|
||||||
List<ProductOffer> offersForProduct =
|
List<ProductOffer> offersForProduct =
|
||||||
offersByProductId.getOrDefault(p.getId(), Collections.emptyList());
|
offersByProductId.getOrDefault(p.getId(), Collections.emptyList());
|
||||||
@@ -71,6 +91,17 @@ public class ProductController {
|
|||||||
return ProductMapper.toSummary(p, price, buyUrl);
|
return ProductMapper.toSummary(p, price, buyUrl);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
long tMapEnd = System.currentTimeMillis();
|
||||||
|
long took = System.currentTimeMillis() - started;
|
||||||
|
|
||||||
|
System.out.println("getGunbuilderProducts: mapping to DTOs took " +
|
||||||
|
(tMapEnd - tMapStart) + " ms");
|
||||||
|
System.out.println("getGunbuilderProducts: TOTAL " + took + " ms (" +
|
||||||
|
"products=" + (tProductsEnd - tProductsStart) + " ms, " +
|
||||||
|
"offers=" + (tOffersEnd - tOffersStart) + " ms, " +
|
||||||
|
"map=" + (tMapEnd - tMapStart) + " ms)");
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/offers")
|
@GetMapping("/{id}/offers")
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package group.goforward.ballistic.repos;
|
|||||||
import group.goforward.ballistic.model.Product;
|
import group.goforward.ballistic.model.Product;
|
||||||
import group.goforward.ballistic.model.Brand;
|
import group.goforward.ballistic.model.Brand;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,4 +26,28 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
|
|||||||
|
|
||||||
// Products filtered by platform + part roles (e.g. upper-receiver, barrel, etc.)
|
// Products filtered by platform + part roles (e.g. upper-receiver, barrel, etc.)
|
||||||
List<Product> findByPlatformAndPartRoleIn(String platform, Collection<String> partRoles);
|
List<Product> findByPlatformAndPartRoleIn(String platform, Collection<String> partRoles);
|
||||||
|
|
||||||
|
// ---------- Optimized variants for Gunbuilder (fetch brand to avoid N+1) ----------
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT p
|
||||||
|
FROM Product p
|
||||||
|
JOIN FETCH p.brand b
|
||||||
|
WHERE p.platform = :platform
|
||||||
|
AND p.deletedAt IS NULL
|
||||||
|
""")
|
||||||
|
List<Product> findByPlatformWithBrand(@Param("platform") String platform);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT p
|
||||||
|
FROM Product p
|
||||||
|
JOIN FETCH p.brand b
|
||||||
|
WHERE p.platform = :platform
|
||||||
|
AND p.partRole IN :partRoles
|
||||||
|
AND p.deletedAt IS NULL
|
||||||
|
""")
|
||||||
|
List<Product> findByPlatformAndPartRoleInWithBrand(
|
||||||
|
@Param("platform") String platform,
|
||||||
|
@Param("partRoles") Collection<String> partRoles
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,15 @@ import java.io.BufferedReader;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
|
||||||
import group.goforward.ballistic.imports.MerchantFeedRow;
|
import group.goforward.ballistic.imports.MerchantFeedRow;
|
||||||
import group.goforward.ballistic.services.MerchantFeedImportService;
|
import group.goforward.ballistic.services.MerchantFeedImportService;
|
||||||
import org.apache.commons.csv.CSVFormat;
|
import org.apache.commons.csv.CSVFormat;
|
||||||
import org.apache.commons.csv.CSVParser;
|
import org.apache.commons.csv.CSVParser;
|
||||||
import org.apache.commons.csv.CSVRecord;
|
import org.apache.commons.csv.CSVRecord;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.Brand;
|
import group.goforward.ballistic.model.Brand;
|
||||||
import group.goforward.ballistic.model.Merchant;
|
import group.goforward.ballistic.model.Merchant;
|
||||||
@@ -36,6 +39,7 @@ import java.time.OffsetDateTime;
|
|||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
public class MerchantFeedImportServiceImpl implements MerchantFeedImportService {
|
public class MerchantFeedImportServiceImpl implements MerchantFeedImportService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MerchantFeedImportServiceImpl.class);
|
||||||
|
|
||||||
private final MerchantRepository merchantRepository;
|
private final MerchantRepository merchantRepository;
|
||||||
private final BrandRepository brandRepository;
|
private final BrandRepository brandRepository;
|
||||||
@@ -56,27 +60,22 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CacheEvict(value = "gunbuilderProducts", allEntries = true)
|
||||||
public void importMerchantFeed(Integer merchantId) {
|
public void importMerchantFeed(Integer merchantId) {
|
||||||
System.out.println("IMPORT >>> importMerchantFeed(" + merchantId + ")");
|
log.info("Starting full import for merchantId={}", merchantId);
|
||||||
|
|
||||||
Merchant merchant = merchantRepository.findById(merchantId)
|
Merchant merchant = merchantRepository.findById(merchantId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + merchantId));
|
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + merchantId));
|
||||||
|
|
||||||
// Read all rows from the merchant feed
|
// Read all rows from the merchant feed
|
||||||
List<MerchantFeedRow> rows = readFeedRowsForMerchant(merchant);
|
List<MerchantFeedRow> rows = readFeedRowsForMerchant(merchant);
|
||||||
System.out.println("IMPORT >>> read " + rows.size() + " rows for merchant=" + merchant.getName());
|
log.info("Read {} feed rows for merchant={}", rows.size(), merchant.getName());
|
||||||
|
|
||||||
for (MerchantFeedRow row : rows) {
|
for (MerchantFeedRow row : rows) {
|
||||||
// Resolve brand from the row (fallback to "Aero Precision" or whatever you want as default)
|
|
||||||
Brand brand = resolveBrand(row);
|
Brand brand = resolveBrand(row);
|
||||||
Product p = upsertProduct(merchant, brand, row);
|
Product p = upsertProduct(merchant, brand, row);
|
||||||
|
log.debug("Upserted product id={}, name={}, slug={}, platform={}, partRole={}, merchant={}",
|
||||||
System.out.println("IMPORT >>> upserted product id=" + p.getId()
|
p.getId(), p.getName(), p.getSlug(), p.getPlatform(), p.getPartRole(), merchant.getName());
|
||||||
+ ", name=" + p.getName()
|
|
||||||
+ ", slug=" + p.getSlug()
|
|
||||||
+ ", platform=" + p.getPlatform()
|
|
||||||
+ ", partRole=" + p.getPartRole()
|
|
||||||
+ ", merchant=" + merchant.getName());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +84,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
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()
|
log.debug("Upserting product for brand={}, sku={}, name={}", brand.getName(), row.sku(), row.productName());
|
||||||
+ ", sku=" + row.sku()
|
|
||||||
+ ", productName=" + row.productName());
|
|
||||||
|
|
||||||
String mpn = trimOrNull(row.manufacturerId());
|
String mpn = trimOrNull(row.manufacturerId());
|
||||||
String upc = trimOrNull(row.sku()); // placeholder until real UPC field
|
String upc = trimOrNull(row.sku()); // placeholder until real UPC field
|
||||||
@@ -109,9 +106,8 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
p.setBrand(brand);
|
p.setBrand(brand);
|
||||||
} else {
|
} else {
|
||||||
if (candidates.size() > 1) {
|
if (candidates.size() > 1) {
|
||||||
System.out.println("IMPORT !!! WARNING: multiple existing products found for brand="
|
log.warn("Multiple existing products found for brand={}, mpn={}, upc={}. Using first match id={}",
|
||||||
+ brand.getName() + ", mpn=" + mpn + ", upc=" + upc
|
brand.getName(), mpn, upc, candidates.get(0).getId());
|
||||||
+ ". Using the first match (id=" + candidates.get(0).getId() + ")");
|
|
||||||
}
|
}
|
||||||
p = candidates.get(0);
|
p = candidates.get(0);
|
||||||
}
|
}
|
||||||
@@ -127,7 +123,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
private List<Map<String, String>> fetchFeedRows(String feedUrl) {
|
private List<Map<String, String>> fetchFeedRows(String feedUrl) {
|
||||||
System.out.println("OFFERS >>> reading offer feed from: " + feedUrl);
|
log.info("Reading offer feed from {}", feedUrl);
|
||||||
|
|
||||||
List<Map<String, String>> rows = new ArrayList<>();
|
List<Map<String, String>> rows = new ArrayList<>();
|
||||||
|
|
||||||
@@ -154,7 +150,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
throw new RuntimeException("Failed to read offer feed from " + feedUrl, ex);
|
throw new RuntimeException("Failed to read offer feed from " + feedUrl, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("OFFERS >>> parsed " + rows.size() + " offer rows");
|
log.info("Parsed {} offer rows from offer feed {}", rows.size(), feedUrl);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +251,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
String avantlinkProductId = trimOrNull(row.sku());
|
String avantlinkProductId = trimOrNull(row.sku());
|
||||||
if (avantlinkProductId == null) {
|
if (avantlinkProductId == null) {
|
||||||
// If there's truly no SKU, bail out – we can't match this offer reliably.
|
// If there's truly no SKU, bail out – we can't match this offer reliably.
|
||||||
System.out.println("IMPORT !!! skipping offer: no SKU for product id=" + product.getId());
|
log.debug("Skipping offer row with no SKU for product id={}", product.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,11 +354,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
|
|
||||||
Map<String, Integer> headerMap = parser.getHeaderMap();
|
Map<String, Integer> headerMap = parser.getHeaderMap();
|
||||||
if (headerMap != null && headerMap.keySet().containsAll(requiredHeaders)) {
|
if (headerMap != null && headerMap.keySet().containsAll(requiredHeaders)) {
|
||||||
System.out.println(
|
log.info("Detected delimiter '{}' for feed {}", (delimiter == '\t' ? "\\t" : String.valueOf(delimiter)), feedUrl);
|
||||||
"IMPORT >>> detected delimiter '" +
|
|
||||||
(delimiter == '\t' ? "\\t" : String.valueOf(delimiter)) +
|
|
||||||
"' for feed: " + feedUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
return CSVFormat.DEFAULT.builder()
|
return CSVFormat.DEFAULT.builder()
|
||||||
.setDelimiter(delimiter)
|
.setDelimiter(delimiter)
|
||||||
@@ -372,16 +364,11 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
.setTrim(true)
|
.setTrim(true)
|
||||||
.build();
|
.build();
|
||||||
} else if (headerMap != null) {
|
} else if (headerMap != null) {
|
||||||
System.out.println(
|
log.debug("Delimiter '{}' produced headers {} for feed {}", (delimiter == '\t' ? "\\t" : String.valueOf(delimiter)), headerMap.keySet(), feedUrl);
|
||||||
"IMPORT !!! delimiter '" +
|
|
||||||
(delimiter == '\t' ? "\\t" : String.valueOf(delimiter)) +
|
|
||||||
"' produced headers: " + headerMap.keySet()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
lastException = ex;
|
lastException = ex;
|
||||||
System.out.println("IMPORT !!! error probing delimiter '" + delimiter +
|
log.warn("Error probing delimiter '{}' for {}: {}", delimiter, feedUrl, ex.getMessage());
|
||||||
"' for " + feedUrl + ": " + ex.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +385,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
String feedUrl = rawFeedUrl.trim();
|
String feedUrl = rawFeedUrl.trim();
|
||||||
System.out.println("IMPORT >>> reading feed for merchant=" + merchant.getName() + " from: " + feedUrl);
|
log.info("Reading product feed for merchant={} from {}", merchant.getName(), feedUrl);
|
||||||
|
|
||||||
List<MerchantFeedRow> rows = new ArrayList<>();
|
List<MerchantFeedRow> rows = new ArrayList<>();
|
||||||
|
|
||||||
@@ -409,7 +396,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
try (Reader reader = openFeedReader(feedUrl);
|
try (Reader reader = openFeedReader(feedUrl);
|
||||||
CSVParser parser = new CSVParser(reader, format)) {
|
CSVParser parser = new CSVParser(reader, format)) {
|
||||||
|
|
||||||
System.out.println("IMPORT >>> detected headers: " + parser.getHeaderMap().keySet());
|
log.debug("Detected feed headers for merchant {}: {}", merchant.getName(), parser.getHeaderMap().keySet());
|
||||||
|
|
||||||
for (CSVRecord rec : parser) {
|
for (CSVRecord rec : parser) {
|
||||||
MerchantFeedRow row = new MerchantFeedRow(
|
MerchantFeedRow row = new MerchantFeedRow(
|
||||||
@@ -447,7 +434,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
+ merchant.getName() + " from " + feedUrl, ex);
|
+ merchant.getName() + " from " + feedUrl, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("IMPORT >>> parsed " + rows.size() + " rows for merchant=" + merchant.getName());
|
log.info("Parsed {} product rows for merchant={}", rows.size(), merchant.getName());
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +461,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
try {
|
try {
|
||||||
return new BigDecimal(trimmed);
|
return new BigDecimal(trimmed);
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
System.out.println("IMPORT !!! bad BigDecimal value: '" + raw + "', skipping");
|
log.debug("Skipping invalid numeric value '{}'", raw);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,8 +482,7 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
try {
|
try {
|
||||||
return rec.get(header);
|
return rec.get(header);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
System.out.println("IMPORT !!! short record #" + rec.getRecordNumber()
|
log.debug("Short CSV record #{} missing column '{}', treating as null", rec.getRecordNumber(), header);
|
||||||
+ " missing column '" + header + "', treating as null");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,7 +579,9 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
|
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
@CacheEvict(value = "gunbuilderProducts", allEntries = true)
|
||||||
public void syncOffersOnly(Integer merchantId) {
|
public void syncOffersOnly(Integer merchantId) {
|
||||||
|
log.info("Starting offers-only sync for merchantId={}", merchantId);
|
||||||
Merchant merchant = merchantRepository.findById(merchantId)
|
Merchant merchant = merchantRepository.findById(merchantId)
|
||||||
.orElseThrow(() -> new RuntimeException("Merchant not found"));
|
.orElseThrow(() -> new RuntimeException("Merchant not found"));
|
||||||
|
|
||||||
@@ -601,7 +589,6 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use offerFeedUrl if present, else fall back to feedUrl
|
|
||||||
String feedUrl = merchant.getOfferFeedUrl() != null
|
String feedUrl = merchant.getOfferFeedUrl() != null
|
||||||
? merchant.getOfferFeedUrl()
|
? merchant.getOfferFeedUrl()
|
||||||
: merchant.getFeedUrl();
|
: merchant.getFeedUrl();
|
||||||
@@ -618,7 +605,9 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
|
|
||||||
merchant.setLastOfferSyncAt(OffsetDateTime.now());
|
merchant.setLastOfferSyncAt(OffsetDateTime.now());
|
||||||
merchantRepository.save(merchant);
|
merchantRepository.save(merchant);
|
||||||
|
log.info("Completed offers-only sync for merchantId={} ({} rows processed)", merchantId, rows.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upsertOfferOnlyFromRow(Merchant merchant, Map<String, String> row) {
|
private void upsertOfferOnlyFromRow(Merchant merchant, Map<String, String> row) {
|
||||||
// For the offer-only sync, we key offers by the same identifier we used when creating them.
|
// For the offer-only sync, we key offers by the same identifier we used when creating them.
|
||||||
// In the current AvantLink-style feed, that is the SKU column.
|
// In the current AvantLink-style feed, that is the SKU column.
|
||||||
|
|||||||
Reference in New Issue
Block a user