mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-05 18:46:44 -05:00
running build. New merchant import admin page.
This commit is contained in:
@@ -1,24 +0,0 @@
|
|||||||
package group.goforward.ballistic.controllers;
|
|
||||||
|
|
||||||
import group.goforward.ballistic.model.Merchant;
|
|
||||||
import group.goforward.ballistic.repos.MerchantRepository;
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/admin/merchants")
|
|
||||||
@CrossOrigin // adjust later if you want
|
|
||||||
public class AdminMerchantController {
|
|
||||||
|
|
||||||
private final MerchantRepository merchantRepository;
|
|
||||||
|
|
||||||
public AdminMerchantController(MerchantRepository merchantRepository) {
|
|
||||||
this.merchantRepository = merchantRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public List<Merchant> getMerchants() {
|
|
||||||
// If you want a DTO here, you can wrap it, but this is fine for internal admin
|
|
||||||
return merchantRepository.findAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,34 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/admin/imports")
|
@RequestMapping("/admin/imports")
|
||||||
|
@CrossOrigin(origins = "http://localhost:3000")
|
||||||
public class ImportController {
|
public class ImportController {
|
||||||
|
|
||||||
private final MerchantFeedImportService importService;
|
private final MerchantFeedImportService merchantFeedImportService;
|
||||||
|
|
||||||
public ImportController(MerchantFeedImportService importService) {
|
public ImportController(MerchantFeedImportService merchantFeedImportService) {
|
||||||
this.importService = importService;
|
this.merchantFeedImportService = merchantFeedImportService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full product + offer import for a merchant.
|
||||||
|
*
|
||||||
|
* POST /admin/imports/{merchantId}
|
||||||
|
*/
|
||||||
@PostMapping("/{merchantId}")
|
@PostMapping("/{merchantId}")
|
||||||
public ResponseEntity<Void> importMerchant(@PathVariable Integer merchantId) {
|
public ResponseEntity<Void> importMerchant(@PathVariable Integer merchantId) {
|
||||||
importService.importMerchantFeed(merchantId);
|
merchantFeedImportService.importMerchantFeed(merchantId);
|
||||||
return ResponseEntity.accepted().build();
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offers-only sync (price/stock) for a merchant.
|
||||||
|
*
|
||||||
|
* POST /admin/imports/{merchantId}/offers-only
|
||||||
|
*/
|
||||||
|
@PostMapping("/{merchantId}/offers-only")
|
||||||
|
public ResponseEntity<Void> syncOffersOnly(@PathVariable Integer merchantId) {
|
||||||
|
merchantFeedImportService.syncOffersOnly(merchantId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// MerchantAdminController.java
|
||||||
|
package group.goforward.ballistic.controllers;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.MerchantAdminDto;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/merchants")
|
||||||
|
@CrossOrigin(origins = "http://localhost:3000") // TEMP for Cross-Origin Bug
|
||||||
|
public class MerchantAdminController {
|
||||||
|
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
|
||||||
|
public MerchantAdminController(MerchantRepository merchantRepository) {
|
||||||
|
this.merchantRepository = merchantRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<MerchantAdminDto> listMerchants() {
|
||||||
|
return merchantRepository.findAll().stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public MerchantAdminDto updateMerchant(
|
||||||
|
@PathVariable Integer id,
|
||||||
|
@RequestBody MerchantAdminDto payload
|
||||||
|
) {
|
||||||
|
Merchant merchant = merchantRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Merchant not found"));
|
||||||
|
|
||||||
|
merchant.setFeedUrl(payload.getFeedUrl());
|
||||||
|
merchant.setOfferFeedUrl(payload.getOfferFeedUrl());
|
||||||
|
merchant.setIsActive(payload.getIsActive() != null ? payload.getIsActive() : true);
|
||||||
|
// don’t touch last* here; those are set by import jobs
|
||||||
|
|
||||||
|
merchant = merchantRepository.save(merchant);
|
||||||
|
return toDto(merchant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MerchantAdminDto toDto(Merchant m) {
|
||||||
|
MerchantAdminDto dto = new MerchantAdminDto();
|
||||||
|
dto.setId(m.getId());
|
||||||
|
dto.setName(m.getName());
|
||||||
|
dto.setFeedUrl(m.getFeedUrl());
|
||||||
|
dto.setOfferFeedUrl(m.getOfferFeedUrl());
|
||||||
|
dto.setIsActive(m.getIsActive());
|
||||||
|
dto.setLastFullImportAt(m.getLastFullImportAt());
|
||||||
|
dto.setLastOfferSyncAt(m.getLastOfferSyncAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import java.time.OffsetDateTime;
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "merchants")
|
@Table(name = "merchants")
|
||||||
public class Merchant {
|
public class Merchant {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "id", nullable = false)
|
@Column(name = "id", nullable = false)
|
||||||
@@ -22,9 +23,18 @@ public class Merchant {
|
|||||||
@Column(name = "feed_url", nullable = false, length = Integer.MAX_VALUE)
|
@Column(name = "feed_url", nullable = false, length = Integer.MAX_VALUE)
|
||||||
private String feedUrl;
|
private String feedUrl;
|
||||||
|
|
||||||
|
@Column(name = "offer_feed_url")
|
||||||
|
private String offerFeedUrl;
|
||||||
|
|
||||||
|
@Column(name = "last_full_import_at")
|
||||||
|
private OffsetDateTime lastFullImportAt;
|
||||||
|
|
||||||
|
@Column(name = "last_offer_sync_at")
|
||||||
|
private OffsetDateTime lastOfferSyncAt;
|
||||||
|
|
||||||
@ColumnDefault("true")
|
@ColumnDefault("true")
|
||||||
@Column(name = "is_active", nullable = false)
|
@Column(name = "is_active", nullable = false)
|
||||||
private Boolean isActive = false;
|
private Boolean isActive = true;
|
||||||
|
|
||||||
@ColumnDefault("now()")
|
@ColumnDefault("now()")
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
@@ -34,6 +44,10 @@ public class Merchant {
|
|||||||
@Column(name = "updated_at", nullable = false)
|
@Column(name = "updated_at", nullable = false)
|
||||||
private OffsetDateTime updatedAt;
|
private OffsetDateTime updatedAt;
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// GETTERS & SETTERS
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
public Integer getId() {
|
public Integer getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -66,12 +80,36 @@ public class Merchant {
|
|||||||
this.feedUrl = feedUrl;
|
this.feedUrl = feedUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOfferFeedUrl() {
|
||||||
|
return offerFeedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfferFeedUrl(String offerFeedUrl) {
|
||||||
|
this.offerFeedUrl = offerFeedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getLastFullImportAt() {
|
||||||
|
return lastFullImportAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastFullImportAt(OffsetDateTime lastFullImportAt) {
|
||||||
|
this.lastFullImportAt = lastFullImportAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getLastOfferSyncAt() {
|
||||||
|
return lastOfferSyncAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastOfferSyncAt(OffsetDateTime lastOfferSyncAt) {
|
||||||
|
this.lastOfferSyncAt = lastOfferSyncAt;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getIsActive() {
|
public Boolean getIsActive() {
|
||||||
return isActive;
|
return isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIsActive(Boolean isActive) {
|
public void setIsActive(Boolean active) {
|
||||||
this.isActive = isActive;
|
this.isActive = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OffsetDateTime getCreatedAt() {
|
public OffsetDateTime getCreatedAt() {
|
||||||
@@ -89,5 +127,4 @@ public class Merchant {
|
|||||||
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,12 @@ package group.goforward.ballistic.services;
|
|||||||
public interface MerchantFeedImportService {
|
public interface MerchantFeedImportService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import the feed for a given merchant id.
|
* Full product + offer import for a given merchant.
|
||||||
*/
|
*/
|
||||||
void importMerchantFeed(Integer merchantId);
|
void importMerchantFeed(Integer merchantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offers-only sync (price / stock) for a given merchant.
|
||||||
|
*/
|
||||||
|
void syncOffersOnly(Integer merchantId);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
@@ -123,7 +125,38 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
private List<Map<String, String>> fetchFeedRows(String feedUrl) {
|
||||||
|
System.out.println("OFFERS >>> reading offer feed from: " + feedUrl);
|
||||||
|
|
||||||
|
List<Map<String, String>> 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)) {
|
||||||
|
|
||||||
|
// capture header names from the CSV
|
||||||
|
List<String> headers = new ArrayList<>(parser.getHeaderMap().keySet());
|
||||||
|
|
||||||
|
for (CSVRecord rec : parser) {
|
||||||
|
Map<String, String> row = new HashMap<>();
|
||||||
|
for (String header : headers) {
|
||||||
|
row.put(header, rec.get(header));
|
||||||
|
}
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException("Failed to read offer feed from " + feedUrl, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("OFFERS >>> parsed " + rows.size() + " offer rows");
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
private void updateProductFromRow(Product p, Merchant merchant, MerchantFeedRow row, boolean isNew) {
|
private void updateProductFromRow(Product p, Merchant merchant, MerchantFeedRow row, boolean isNew) {
|
||||||
// ---------- NAME ----------
|
// ---------- NAME ----------
|
||||||
String name = coalesce(
|
String name = coalesce(
|
||||||
@@ -465,4 +498,79 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
|||||||
|
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
public void syncOffersOnly(Integer merchantId) {
|
||||||
|
Merchant merchant = merchantRepository.findById(merchantId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Merchant not found"));
|
||||||
|
|
||||||
|
if (Boolean.FALSE.equals(merchant.getIsActive())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use offerFeedUrl if present, else fall back to feedUrl
|
||||||
|
String feedUrl = merchant.getOfferFeedUrl() != null
|
||||||
|
? merchant.getOfferFeedUrl()
|
||||||
|
: merchant.getFeedUrl();
|
||||||
|
|
||||||
|
if (feedUrl == null) {
|
||||||
|
throw new RuntimeException("No offer feed URL configured for merchant " + merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, String>> rows = fetchFeedRows(feedUrl);
|
||||||
|
|
||||||
|
for (Map<String, String> row : rows) {
|
||||||
|
upsertOfferOnlyFromRow(merchant, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
merchant.setLastOfferSyncAt(OffsetDateTime.now());
|
||||||
|
merchantRepository.save(merchant);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
// In the current AvantLink-style feed, that is the SKU column.
|
||||||
|
String avantlinkProductId = trimOrNull(row.get("SKU"));
|
||||||
|
if (avantlinkProductId == null || avantlinkProductId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing offer
|
||||||
|
ProductOffer offer = productOfferRepository
|
||||||
|
.findByMerchantIdAndAvantlinkProductId(merchant.getId(), avantlinkProductId)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (offer == null) {
|
||||||
|
// This is a *sync* pass, not full ETL – if we don't already have an offer, skip.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse price fields (column names match the main product feed)
|
||||||
|
BigDecimal price = parseBigDecimal(row.get("Sale Price"));
|
||||||
|
BigDecimal originalPrice = parseBigDecimal(row.get("Retail Price"));
|
||||||
|
|
||||||
|
// Update only *offer* fields – do not touch Product
|
||||||
|
offer.setPrice(price);
|
||||||
|
offer.setOriginalPrice(originalPrice);
|
||||||
|
offer.setInStock(parseInStock(row));
|
||||||
|
|
||||||
|
// Prefer a fresh Buy Link from the feed if present, otherwise keep existing
|
||||||
|
String newBuyUrl = trimOrNull(row.get("Buy Link"));
|
||||||
|
offer.setBuyUrl(coalesce(newBuyUrl, offer.getBuyUrl()));
|
||||||
|
|
||||||
|
offer.setLastSeenAt(OffsetDateTime.now());
|
||||||
|
|
||||||
|
productOfferRepository.save(offer);
|
||||||
|
}
|
||||||
|
private Boolean parseInStock(Map<String, String> row) {
|
||||||
|
String inStock = trimOrNull(row.get("In Stock"));
|
||||||
|
if (inStock == null) return Boolean.FALSE;
|
||||||
|
|
||||||
|
String lower = inStock.toLowerCase();
|
||||||
|
if (lower.contains("true") || lower.contains("yes") || lower.contains("1")) {
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
if (lower.contains("false") || lower.contains("no") || lower.contains("0")) {
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// MerchantAdminDto.java
|
||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
public class MerchantAdminDto {
|
||||||
|
private Integer id;
|
||||||
|
private String name;
|
||||||
|
private String feedUrl;
|
||||||
|
private String offerFeedUrl;
|
||||||
|
private Boolean isActive;
|
||||||
|
private OffsetDateTime lastFullImportAt;
|
||||||
|
private OffsetDateTime lastOfferSyncAt;
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFeedUrl() {
|
||||||
|
return feedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeedUrl(String feedUrl) {
|
||||||
|
this.feedUrl = feedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOfferFeedUrl() {
|
||||||
|
return offerFeedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfferFeedUrl(String offerFeedUrl) {
|
||||||
|
this.offerFeedUrl = offerFeedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsActive(Boolean isActive) {
|
||||||
|
this.isActive = isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getLastFullImportAt() {
|
||||||
|
return lastFullImportAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastFullImportAt(OffsetDateTime lastFullImportAt) {
|
||||||
|
this.lastFullImportAt = lastFullImportAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getLastOfferSyncAt() {
|
||||||
|
return lastOfferSyncAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastOfferSyncAt(OffsetDateTime lastOfferSyncAt) {
|
||||||
|
this.lastOfferSyncAt = lastOfferSyncAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user