mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-06 02:56:44 -05:00
Compare commits
2 Commits
f1dcd10a79
...
c4d2adad1a
| Author | SHA1 | Date | |
|---|---|---|---|
| c4d2adad1a | |||
| 0b2b3afd0c |
@@ -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
|
||||
@RequestMapping("/admin/imports")
|
||||
@CrossOrigin(origins = "http://localhost:3000")
|
||||
public class ImportController {
|
||||
|
||||
private final MerchantFeedImportService importService;
|
||||
private final MerchantFeedImportService merchantFeedImportService;
|
||||
|
||||
public ImportController(MerchantFeedImportService importService) {
|
||||
this.importService = importService;
|
||||
public ImportController(MerchantFeedImportService merchantFeedImportService) {
|
||||
this.merchantFeedImportService = merchantFeedImportService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full product + offer import for a merchant.
|
||||
*
|
||||
* POST /admin/imports/{merchantId}
|
||||
*/
|
||||
@PostMapping("/{merchantId}")
|
||||
public ResponseEntity<Void> importMerchant(@PathVariable Integer merchantId) {
|
||||
importService.importMerchantFeed(merchantId);
|
||||
return ResponseEntity.accepted().build();
|
||||
merchantFeedImportService.importMerchantFeed(merchantId);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,13 @@
|
||||
/**
|
||||
* Provides the classes necessary for the Spring Controllers for the ballistic -Builder application.
|
||||
* This package includes Controllers for Spring-Boot application
|
||||
*
|
||||
*
|
||||
* <p>The main entry point for managing the inventory is the
|
||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
||||
*
|
||||
* @since 1.0
|
||||
* @author Don Strawsburg
|
||||
* @version 1.1
|
||||
*/
|
||||
package group.goforward.ballistic.controllers;
|
||||
@@ -1 +1,13 @@
|
||||
/**
|
||||
* Provides the classes necessary for the Spring Data Transfer Objects for the ballistic -Builder application.
|
||||
* This package includes DTO for Spring-Boot application
|
||||
*
|
||||
*
|
||||
* <p>The main entry point for managing the inventory is the
|
||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
||||
*
|
||||
* @since 1.0
|
||||
* @author Sean Strawsburg
|
||||
* @version 1.1
|
||||
*/
|
||||
package group.goforward.ballistic.imports.dto;
|
||||
@@ -8,6 +8,7 @@ import java.time.OffsetDateTime;
|
||||
@Entity
|
||||
@Table(name = "merchants")
|
||||
public class Merchant {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
@@ -22,9 +23,18 @@ public class Merchant {
|
||||
@Column(name = "feed_url", nullable = false, length = Integer.MAX_VALUE)
|
||||
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")
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive = false;
|
||||
private Boolean isActive = true;
|
||||
|
||||
@ColumnDefault("now()")
|
||||
@Column(name = "created_at", nullable = false)
|
||||
@@ -34,6 +44,10 @@ public class Merchant {
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private OffsetDateTime updatedAt;
|
||||
|
||||
// -----------------------
|
||||
// GETTERS & SETTERS
|
||||
// -----------------------
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -66,12 +80,36 @@ public class Merchant {
|
||||
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() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setIsActive(Boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
public void setIsActive(Boolean active) {
|
||||
this.isActive = active;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
@@ -89,5 +127,4 @@ public class Merchant {
|
||||
public void setUpdatedAt(OffsetDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,12 @@ package group.goforward.ballistic.services;
|
||||
public interface MerchantFeedImportService {
|
||||
|
||||
/**
|
||||
* Import the feed for a given merchant id.
|
||||
* Full product + offer import for a given merchant.
|
||||
*/
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.io.Reader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
@@ -123,6 +125,37 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
|
||||
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) {
|
||||
// ---------- NAME ----------
|
||||
@@ -465,4 +498,79 @@ public class MerchantFeedImportServiceImpl implements MerchantFeedImportService
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,13 @@
|
||||
/**
|
||||
* Provides the classes necessary for the Spring Services implementations for the ballistic -Builder application.
|
||||
* This package includes Services implementations for Spring-Boot application
|
||||
*
|
||||
*
|
||||
* <p>The main entry point for managing the inventory is the
|
||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
||||
*
|
||||
* @since 1.0
|
||||
* @author Don Strawsburg
|
||||
* @version 1.1
|
||||
*/
|
||||
package group.goforward.ballistic.services.impl;
|
||||
@@ -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