mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2026-01-20 16:51:03 -05:00
fixed the merge conflicts on role-fixed branch and merged
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -15,7 +15,7 @@
|
|||||||
<artifactId>ballistic</artifactId>
|
<artifactId>ballistic</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>ballistic</name>
|
<name>ballistic</name>
|
||||||
<description>Ballistic Builder API</description>
|
<description>Battl Builder API</description>
|
||||||
|
|
||||||
<url/>
|
<url/>
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|||||||
@EnableCaching
|
@EnableCaching
|
||||||
@EntityScan(basePackages = "group.goforward.ballistic.model")
|
@EntityScan(basePackages = "group.goforward.ballistic.model")
|
||||||
@EnableJpaRepositories(basePackages = "group.goforward.ballistic.repos")
|
@EnableJpaRepositories(basePackages = "group.goforward.ballistic.repos")
|
||||||
public class BallisticApplication {
|
public class BattlBuilderApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(BallisticApplication.class, args);
|
SpringApplication.run(BattlBuilderApplication.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* <p>The main entry point for managing the inventory is the
|
* <p>The main entry point for managing the inventory is the
|
||||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
|
||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author Don Strawsburg
|
* @author Don Strawsburg
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package group.goforward.ballistic.web;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.AdminDashboardService;
|
||||||
|
import group.goforward.ballistic.web.dto.AdminDashboardOverviewDto;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/dashboard")
|
||||||
|
public class AdminDashboardController {
|
||||||
|
|
||||||
|
private final AdminDashboardService adminDashboardService;
|
||||||
|
|
||||||
|
public AdminDashboardController(AdminDashboardService adminDashboardService) {
|
||||||
|
this.adminDashboardService = adminDashboardService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/overview")
|
||||||
|
public ResponseEntity<AdminDashboardOverviewDto> getOverview() {
|
||||||
|
AdminDashboardOverviewDto dto = adminDashboardService.getOverview();
|
||||||
|
return ResponseEntity.ok(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,8 @@ public class AdminPartRoleMappingController {
|
|||||||
List<PartRoleMapping> mappings;
|
List<PartRoleMapping> mappings;
|
||||||
|
|
||||||
if (platform != null && !platform.isBlank()) {
|
if (platform != null && !platform.isBlank()) {
|
||||||
mappings = partRoleMappingRepository.findByPlatformOrderByPartRoleAsc(platform);
|
mappings = partRoleMappingRepository
|
||||||
|
.findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(platform);
|
||||||
} else {
|
} else {
|
||||||
mappings = partRoleMappingRepository.findAll();
|
mappings = partRoleMappingRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* <p>The main entry point for managing the inventory is the
|
* <p>The main entry point for managing the inventory is the
|
||||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
|
||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author Don Strawsburg
|
* @author Don Strawsburg
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* <p>The main entry point for managing the inventory is the
|
* <p>The main entry point for managing the inventory is the
|
||||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
|
||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author Sean Strawsburg
|
* @author Sean Strawsburg
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package group.goforward.ballistic.model;
|
||||||
|
|
||||||
|
public enum ImportStatus {
|
||||||
|
PENDING_MAPPING, // Ingested but not fully mapped / trusted
|
||||||
|
MAPPED, // Clean + mapped + safe for builder
|
||||||
|
REJECTED // Junk / not relevant / explicitly excluded
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package group.goforward.ballistic.model;
|
package group.goforward.ballistic.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "part_role_mappings")
|
@Table(name = "part_role_mappings")
|
||||||
@@ -14,15 +15,38 @@ public class PartRoleMapping {
|
|||||||
private String platform; // e.g. "AR-15"
|
private String platform; // e.g. "AR-15"
|
||||||
|
|
||||||
@Column(name = "part_role", nullable = false)
|
@Column(name = "part_role", nullable = false)
|
||||||
private String partRole; // e.g. "UPPER", "BARREL", etc.
|
private String partRole; // e.g. "LOWER_RECEIVER_STRIPPED"
|
||||||
|
|
||||||
@ManyToOne(optional = false)
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "part_category_id")
|
@JoinColumn(name = "part_category_id")
|
||||||
private PartCategory partCategory;
|
private PartCategory partCategory;
|
||||||
|
|
||||||
@Column(columnDefinition = "text")
|
@Column(columnDefinition = "text")
|
||||||
private String notes;
|
private String notes;
|
||||||
|
|
||||||
|
@Column(name = "created_at", updatable = false)
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private OffsetDateTime updatedAt;
|
||||||
|
|
||||||
|
@Column(name = "deleted_at")
|
||||||
|
private OffsetDateTime deletedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
this.createdAt = now;
|
||||||
|
this.updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters/setters
|
||||||
|
|
||||||
public Integer getId() {
|
public Integer getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -62,4 +86,20 @@ public class PartRoleMapping {
|
|||||||
public void setNotes(String notes) {
|
public void setNotes(String notes) {
|
||||||
this.notes = notes;
|
this.notes = notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getDeletedAt() {
|
||||||
|
return deletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeletedAt(OffsetDateTime deletedAt) {
|
||||||
|
this.deletedAt = deletedAt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,22 +9,19 @@ import java.util.Objects;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.ProductOffer;
|
|
||||||
import group.goforward.ballistic.model.ProductConfiguration;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "products")
|
@Table(name = "products")
|
||||||
@NamedQuery(name="Products.findByPlatformWithBrand", query= "" +
|
@NamedQuery(name="Products.findByPlatformWithBrand", query= "" +
|
||||||
"SELECT p FROM Product p" +
|
"SELECT p FROM Product p" +
|
||||||
" JOIN FETCH p.brand b" +
|
" JOIN FETCH p.brand b" +
|
||||||
" WHERE p.platform = :platform" +
|
" WHERE p.platform = :platform" +
|
||||||
" AND p.deletedAt IS NULL")
|
" AND p.deletedAt IS NULL")
|
||||||
|
|
||||||
@NamedQuery(name="Product.findByPlatformAndPartRoleInWithBrand", query= "" +
|
@NamedQuery(name="Product.findByPlatformAndPartRoleInWithBrand", query= "" +
|
||||||
"SELECT p FROM Product p JOIN FETCH p.brand b" +
|
"SELECT p FROM Product p JOIN FETCH p.brand b" +
|
||||||
" WHERE p.platform = :platform" +
|
" WHERE p.platform = :platform" +
|
||||||
" AND p.partRole IN :roles" +
|
" AND p.partRole IN :roles" +
|
||||||
" AND p.deletedAt IS NULL")
|
" AND p.deletedAt IS NULL")
|
||||||
|
|
||||||
@NamedQuery(name="Product.findProductsbyBrandByOffers", query="" +
|
@NamedQuery(name="Product.findProductsbyBrandByOffers", query="" +
|
||||||
" SELECT DISTINCT p FROM Product p" +
|
" SELECT DISTINCT p FROM Product p" +
|
||||||
@@ -32,11 +29,10 @@ import group.goforward.ballistic.model.ProductConfiguration;
|
|||||||
" LEFT JOIN FETCH p.offers o" +
|
" LEFT JOIN FETCH p.offers o" +
|
||||||
" WHERE p.platform = :platform" +
|
" WHERE p.platform = :platform" +
|
||||||
" AND p.deletedAt IS NULL")
|
" AND p.deletedAt IS NULL")
|
||||||
|
|
||||||
public class Product {
|
public class Product {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY) // uses products_id_seq in Postgres
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "id", nullable = false)
|
@Column(name = "id", nullable = false)
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
@@ -86,38 +82,27 @@ public class Product {
|
|||||||
|
|
||||||
@Column(name = "deleted_at")
|
@Column(name = "deleted_at")
|
||||||
private Instant deletedAt;
|
private Instant deletedAt;
|
||||||
|
|
||||||
@Column(name = "raw_category_key")
|
@Column(name = "raw_category_key")
|
||||||
private String rawCategoryKey;
|
private String rawCategoryKey;
|
||||||
|
|
||||||
@Column(name = "platform_locked", nullable = false)
|
@Column(name = "platform_locked", nullable = false)
|
||||||
private Boolean platformLocked = false;
|
private Boolean platformLocked = false;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "import_status", nullable = false)
|
||||||
|
private ImportStatus importStatus = ImportStatus.MAPPED;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
|
||||||
private Set<ProductOffer> offers = new HashSet<>();
|
private Set<ProductOffer> offers = new HashSet<>();
|
||||||
|
|
||||||
public Set<ProductOffer> getOffers() {
|
|
||||||
return offers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOffers(Set<ProductOffer> offers) {
|
|
||||||
this.offers = offers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- lifecycle hooks ---
|
// --- lifecycle hooks ---
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void prePersist() {
|
public void prePersist() {
|
||||||
if (uuid == null) {
|
if (uuid == null) uuid = UUID.randomUUID();
|
||||||
uuid = UUID.randomUUID();
|
|
||||||
}
|
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
if (createdAt == null) {
|
if (createdAt == null) createdAt = now;
|
||||||
createdAt = now;
|
if (updatedAt == null) updatedAt = now;
|
||||||
}
|
|
||||||
if (updatedAt == null) {
|
|
||||||
updatedAt = now;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
@@ -125,181 +110,101 @@ public class Product {
|
|||||||
updatedAt = Instant.now();
|
updatedAt = Instant.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRawCategoryKey() {
|
|
||||||
return rawCategoryKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRawCategoryKey(String rawCategoryKey) {
|
|
||||||
this.rawCategoryKey = rawCategoryKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- getters & setters ---
|
// --- getters & setters ---
|
||||||
|
public Integer getId() { return id; }
|
||||||
|
public void setId(Integer id) { this.id = id; }
|
||||||
|
|
||||||
public Integer getId() {
|
public UUID getUuid() { return uuid; }
|
||||||
return id;
|
public void setUuid(UUID uuid) { this.uuid = uuid; }
|
||||||
}
|
|
||||||
|
public Brand getBrand() { return brand; }
|
||||||
public void setId(Integer id) {
|
public void setBrand(Brand brand) { this.brand = brand; }
|
||||||
this.id = id;
|
|
||||||
}
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
public UUID getUuid() {
|
|
||||||
return uuid;
|
public String getSlug() { return slug; }
|
||||||
}
|
public void setSlug(String slug) { this.slug = slug; }
|
||||||
|
|
||||||
public void setUuid(UUID uuid) {
|
public String getMpn() { return mpn; }
|
||||||
this.uuid = uuid;
|
public void setMpn(String mpn) { this.mpn = mpn; }
|
||||||
}
|
|
||||||
|
public String getUpc() { return upc; }
|
||||||
public Brand getBrand() {
|
public void setUpc(String upc) { this.upc = upc; }
|
||||||
return brand;
|
|
||||||
}
|
public String getPlatform() { return platform; }
|
||||||
|
public void setPlatform(String platform) { this.platform = platform; }
|
||||||
public void setBrand(Brand brand) {
|
|
||||||
this.brand = brand;
|
public String getPartRole() { return partRole; }
|
||||||
}
|
public void setPartRole(String partRole) { this.partRole = partRole; }
|
||||||
|
|
||||||
public String getName() {
|
public ProductConfiguration getConfiguration() { return configuration; }
|
||||||
return name;
|
public void setConfiguration(ProductConfiguration configuration) {
|
||||||
}
|
this.configuration = configuration;
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSlug() {
|
|
||||||
return slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSlug(String slug) {
|
|
||||||
this.slug = slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMpn() {
|
|
||||||
return mpn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMpn(String mpn) {
|
|
||||||
this.mpn = mpn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUpc() {
|
|
||||||
return upc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUpc(String upc) {
|
|
||||||
this.upc = upc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPlatform() {
|
|
||||||
return platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPlatform(String platform) {
|
|
||||||
this.platform = platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPartRole() {
|
|
||||||
return partRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPartRole(String partRole) {
|
|
||||||
this.partRole = partRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getShortDescription() {
|
|
||||||
return shortDescription;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getShortDescription() { return shortDescription; }
|
||||||
public void setShortDescription(String shortDescription) {
|
public void setShortDescription(String shortDescription) {
|
||||||
this.shortDescription = shortDescription;
|
this.shortDescription = shortDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getDescription() { return description; }
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public void setDescription(String description) {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMainImageUrl() {
|
public String getMainImageUrl() { return mainImageUrl; }
|
||||||
return mainImageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMainImageUrl(String mainImageUrl) {
|
public void setMainImageUrl(String mainImageUrl) {
|
||||||
this.mainImageUrl = mainImageUrl;
|
this.mainImageUrl = mainImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant getCreatedAt() {
|
public Instant getCreatedAt() { return createdAt; }
|
||||||
return createdAt;
|
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreatedAt(Instant createdAt) {
|
public Instant getUpdatedAt() { return updatedAt; }
|
||||||
this.createdAt = createdAt;
|
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getUpdatedAt() {
|
public Instant getDeletedAt() { return deletedAt; }
|
||||||
return updatedAt;
|
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setUpdatedAt(Instant updatedAt) {
|
|
||||||
this.updatedAt = updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getDeletedAt() {
|
|
||||||
return deletedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDeletedAt(Instant deletedAt) {
|
|
||||||
this.deletedAt = deletedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getPlatformLocked() {
|
|
||||||
return platformLocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public Boolean getPlatformLocked() { return platformLocked; }
|
||||||
public void setPlatformLocked(Boolean platformLocked) {
|
public void setPlatformLocked(Boolean platformLocked) {
|
||||||
this.platformLocked = platformLocked;
|
this.platformLocked = platformLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProductConfiguration getConfiguration() {
|
public String getRawCategoryKey() { return rawCategoryKey; }
|
||||||
return configuration;
|
public void setRawCategoryKey(String rawCategoryKey) {
|
||||||
|
this.rawCategoryKey = rawCategoryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConfiguration(ProductConfiguration configuration) {
|
public ImportStatus getImportStatus() { return importStatus; }
|
||||||
this.configuration = configuration;
|
public void setImportStatus(ImportStatus importStatus) {
|
||||||
}
|
this.importStatus = importStatus;
|
||||||
// Convenience: best offer price for Gunbuilder
|
|
||||||
public BigDecimal getBestOfferPrice() {
|
|
||||||
if (offers == null || offers.isEmpty()) {
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return offers.stream()
|
public Set<ProductOffer> getOffers() { return offers; }
|
||||||
// pick sale_price if present, otherwise retail_price
|
public void setOffers(Set<ProductOffer> offers) { this.offers = offers; }
|
||||||
.map(offer -> {
|
|
||||||
if (offer.getSalePrice() != null) {
|
// --- computed helpers ---
|
||||||
return offer.getSalePrice();
|
|
||||||
}
|
public BigDecimal getBestOfferPrice() {
|
||||||
return offer.getRetailPrice();
|
if (offers == null || offers.isEmpty()) return BigDecimal.ZERO;
|
||||||
})
|
|
||||||
.filter(Objects::nonNull)
|
return offers.stream()
|
||||||
.min(BigDecimal::compareTo)
|
.map(offer -> offer.getSalePrice() != null
|
||||||
.orElse(BigDecimal.ZERO);
|
? offer.getSalePrice()
|
||||||
}
|
: offer.getRetailPrice())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.min(BigDecimal::compareTo)
|
||||||
|
.orElse(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
// Convenience: URL for the best-priced offer
|
|
||||||
public String getBestOfferBuyUrl() {
|
public String getBestOfferBuyUrl() {
|
||||||
if (offers == null || offers.isEmpty()) {
|
if (offers == null || offers.isEmpty()) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return offers.stream()
|
return offers.stream()
|
||||||
.sorted(Comparator.comparing(offer -> {
|
.sorted(Comparator.comparing(offer -> {
|
||||||
if (offer.getSalePrice() != null) {
|
if (offer.getSalePrice() != null) return offer.getSalePrice();
|
||||||
return offer.getSalePrice();
|
|
||||||
}
|
|
||||||
return offer.getRetailPrice();
|
return offer.getRetailPrice();
|
||||||
}, Comparator.nullsLast(BigDecimal::compareTo)))
|
}, Comparator.nullsLast(BigDecimal::compareTo)))
|
||||||
.map(ProductOffer::getAffiliateUrl)
|
.map(ProductOffer::getAffiliateUrl)
|
||||||
@@ -307,4 +212,4 @@ public BigDecimal getBestOfferPrice() {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package group.goforward.ballistic.repos;
|
package group.goforward.ballistic.repos;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -13,5 +14,15 @@ public interface MerchantCategoryMappingRepository
|
|||||||
String rawCategory
|
String rawCategory
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Optional<MerchantCategoryMapping> findByMerchantIdAndRawCategory(
|
||||||
|
Integer merchantId,
|
||||||
|
String rawCategory
|
||||||
|
);
|
||||||
|
|
||||||
List<MerchantCategoryMapping> findByMerchantIdOrderByRawCategoryAsc(Integer merchantId);
|
List<MerchantCategoryMapping> findByMerchantIdOrderByRawCategoryAsc(Integer merchantId);
|
||||||
|
|
||||||
|
Optional<MerchantCategoryMapping> findByMerchantAndRawCategoryIgnoreCase(
|
||||||
|
Merchant merchant,
|
||||||
|
String rawCategory
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,29 @@ import group.goforward.ballistic.model.PartRoleMapping;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface PartRoleMappingRepository extends JpaRepository<PartRoleMapping, Integer> {
|
public interface PartRoleMappingRepository extends JpaRepository<PartRoleMapping, Integer> {
|
||||||
|
|
||||||
// List mappings for a platform, ordered nicely for the UI
|
// For resolver: one mapping per platform + partRole
|
||||||
List<PartRoleMapping> findByPlatformOrderByPartRoleAsc(String platform);
|
Optional<PartRoleMapping> findFirstByPlatformAndPartRoleAndDeletedAtIsNull(
|
||||||
|
String platform,
|
||||||
|
String partRole
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optional: debug / inspection
|
||||||
|
List<PartRoleMapping> findAllByPlatformAndPartRoleAndDeletedAtIsNull(
|
||||||
|
String platform,
|
||||||
|
String partRole
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is the one PartRoleMappingService needs
|
||||||
|
List<PartRoleMapping> findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(
|
||||||
|
String platform
|
||||||
|
);
|
||||||
|
|
||||||
|
List<PartRoleMapping> findByPlatformAndPartCategory_SlugAndDeletedAtIsNull(
|
||||||
|
String platform,
|
||||||
|
String slug
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package group.goforward.ballistic.repos;
|
|||||||
|
|
||||||
import group.goforward.ballistic.model.ProductOffer;
|
import group.goforward.ballistic.model.ProductOffer;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -19,4 +20,15 @@ public interface ProductOfferRepository extends JpaRepository<ProductOffer, Inte
|
|||||||
Integer merchantId,
|
Integer merchantId,
|
||||||
String avantlinkProductId
|
String avantlinkProductId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT m.name, p.platform, p.importStatus, COUNT(DISTINCT p.id)
|
||||||
|
FROM ProductOffer o
|
||||||
|
JOIN o.product p
|
||||||
|
JOIN o.merchant m
|
||||||
|
WHERE p.deletedAt IS NULL
|
||||||
|
GROUP BY m.name, p.platform, p.importStatus
|
||||||
|
ORDER BY m.name ASC, p.platform ASC, p.importStatus ASC
|
||||||
|
""")
|
||||||
|
List<Object[]> countByMerchantPlatformAndStatus();
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
package group.goforward.ballistic.repos;
|
package group.goforward.ballistic.repos;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
import group.goforward.ballistic.model.Brand;
|
import group.goforward.ballistic.model.Brand;
|
||||||
import group.goforward.ballistic.model.Product;
|
import group.goforward.ballistic.model.Product;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ProductRepository extends JpaRepository<Product, Integer> {
|
public interface ProductRepository extends JpaRepository<Product, Integer> {
|
||||||
|
|
||||||
@@ -18,6 +24,8 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
|
|||||||
|
|
||||||
List<Product> findAllByBrandAndUpc(Brand brand, String upc);
|
List<Product> findAllByBrandAndUpc(Brand brand, String upc);
|
||||||
|
|
||||||
|
long countByImportStatus(ImportStatus importStatus);
|
||||||
|
|
||||||
boolean existsBySlug(String slug);
|
boolean existsBySlug(String slug);
|
||||||
|
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
@@ -25,16 +33,16 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
|
|||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT p
|
SELECT p
|
||||||
FROM Product p
|
FROM Product p
|
||||||
JOIN FETCH p.brand b
|
JOIN FETCH p.brand b
|
||||||
WHERE p.platform = :platform
|
WHERE p.platform = :platform
|
||||||
AND p.deletedAt IS NULL
|
AND p.deletedAt IS NULL
|
||||||
""")
|
""")
|
||||||
List<Product> findByPlatformWithBrand(@Param("platform") String platform);
|
List<Product> findByPlatformWithBrand(@Param("platform") String platform);
|
||||||
|
|
||||||
@Query(name="Products.findByPlatformWithBrand")
|
@Query(name = "Products.findByPlatformWithBrand")
|
||||||
List<Product> findByPlatformWithBrandNQ(@Param("platform") String platform);
|
List<Product> findByPlatformWithBrandNQ(@Param("platform") String platform);
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT p
|
SELECT p
|
||||||
@@ -50,16 +58,149 @@ List<Product> findByPlatformWithBrandNQ(@Param("platform") String platform);
|
|||||||
);
|
);
|
||||||
|
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
// Used by Gunbuilder service (if you wired this)
|
// Used by /api/gunbuilder/test-products-db
|
||||||
// -------------------------------------------------
|
// -------------------------------------------------
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT DISTINCT p
|
SELECT p
|
||||||
FROM Product p
|
FROM Product p
|
||||||
LEFT JOIN FETCH p.brand b
|
JOIN FETCH p.brand b
|
||||||
LEFT JOIN FETCH p.offers o
|
WHERE p.platform = :platform
|
||||||
WHERE p.platform = :platform
|
AND p.deletedAt IS NULL
|
||||||
AND p.deletedAt IS NULL
|
ORDER BY p.id
|
||||||
""")
|
""")
|
||||||
List<Product> findSomethingForGunbuilder(@Param("platform") String platform);
|
List<Product> findTop5ByPlatformWithBrand(@Param("platform") String platform);
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Used by GunbuilderProductService (builder UI)
|
||||||
|
// Only returns MAPPED products
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT DISTINCT p
|
||||||
|
FROM Product p
|
||||||
|
LEFT JOIN FETCH p.brand b
|
||||||
|
LEFT JOIN FETCH p.offers o
|
||||||
|
WHERE p.platform = :platform
|
||||||
|
AND p.partRole IN :partRoles
|
||||||
|
AND p.importStatus = :status
|
||||||
|
AND p.deletedAt IS NULL
|
||||||
|
""")
|
||||||
|
List<Product> findForGunbuilderByPlatformAndPartRoles(
|
||||||
|
@Param("platform") String platform,
|
||||||
|
@Param("partRoles") Collection<String> partRoles,
|
||||||
|
@Param("status") ImportStatus status
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Admin import-status dashboard (summary)
|
||||||
|
// -------------------------------------------------
|
||||||
|
@Query("""
|
||||||
|
SELECT p.importStatus AS status, COUNT(p) AS count
|
||||||
|
FROM Product p
|
||||||
|
WHERE p.deletedAt IS NULL
|
||||||
|
GROUP BY p.importStatus
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> aggregateByImportStatus();
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Admin import-status dashboard (by merchant)
|
||||||
|
// -------------------------------------------------
|
||||||
|
@Query("""
|
||||||
|
SELECT m.id AS merchantId,
|
||||||
|
m.name AS merchantName,
|
||||||
|
p.platform AS platform,
|
||||||
|
p.importStatus AS status,
|
||||||
|
COUNT(p) AS count
|
||||||
|
FROM Product p
|
||||||
|
JOIN p.offers o
|
||||||
|
JOIN o.merchant m
|
||||||
|
WHERE p.deletedAt IS NULL
|
||||||
|
GROUP BY m.id, m.name, p.platform, p.importStatus
|
||||||
|
ORDER BY m.name ASC, p.platform ASC, p.importStatus ASC
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> aggregateByMerchantAndStatus();
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Admin: Unmapped category clusters
|
||||||
|
// -------------------------------------------------
|
||||||
|
@Query("""
|
||||||
|
SELECT p.rawCategoryKey AS rawCategoryKey,
|
||||||
|
m.name AS merchantName,
|
||||||
|
COUNT(DISTINCT p.id) AS productCount
|
||||||
|
FROM Product p
|
||||||
|
JOIN p.offers o
|
||||||
|
JOIN o.merchant m
|
||||||
|
WHERE p.deletedAt IS NULL
|
||||||
|
AND (p.importStatus = group.goforward.ballistic.model.ImportStatus.PENDING_MAPPING
|
||||||
|
OR p.partRole IS NULL
|
||||||
|
OR LOWER(p.partRole) = 'unknown')
|
||||||
|
AND p.rawCategoryKey IS NOT NULL
|
||||||
|
GROUP BY p.rawCategoryKey, m.name
|
||||||
|
ORDER BY productCount DESC
|
||||||
|
""")
|
||||||
|
List<Map<String, Object>> findUnmappedCategoryGroups();
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT p
|
||||||
|
FROM Product p
|
||||||
|
JOIN p.offers o
|
||||||
|
JOIN o.merchant m
|
||||||
|
WHERE p.deletedAt IS NULL
|
||||||
|
AND m.name = :merchantName
|
||||||
|
AND p.rawCategoryKey = :rawCategoryKey
|
||||||
|
ORDER BY p.id
|
||||||
|
""")
|
||||||
|
List<Product> findExamplesForCategoryGroup(
|
||||||
|
@Param("merchantName") String merchantName,
|
||||||
|
@Param("rawCategoryKey") String rawCategoryKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Mapping admin – pending buckets (all merchants)
|
||||||
|
// -------------------------------------------------
|
||||||
|
@Query("""
|
||||||
|
SELECT m.id AS merchantId,
|
||||||
|
m.name AS merchantName,
|
||||||
|
p.rawCategoryKey AS rawCategoryKey,
|
||||||
|
mcm.mappedPartRole AS mappedPartRole,
|
||||||
|
COUNT(DISTINCT p.id) AS productCount
|
||||||
|
FROM Product p
|
||||||
|
JOIN p.offers o
|
||||||
|
JOIN o.merchant m
|
||||||
|
LEFT JOIN MerchantCategoryMapping mcm
|
||||||
|
ON mcm.merchant.id = m.id
|
||||||
|
AND mcm.rawCategory = p.rawCategoryKey
|
||||||
|
WHERE p.importStatus = :status
|
||||||
|
GROUP BY m.id, m.name, p.rawCategoryKey, mcm.mappedPartRole
|
||||||
|
ORDER BY productCount DESC
|
||||||
|
""")
|
||||||
|
List<Object[]> findPendingMappingBuckets(
|
||||||
|
@Param("status") ImportStatus status
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Mapping admin – pending buckets for a single merchant
|
||||||
|
// -------------------------------------------------
|
||||||
|
@Query("""
|
||||||
|
SELECT m.id AS merchantId,
|
||||||
|
m.name AS merchantName,
|
||||||
|
p.rawCategoryKey AS rawCategoryKey,
|
||||||
|
mcm.mappedPartRole AS mappedPartRole,
|
||||||
|
COUNT(DISTINCT p.id) AS productCount
|
||||||
|
FROM Product p
|
||||||
|
JOIN p.offers o
|
||||||
|
JOIN o.merchant m
|
||||||
|
LEFT JOIN MerchantCategoryMapping mcm
|
||||||
|
ON mcm.merchant.id = m.id
|
||||||
|
AND mcm.rawCategory = p.rawCategoryKey
|
||||||
|
WHERE p.importStatus = :status
|
||||||
|
AND m.id = :merchantId
|
||||||
|
GROUP BY m.id, m.name, p.rawCategoryKey, mcm.mappedPartRole
|
||||||
|
ORDER BY productCount DESC
|
||||||
|
""")
|
||||||
|
List<Object[]> findPendingMappingBucketsForMerchant(
|
||||||
|
@Param("merchantId") Integer merchantId,
|
||||||
|
@Param("status") ImportStatus status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -13,4 +13,6 @@ public interface UserRepository extends JpaRepository<User, Integer> {
|
|||||||
boolean existsByEmailIgnoreCaseAndDeletedAtIsNull(String email);
|
boolean existsByEmailIgnoreCaseAndDeletedAtIsNull(String email);
|
||||||
|
|
||||||
Optional<User> findByUuid(UUID uuid);
|
Optional<User> findByUuid(UUID uuid);
|
||||||
|
|
||||||
|
boolean existsByRole(String role);
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* <p>The main entry point for managing the inventory is the
|
* <p>The main entry point for managing the inventory is the
|
||||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
|
||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author Sean Strawsburg
|
* @author Sean Strawsburg
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.AdminDashboardOverviewDto;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminDashboardService {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
private final MerchantCategoryMappingRepository merchantCategoryMappingRepository;
|
||||||
|
|
||||||
|
public AdminDashboardService(
|
||||||
|
ProductRepository productRepository,
|
||||||
|
MerchantRepository merchantRepository,
|
||||||
|
MerchantCategoryMappingRepository merchantCategoryMappingRepository
|
||||||
|
) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
this.merchantRepository = merchantRepository;
|
||||||
|
this.merchantCategoryMappingRepository = merchantCategoryMappingRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public AdminDashboardOverviewDto getOverview() {
|
||||||
|
long totalProducts = productRepository.count();
|
||||||
|
long unmappedProducts = productRepository.countByImportStatus(ImportStatus.PENDING_MAPPING);
|
||||||
|
long mappedProducts = totalProducts - unmappedProducts;
|
||||||
|
|
||||||
|
long merchantCount = merchantRepository.count();
|
||||||
|
long categoryMappings = merchantCategoryMappingRepository.count();
|
||||||
|
|
||||||
|
return new AdminDashboardOverviewDto(
|
||||||
|
totalProducts,
|
||||||
|
mappedProducts,
|
||||||
|
unmappedProducts,
|
||||||
|
merchantCount,
|
||||||
|
categoryMappings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.Brand;
|
import group.goforward.ballistic.model.Brand;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface BrandService {
|
public interface BrandService {
|
||||||
|
|
||||||
List<Brand> findAll();
|
List<Brand> findAll();
|
||||||
|
|
||||||
Optional<Brand> findById(Integer id);
|
Optional<Brand> findById(Integer id);
|
||||||
|
|
||||||
Brand save(Brand item);
|
Brand save(Brand item);
|
||||||
void deleteById(Integer id);
|
void deleteById(Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/services/CategoryMappingRecommendationService.java
|
||||||
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.Product;
|
||||||
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.CategoryMappingRecommendationDto;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CategoryMappingRecommendationService {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
|
||||||
|
public CategoryMappingRecommendationService(ProductRepository productRepository) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CategoryMappingRecommendationDto> listRecommendations() {
|
||||||
|
var groups = productRepository.findUnmappedCategoryGroups();
|
||||||
|
|
||||||
|
return groups.stream()
|
||||||
|
.map(row -> {
|
||||||
|
String merchantName = (String) row.get("merchantName");
|
||||||
|
String rawCategoryKey = (String) row.get("rawCategoryKey");
|
||||||
|
long count = (long) row.get("productCount");
|
||||||
|
|
||||||
|
// Pull one sample product name
|
||||||
|
List<Product> examples = productRepository
|
||||||
|
.findExamplesForCategoryGroup(merchantName, rawCategoryKey);
|
||||||
|
String sampleName = examples.isEmpty()
|
||||||
|
? null
|
||||||
|
: examples.get(0).getName();
|
||||||
|
|
||||||
|
String recommendedRole = inferPartRoleFromRawKey(rawCategoryKey, sampleName);
|
||||||
|
|
||||||
|
return new CategoryMappingRecommendationDto(
|
||||||
|
merchantName,
|
||||||
|
rawCategoryKey,
|
||||||
|
count,
|
||||||
|
recommendedRole,
|
||||||
|
sampleName
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String inferPartRoleFromRawKey(String rawKey, String sampleName) {
|
||||||
|
String blob = (rawKey + " " + (sampleName != null ? sampleName : "")).toLowerCase();
|
||||||
|
|
||||||
|
if (blob.contains("handguard") || blob.contains("rail")) return "handguard";
|
||||||
|
if (blob.contains("barrel")) return "barrel";
|
||||||
|
if (blob.contains("upper")) return "upper-receiver";
|
||||||
|
if (blob.contains("lower")) return "lower-receiver";
|
||||||
|
if (blob.contains("mag") || blob.contains("magazine")) return "magazine";
|
||||||
|
if (blob.contains("stock") || blob.contains("buttstock")) return "stock";
|
||||||
|
if (blob.contains("grip")) return "grip";
|
||||||
|
if (blob.contains("trigger")) return "trigger";
|
||||||
|
if (blob.contains("sight") || blob.contains("iron sights")) return "sights";
|
||||||
|
if (blob.contains("optic") || blob.contains("scope") || blob.contains("red dot")) return "optic";
|
||||||
|
if (blob.contains("buffer")) return "buffer";
|
||||||
|
if (blob.contains("gas block")) return "gas-block";
|
||||||
|
if (blob.contains("gas tube")) return "gas-tube";
|
||||||
|
if (blob.contains("muzzle")) return "muzzle-device";
|
||||||
|
if (blob.contains("sling")) return "sling";
|
||||||
|
if (blob.contains("bipod")) return "bipod";
|
||||||
|
if (blob.contains("tool")) return "tools";
|
||||||
|
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,38 +5,82 @@ import group.goforward.ballistic.model.Product;
|
|||||||
import group.goforward.ballistic.repos.ProductRepository;
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
import group.goforward.ballistic.web.dto.GunbuilderProductDto;
|
import group.goforward.ballistic.web.dto.GunbuilderProductDto;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import group.goforward.ballistic.model.PartRoleMapping;
|
||||||
|
import group.goforward.ballistic.repos.PartRoleMappingRepository;
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GunbuilderProductService {
|
public class GunbuilderProductService {
|
||||||
|
|
||||||
private final ProductRepository productRepository;
|
private final ProductRepository productRepository;
|
||||||
private final PartCategoryResolverService partCategoryResolverService;
|
private final PartCategoryResolverService partCategoryResolverService;
|
||||||
|
private final PartRoleMappingRepository partRoleMappingRepository;
|
||||||
|
|
||||||
public GunbuilderProductService(
|
public GunbuilderProductService(
|
||||||
ProductRepository productRepository,
|
ProductRepository productRepository,
|
||||||
PartCategoryResolverService partCategoryResolverService
|
PartCategoryResolverService partCategoryResolverService,
|
||||||
|
PartRoleMappingRepository partRoleMappingRepository
|
||||||
) {
|
) {
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
this.partCategoryResolverService = partCategoryResolverService;
|
this.partCategoryResolverService = partCategoryResolverService;
|
||||||
|
this.partRoleMappingRepository = partRoleMappingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<GunbuilderProductDto> listGunbuilderProducts(String platform) {
|
/**
|
||||||
|
* Main builder endpoint.
|
||||||
|
* For now we ONLY support calls that provide partRoles,
|
||||||
|
* to avoid pulling the entire catalog into memory.
|
||||||
|
*/
|
||||||
|
public List<GunbuilderProductDto> listGunbuilderProducts(String platform, List<String> partRoles) {
|
||||||
|
|
||||||
List<Product> products = productRepository.findSomethingForGunbuilder(platform);
|
System.out.println(">>> GB: listGunbuilderProducts platform=" + platform
|
||||||
|
+ ", partRoles=" + partRoles);
|
||||||
|
|
||||||
|
if (partRoles == null || partRoles.isEmpty()) {
|
||||||
|
System.out.println(">>> GB: no partRoles provided, returning empty list");
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Product> products =
|
||||||
|
productRepository.findForGunbuilderByPlatformAndPartRoles(
|
||||||
|
platform,
|
||||||
|
partRoles,
|
||||||
|
ImportStatus.MAPPED
|
||||||
|
);
|
||||||
|
|
||||||
|
System.out.println(">>> GB: repo returned " + products.size() + " products");
|
||||||
|
|
||||||
|
Map<String, PartCategory> categoryCache = new HashMap<>();
|
||||||
|
|
||||||
return products.stream()
|
return products.stream()
|
||||||
.map(p -> {
|
.map(p -> {
|
||||||
var maybeCategory = partCategoryResolverService
|
PartCategory cat = categoryCache.computeIfAbsent(
|
||||||
.resolveForPlatformAndPartRole(platform, p.getPartRole());
|
p.getPartRole(),
|
||||||
|
role -> partCategoryResolverService
|
||||||
|
.resolveForPlatformAndPartRole(platform, role)
|
||||||
|
.orElse(null)
|
||||||
|
);
|
||||||
|
|
||||||
if (maybeCategory.isEmpty()) {
|
if (cat == null) {
|
||||||
// you can also log here
|
System.out.println(">>> GB: NO CATEGORY for platform=" + platform
|
||||||
return null;
|
+ ", partRole=" + p.getPartRole()
|
||||||
|
+ ", productId=" + p.getId());
|
||||||
|
} else {
|
||||||
|
System.out.println(">>> GB: CATEGORY for productId=" + p.getId()
|
||||||
|
+ " -> slug=" + cat.getSlug()
|
||||||
|
+ ", group=" + cat.getGroupName());
|
||||||
}
|
}
|
||||||
|
|
||||||
PartCategory cat = maybeCategory.get();
|
// TEMP: do NOT drop products if category is null.
|
||||||
|
// Just mark them as "unmapped" so we can see them in the JSON.
|
||||||
|
String categorySlug = (cat != null) ? cat.getSlug() : "unmapped";
|
||||||
|
String categoryGroup = (cat != null) ? cat.getGroupName() : "Unmapped";
|
||||||
|
|
||||||
return new GunbuilderProductDto(
|
return new GunbuilderProductDto(
|
||||||
p.getId(),
|
p.getId(),
|
||||||
@@ -47,11 +91,54 @@ public class GunbuilderProductService {
|
|||||||
p.getBestOfferPrice(),
|
p.getBestOfferPrice(),
|
||||||
p.getMainImageUrl(),
|
p.getMainImageUrl(),
|
||||||
p.getBestOfferBuyUrl(),
|
p.getBestOfferBuyUrl(),
|
||||||
cat.getSlug(),
|
categorySlug,
|
||||||
cat.getGroupName()
|
categoryGroup
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter(dto -> dto != null)
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<GunbuilderProductDto> listGunbuilderProductsByCategory(
|
||||||
|
String platform,
|
||||||
|
String categorySlug
|
||||||
|
) {
|
||||||
|
System.out.println(">>> GB: listGunbuilderProductsByCategory platform=" + platform
|
||||||
|
+ ", categorySlug=" + categorySlug);
|
||||||
|
|
||||||
|
if (platform == null || platform.isBlank()
|
||||||
|
|| categorySlug == null || categorySlug.isBlank()) {
|
||||||
|
System.out.println(">>> GB: missing platform or categorySlug, returning empty list");
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PartRoleMapping> mappings =
|
||||||
|
partRoleMappingRepository.findByPlatformAndPartCategory_SlugAndDeletedAtIsNull(
|
||||||
|
platform,
|
||||||
|
categorySlug
|
||||||
|
);
|
||||||
|
|
||||||
|
List<String> partRoles = mappings.stream()
|
||||||
|
.map(PartRoleMapping::getPartRole)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
System.out.println(">>> GB: resolved " + partRoles.size()
|
||||||
|
+ " partRoles for categorySlug=" + categorySlug + " -> " + partRoles);
|
||||||
|
|
||||||
|
if (partRoles.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse the existing method that already does all the DTO + category resolution logic
|
||||||
|
return listGunbuilderProducts(platform, partRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tiny helper used ONLY by /test-products-db to prove DB wiring.
|
||||||
|
*/
|
||||||
|
public List<Product> getSampleProducts(String platform) {
|
||||||
|
// You already have this wired via ProductRepository.findTop5ByPlatformWithBrand
|
||||||
|
// If that method exists, keep using it; if not, you can stub a tiny query here.
|
||||||
|
return productRepository.findTop5ByPlatformWithBrand(platform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/services/ImportStatusAdminService.java
|
||||||
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.ImportStatusByMerchantDto;
|
||||||
|
import group.goforward.ballistic.web.dto.ImportStatusSummaryDto;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ImportStatusAdminService {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
|
||||||
|
public ImportStatusAdminService(ProductRepository productRepository) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportStatusSummaryDto> summarizeByStatus() {
|
||||||
|
return productRepository.aggregateByImportStatus()
|
||||||
|
.stream()
|
||||||
|
.map(row -> new ImportStatusSummaryDto(
|
||||||
|
(ImportStatus) row.get("status"),
|
||||||
|
(long) row.get("count")
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportStatusByMerchantDto> summarizeByMerchant() {
|
||||||
|
return productRepository.aggregateByMerchantAndStatus()
|
||||||
|
.stream()
|
||||||
|
.map(row -> new ImportStatusByMerchantDto(
|
||||||
|
(String) row.get("merchantName"),
|
||||||
|
(String) row.get("platform"),
|
||||||
|
(ImportStatus) row.get("status"),
|
||||||
|
(long) row.get("count")
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
import group.goforward.ballistic.model.Merchant;
|
||||||
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
|
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.PendingMappingBucketDto;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MappingAdminService {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
private final MerchantCategoryMappingRepository merchantCategoryMappingRepository;
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
|
||||||
|
public MappingAdminService(
|
||||||
|
ProductRepository productRepository,
|
||||||
|
MerchantCategoryMappingRepository merchantCategoryMappingRepository,
|
||||||
|
MerchantRepository merchantRepository
|
||||||
|
) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
this.merchantCategoryMappingRepository = merchantCategoryMappingRepository;
|
||||||
|
this.merchantRepository = merchantRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all pending mapping buckets across all merchants.
|
||||||
|
* Each row is:
|
||||||
|
* [0] merchantId (Integer)
|
||||||
|
* [1] merchantName (String)
|
||||||
|
* [2] rawCategoryKey (String)
|
||||||
|
* [3] mappedPartRole (String, currently null from query)
|
||||||
|
* [4] productCount (Long)
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<PendingMappingBucketDto> listPendingBuckets() {
|
||||||
|
List<Object[]> rows = productRepository.findPendingMappingBuckets(
|
||||||
|
ImportStatus.PENDING_MAPPING
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.stream()
|
||||||
|
.map(row -> {
|
||||||
|
Integer merchantId = (Integer) row[0];
|
||||||
|
String merchantName = (String) row[1];
|
||||||
|
String rawCategoryKey = (String) row[2];
|
||||||
|
String mappedPartRole = (String) row[3];
|
||||||
|
Long count = (Long) row[4];
|
||||||
|
|
||||||
|
return new PendingMappingBucketDto(
|
||||||
|
merchantId,
|
||||||
|
merchantName,
|
||||||
|
rawCategoryKey,
|
||||||
|
(mappedPartRole != null && !mappedPartRole.isBlank()) ? mappedPartRole : null,
|
||||||
|
count != null ? count : 0L
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies or updates a mapping for (merchant, rawCategoryKey) to a given partRole.
|
||||||
|
* Does NOT retroactively update Product rows; they will be updated on the next import.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void applyMapping(Integer merchantId, String rawCategoryKey, String mappedPartRole) {
|
||||||
|
if (merchantId == null || rawCategoryKey == null || mappedPartRole == null || mappedPartRole.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("merchantId, rawCategoryKey, and mappedPartRole are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
Merchant merchant = merchantRepository.findById(merchantId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Merchant not found: " + merchantId));
|
||||||
|
|
||||||
|
MerchantCategoryMapping mapping = merchantCategoryMappingRepository
|
||||||
|
.findByMerchantIdAndRawCategory(merchantId, rawCategoryKey)
|
||||||
|
.orElseGet(() -> {
|
||||||
|
MerchantCategoryMapping m = new MerchantCategoryMapping();
|
||||||
|
m.setMerchant(merchant);
|
||||||
|
m.setRawCategory(rawCategoryKey);
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
|
||||||
|
mapping.setMappedPartRole(mappedPartRole.trim());
|
||||||
|
merchantCategoryMappingRepository.save(mapping);
|
||||||
|
|
||||||
|
// Products will pick up this mapping on the next merchant import run.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +1,95 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.Merchant;
|
import group.goforward.ballistic.model.Merchant;
|
||||||
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
import group.goforward.ballistic.model.MerchantCategoryMapping;
|
||||||
import group.goforward.ballistic.model.ProductConfiguration;
|
import group.goforward.ballistic.model.ProductConfiguration;
|
||||||
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
import group.goforward.ballistic.repos.MerchantCategoryMappingRepository;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
@Service
|
||||||
@Service
|
public class MerchantCategoryMappingService {
|
||||||
public class MerchantCategoryMappingService {
|
|
||||||
|
private final MerchantCategoryMappingRepository mappingRepository;
|
||||||
private final MerchantCategoryMappingRepository mappingRepository;
|
|
||||||
|
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
|
||||||
public MerchantCategoryMappingService(MerchantCategoryMappingRepository mappingRepository) {
|
this.mappingRepository = mappingRepository;
|
||||||
this.mappingRepository = mappingRepository;
|
}
|
||||||
}
|
|
||||||
|
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
|
||||||
public List<MerchantCategoryMapping> findByMerchant(Integer merchantId) {
|
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
|
||||||
return mappingRepository.findByMerchantIdOrderByRawCategoryAsc(merchantId);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Resolve (or create) a mapping row for this merchant + raw category.
|
||||||
* Resolve (or create) a mapping row for this merchant + raw category.
|
* - If it exists, returns it (with whatever mappedPartRole / mappedConfiguration are set).
|
||||||
* - If it exists, returns it (with whatever mappedPartRole / mappedConfiguration are set).
|
* - If it doesn't exist, creates a placeholder row with null mappings and returns it.
|
||||||
* - If it doesn't exist, creates a placeholder row with null mappings and returns it.
|
*
|
||||||
*
|
* The importer can then:
|
||||||
* The importer can then:
|
* - skip rows where mappedPartRole is still null
|
||||||
* - skip rows where mappedPartRole is still null
|
* - use mappedConfiguration if present
|
||||||
* - use mappedConfiguration if present
|
*/
|
||||||
*/
|
@Transactional
|
||||||
@Transactional
|
public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
|
||||||
public MerchantCategoryMapping resolveMapping(Merchant merchant, String rawCategory) {
|
if (rawCategory == null || rawCategory.isBlank()) {
|
||||||
if (rawCategory == null || rawCategory.isBlank()) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
String trimmed = rawCategory.trim();
|
||||||
String trimmed = rawCategory.trim();
|
|
||||||
|
return mappingRepository
|
||||||
return mappingRepository
|
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
|
||||||
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
|
.orElseGet(() -> {
|
||||||
.orElseGet(() -> {
|
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
|
||||||
MerchantCategoryMapping mapping = new MerchantCategoryMapping();
|
mapping.setMerchant(merchant);
|
||||||
mapping.setMerchant(merchant);
|
mapping.setRawCategory(trimmed);
|
||||||
mapping.setRawCategory(trimmed);
|
mapping.setMappedPartRole(null);
|
||||||
mapping.setMappedPartRole(null);
|
mapping.setMappedConfiguration(null);
|
||||||
mapping.setMappedConfiguration(null);
|
return mappingRepository.save(mapping);
|
||||||
return mappingRepository.save(mapping);
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Upsert mapping (admin UI).
|
||||||
* Upsert mapping (admin UI).
|
*/
|
||||||
*/
|
@Transactional
|
||||||
@Transactional
|
public MerchantCategoryMapping upsertMapping(
|
||||||
public MerchantCategoryMapping upsertMapping(
|
Merchant merchant,
|
||||||
Merchant merchant,
|
String rawCategory,
|
||||||
String rawCategory,
|
String mappedPartRole,
|
||||||
String mappedPartRole,
|
ProductConfiguration mappedConfiguration
|
||||||
ProductConfiguration mappedConfiguration
|
) {
|
||||||
) {
|
String trimmed = rawCategory.trim();
|
||||||
String trimmed = rawCategory.trim();
|
|
||||||
|
MerchantCategoryMapping mapping = mappingRepository
|
||||||
MerchantCategoryMapping mapping = mappingRepository
|
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
|
||||||
.findByMerchantIdAndRawCategoryIgnoreCase(merchant.getId(), trimmed)
|
.orElseGet(() -> {
|
||||||
.orElseGet(() -> {
|
MerchantCategoryMapping m = new MerchantCategoryMapping();
|
||||||
MerchantCategoryMapping m = new MerchantCategoryMapping();
|
m.setMerchant(merchant);
|
||||||
m.setMerchant(merchant);
|
m.setRawCategory(trimmed);
|
||||||
m.setRawCategory(trimmed);
|
return m;
|
||||||
return m;
|
});
|
||||||
});
|
|
||||||
|
mapping.setMappedPartRole(
|
||||||
mapping.setMappedPartRole(
|
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
|
||||||
(mappedPartRole == null || mappedPartRole.isBlank()) ? null : mappedPartRole.trim()
|
);
|
||||||
);
|
|
||||||
|
mapping.setMappedConfiguration(mappedConfiguration);
|
||||||
mapping.setMappedConfiguration(mappedConfiguration);
|
|
||||||
|
return mappingRepository.save(mapping);
|
||||||
return mappingRepository.save(mapping);
|
}
|
||||||
}
|
/**
|
||||||
/**
|
* Backwards-compatible overload for existing callers (e.g. controller)
|
||||||
* Backwards-compatible overload for existing callers (e.g. controller)
|
* that don’t care about productConfiguration yet.
|
||||||
* that don’t care about productConfiguration yet.
|
*/
|
||||||
*/
|
@Transactional
|
||||||
@Transactional
|
public MerchantCategoryMapping upsertMapping(
|
||||||
public MerchantCategoryMapping upsertMapping(
|
Merchant merchant,
|
||||||
Merchant merchant,
|
String rawCategory,
|
||||||
String rawCategory,
|
String mappedPartRole
|
||||||
String mappedPartRole
|
) {
|
||||||
) {
|
// Delegate to the new method with `null` configuration
|
||||||
// Delegate to the new method with `null` configuration
|
return upsertMapping(merchant, rawCategory, mappedPartRole, null);
|
||||||
return upsertMapping(merchant, rawCategory, mappedPartRole, null);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
public interface MerchantFeedImportService {
|
public interface MerchantFeedImportService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full product + offer import for a given merchant.
|
* 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.
|
* Offers-only sync (price / stock) for a given merchant.
|
||||||
*/
|
*/
|
||||||
void syncOffersOnly(Integer merchantId);
|
void syncOffersOnly(Integer merchantId);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.PartCategory;
|
import group.goforward.ballistic.model.PartCategory;
|
||||||
import group.goforward.ballistic.repos.PartCategoryRepository;
|
import group.goforward.ballistic.model.PartRoleMapping;
|
||||||
|
import group.goforward.ballistic.repos.PartRoleMappingRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -9,33 +10,29 @@ import java.util.Optional;
|
|||||||
@Service
|
@Service
|
||||||
public class PartCategoryResolverService {
|
public class PartCategoryResolverService {
|
||||||
|
|
||||||
private final PartCategoryRepository partCategoryRepository;
|
private final PartRoleMappingRepository partRoleMappingRepository;
|
||||||
|
|
||||||
public PartCategoryResolverService(PartCategoryRepository partCategoryRepository) {
|
public PartCategoryResolverService(PartRoleMappingRepository partRoleMappingRepository) {
|
||||||
this.partCategoryRepository = partCategoryRepository;
|
this.partRoleMappingRepository = partRoleMappingRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the canonical PartCategory for a given platform + partRole.
|
* Resolve a PartCategory for a given platform + partRole.
|
||||||
*
|
* Example: ("AR-15", "LOWER_RECEIVER_STRIPPED") -> category "lower"
|
||||||
* For now we keep it simple:
|
|
||||||
* - We treat partRole as the slug (e.g. "barrel", "upper", "trigger").
|
|
||||||
* - Normalize to lower-kebab (spaces -> dashes, lowercased).
|
|
||||||
* - Look up by slug in part_categories.
|
|
||||||
*
|
|
||||||
* Later, if we want per-merchant / per-platform overrides using category_mappings,
|
|
||||||
* we can extend this method without changing callers.
|
|
||||||
*/
|
*/
|
||||||
public Optional<PartCategory> resolveForPlatformAndPartRole(String platform, String partRole) {
|
public Optional<PartCategory> resolveForPlatformAndPartRole(String platform, String partRole) {
|
||||||
if (partRole == null || partRole.isBlank()) {
|
|
||||||
|
if (platform == null || partRole == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
String normalizedSlug = partRole
|
// Keep things case-sensitive since your DB values are already uppercase.
|
||||||
.trim()
|
Optional<PartRoleMapping> mappingOpt =
|
||||||
.toLowerCase()
|
partRoleMappingRepository.findFirstByPlatformAndPartRoleAndDeletedAtIsNull(
|
||||||
.replace(" ", "-");
|
platform,
|
||||||
|
partRole
|
||||||
|
);
|
||||||
|
|
||||||
return partCategoryRepository.findBySlug(normalizedSlug);
|
return mappingOpt.map(PartRoleMapping::getPartCategory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.repos.PartRoleMappingRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto;
|
||||||
|
import group.goforward.ballistic.web.dto.PartRoleToCategoryDto;
|
||||||
|
import group.goforward.ballistic.web.mapper.PartRoleMappingMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PartRoleMappingService {
|
||||||
|
|
||||||
|
private final PartRoleMappingRepository repository;
|
||||||
|
|
||||||
|
public PartRoleMappingService(PartRoleMappingRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PartRoleMappingDto> getMappingsForPlatform(String platform) {
|
||||||
|
return repository
|
||||||
|
.findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(platform)
|
||||||
|
.stream()
|
||||||
|
.map(PartRoleMappingMapper::toDto)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PartRoleToCategoryDto> getRoleToCategoryMap(String platform) {
|
||||||
|
return repository
|
||||||
|
.findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(platform)
|
||||||
|
.stream()
|
||||||
|
.map(PartRoleMappingMapper::toRoleMapDto)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.State;
|
import group.goforward.ballistic.model.State;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface StatesService {
|
public interface StatesService {
|
||||||
|
|
||||||
List<State> findAll();
|
List<State> findAll();
|
||||||
|
|
||||||
Optional<State> findById(Integer id);
|
Optional<State> findById(Integer id);
|
||||||
|
|
||||||
State save(State item);
|
State save(State item);
|
||||||
void deleteById(Integer id);
|
void deleteById(Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.User;
|
import group.goforward.ballistic.model.User;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UsersService {
|
public interface UsersService {
|
||||||
|
|
||||||
List<User> findAll();
|
List<User> findAll();
|
||||||
|
|
||||||
Optional<User> findById(Integer id);
|
Optional<User> findById(Integer id);
|
||||||
|
|
||||||
User save(User item);
|
User save(User item);
|
||||||
void deleteById(Integer id);
|
void deleteById(Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package group.goforward.ballistic.services.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.User;
|
||||||
|
import group.goforward.ballistic.repos.UserRepository;
|
||||||
|
import group.goforward.ballistic.web.dto.admin.AdminUserDto;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AdminUserService {
|
||||||
|
|
||||||
|
private static final Set<String> ALLOWED_ROLES = Set.of("USER", "ADMIN");
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public AdminUserService(UserRepository userRepository) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AdminUserDto> getAllUsersForAdmin() {
|
||||||
|
return userRepository.findAll()
|
||||||
|
.stream()
|
||||||
|
.map(AdminUserDto::fromUser)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public AdminUserDto updateUserRole(UUID userUuid, String newRole, Authentication auth) {
|
||||||
|
if (newRole == null || !ALLOWED_ROLES.contains(newRole.toUpperCase())) {
|
||||||
|
throw new IllegalArgumentException("Invalid role: " + newRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userRepository.findByUuid(userUuid)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||||
|
|
||||||
|
// Optional safety: do not allow demoting yourself (you can loosen this later)
|
||||||
|
String currentEmail = auth != null ? auth.getName() : null;
|
||||||
|
boolean isSelf = currentEmail != null
|
||||||
|
&& currentEmail.equalsIgnoreCase(user.getEmail());
|
||||||
|
|
||||||
|
if (isSelf && !"ADMIN".equalsIgnoreCase(newRole)) {
|
||||||
|
throw new IllegalStateException("You cannot change your own role to non-admin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setRole(newRole.toUpperCase());
|
||||||
|
// updatedAt will be handled by your entity / DB defaults
|
||||||
|
|
||||||
|
return AdminUserDto.fromUser(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,38 @@
|
|||||||
package group.goforward.ballistic.services.impl;
|
package group.goforward.ballistic.services.impl;
|
||||||
|
|
||||||
|
|
||||||
import group.goforward.ballistic.model.Brand;
|
import group.goforward.ballistic.model.Brand;
|
||||||
import group.goforward.ballistic.repos.BrandRepository;
|
import group.goforward.ballistic.repos.BrandRepository;
|
||||||
import group.goforward.ballistic.services.BrandService;
|
import group.goforward.ballistic.services.BrandService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class BrandServiceImpl implements BrandService {
|
public class BrandServiceImpl implements BrandService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BrandRepository repo;
|
private BrandRepository repo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Brand> findAll() {
|
public List<Brand> findAll() {
|
||||||
return repo.findAll();
|
return repo.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Brand> findById(Integer id) {
|
public Optional<Brand> findById(Integer id) {
|
||||||
return repo.findById(id);
|
return repo.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Brand save(Brand item) {
|
public Brand save(Brand item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteById(Integer id) {
|
public void deleteById(Integer id) {
|
||||||
deleteById(id);
|
deleteById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,38 @@
|
|||||||
package group.goforward.ballistic.services.impl;
|
package group.goforward.ballistic.services.impl;
|
||||||
|
|
||||||
|
|
||||||
import group.goforward.ballistic.model.State;
|
import group.goforward.ballistic.model.State;
|
||||||
import group.goforward.ballistic.repos.StateRepository;
|
import group.goforward.ballistic.repos.StateRepository;
|
||||||
import group.goforward.ballistic.services.StatesService;
|
import group.goforward.ballistic.services.StatesService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class StatesServiceImpl implements StatesService {
|
public class StatesServiceImpl implements StatesService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private StateRepository repo;
|
private StateRepository repo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<State> findAll() {
|
public List<State> findAll() {
|
||||||
return repo.findAll();
|
return repo.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<State> findById(Integer id) {
|
public Optional<State> findById(Integer id) {
|
||||||
return repo.findById(id);
|
return repo.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public State save(State item) {
|
public State save(State item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteById(Integer id) {
|
public void deleteById(Integer id) {
|
||||||
deleteById(id);
|
deleteById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
package group.goforward.ballistic.services.impl;
|
package group.goforward.ballistic.services.impl;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.User;
|
import group.goforward.ballistic.model.User;
|
||||||
import group.goforward.ballistic.repos.UserRepository;
|
import group.goforward.ballistic.repos.UserRepository;
|
||||||
import group.goforward.ballistic.services.UsersService;
|
import group.goforward.ballistic.services.UsersService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UsersServiceImpl implements UsersService {
|
public class UsersServiceImpl implements UsersService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository repo;
|
private UserRepository repo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
return repo.findAll();
|
return repo.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> findById(Integer id) {
|
public Optional<User> findById(Integer id) {
|
||||||
return repo.findById(id);
|
return repo.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User save(User item) {
|
public User save(User item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteById(Integer id) {
|
public void deleteById(Integer id) {
|
||||||
deleteById(id);
|
deleteById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Provides the classes necessary for the Spring Services implementations for the ballistic -Builder application.
|
* Provides the classes necessary for the Spring Services implementations for the ballistic -Builder application.
|
||||||
* This package includes Services implementations for Spring-Boot application
|
* This package includes Services implementations for Spring-Boot application
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* <p>The main entry point for managing the inventory is the
|
* <p>The main entry point for managing the inventory is the
|
||||||
* {@link group.goforward.ballistic.BallisticApplication} class.</p>
|
* {@link group.goforward.ballistic.BattlBuilderApplication} class.</p>
|
||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
* @author Don Strawsburg
|
* @author Don Strawsburg
|
||||||
* @version 1.1
|
* @version 1.1
|
||||||
*/
|
*/
|
||||||
package group.goforward.ballistic.services.impl;
|
package group.goforward.ballistic.services.impl;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package group.goforward.ballistic.web;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.GunbuilderProductService;
|
||||||
|
import group.goforward.ballistic.web.dto.GunbuilderProductDto;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/gunbuilder")
|
||||||
|
public class GunbuilderProductController {
|
||||||
|
|
||||||
|
private final GunbuilderProductService gunbuilderProductService;
|
||||||
|
|
||||||
|
public GunbuilderProductController(GunbuilderProductService gunbuilderProductService) {
|
||||||
|
this.gunbuilderProductService = gunbuilderProductService;
|
||||||
|
System.out.println(">>> GunbuilderProductController initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 sanity check: is this controller even mapped?
|
||||||
|
@GetMapping("/ping")
|
||||||
|
public Map<String, String> ping() {
|
||||||
|
return Map.of("status", "ok", "source", "GunbuilderProductController");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 super-dumb test: no DB, no service, just prove the route works
|
||||||
|
@GetMapping("/test-products")
|
||||||
|
public Map<String, Object> testProducts(@RequestParam String platform) {
|
||||||
|
System.out.println(">>> /api/gunbuilder/test-products hit for platform=" + platform);
|
||||||
|
|
||||||
|
Map<String, Object> m = new java.util.HashMap<>();
|
||||||
|
m.put("platform", platform);
|
||||||
|
m.put("note", "test endpoint only");
|
||||||
|
m.put("ok", true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List products for the builder UI.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* GET /api/gunbuilder/products?platform=AR-15
|
||||||
|
* GET /api/gunbuilder/products?platform=AR-15&partRoles=LOWER_RECEIVER_STRIPPED&partRoles=LOWER_RECEIVER_COMPLETE
|
||||||
|
*/
|
||||||
|
@GetMapping("/products")
|
||||||
|
public List<GunbuilderProductDto> listProducts(
|
||||||
|
@RequestParam String platform,
|
||||||
|
@RequestParam(required = false) List<String> partRoles
|
||||||
|
) {
|
||||||
|
return gunbuilderProductService.listGunbuilderProducts(platform, partRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/products/by-category")
|
||||||
|
public List<GunbuilderProductDto> listProductsByCategory(
|
||||||
|
@RequestParam String platform,
|
||||||
|
@RequestParam String categorySlug
|
||||||
|
) {
|
||||||
|
return gunbuilderProductService.listGunbuilderProductsByCategory(platform, categorySlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 DB test: hit repo via service and return a tiny view of products
|
||||||
|
@GetMapping("/test-products-db")
|
||||||
|
public List<Map<String, Object>> testProductsDb(@RequestParam String platform) {
|
||||||
|
System.out.println(">>> /api/gunbuilder/test-products-db hit for platform=" + platform);
|
||||||
|
|
||||||
|
var products = gunbuilderProductService.getSampleProducts(platform);
|
||||||
|
|
||||||
|
return products.stream()
|
||||||
|
.map(p -> {
|
||||||
|
Map<String, Object> m = new java.util.HashMap<>();
|
||||||
|
m.put("id", p.getId());
|
||||||
|
m.put("name", p.getName());
|
||||||
|
m.put("brand", p.getBrand() != null ? p.getBrand().getName() : null);
|
||||||
|
m.put("partRole", p.getPartRole());
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package group.goforward.ballistic.web;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.PartRoleMappingService;
|
||||||
|
import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto;
|
||||||
|
import group.goforward.ballistic.web.dto.PartRoleToCategoryDto;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/part-role-mappings")
|
||||||
|
public class PartRoleMappingController {
|
||||||
|
|
||||||
|
private final PartRoleMappingService service;
|
||||||
|
|
||||||
|
public PartRoleMappingController(PartRoleMappingService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full view for admin UI
|
||||||
|
@GetMapping("/{platform}")
|
||||||
|
public List<PartRoleMappingDto> getMappings(@PathVariable String platform) {
|
||||||
|
return service.getMappingsForPlatform(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thin mapping for the builder
|
||||||
|
@GetMapping("/{platform}/map")
|
||||||
|
public List<PartRoleToCategoryDto> getRoleMap(@PathVariable String platform) {
|
||||||
|
return service.getRoleToCategoryMap(platform);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package group.goforward.ballistic.web.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
import group.goforward.ballistic.repos.ProductRepository;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/import-status")
|
||||||
|
public class AdminImportStatusController {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
|
||||||
|
public AdminImportStatusController(ProductRepository productRepository) {
|
||||||
|
this.productRepository = productRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ImportSummaryDto(
|
||||||
|
long totalProducts,
|
||||||
|
long mappedProducts,
|
||||||
|
long pendingProducts
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record ByMerchantRowDto(
|
||||||
|
Integer merchantId,
|
||||||
|
String merchantName,
|
||||||
|
String platform,
|
||||||
|
ImportStatus status,
|
||||||
|
long count
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@GetMapping("/summary")
|
||||||
|
public ImportSummaryDto summary() {
|
||||||
|
List<Map<String, Object>> rows = productRepository.aggregateByImportStatus();
|
||||||
|
|
||||||
|
long total = 0L;
|
||||||
|
long mapped = 0L;
|
||||||
|
long pending = 0L;
|
||||||
|
|
||||||
|
for (Map<String, Object> row : rows) {
|
||||||
|
ImportStatus status = (ImportStatus) row.get("status");
|
||||||
|
long count = ((Number) row.get("count")).longValue();
|
||||||
|
total += count;
|
||||||
|
|
||||||
|
if (status == ImportStatus.MAPPED) {
|
||||||
|
mapped += count;
|
||||||
|
} else if (status == ImportStatus.PENDING_MAPPING) {
|
||||||
|
pending += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImportSummaryDto(total, mapped, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/by-merchant")
|
||||||
|
public List<ByMerchantRowDto> byMerchant() {
|
||||||
|
List<Map<String, Object>> rows = productRepository.aggregateByMerchantAndStatus();
|
||||||
|
|
||||||
|
return rows.stream()
|
||||||
|
.map(row -> new ByMerchantRowDto(
|
||||||
|
(Integer) row.get("merchantId"),
|
||||||
|
(String) row.get("merchantName"),
|
||||||
|
(String) row.get("platform"),
|
||||||
|
(ImportStatus) row.get("status"),
|
||||||
|
((Number) row.get("count")).longValue()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package group.goforward.ballistic.web.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.MappingAdminService;
|
||||||
|
import group.goforward.ballistic.web.dto.PendingMappingBucketDto;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/mapping")
|
||||||
|
public class AdminMappingController {
|
||||||
|
|
||||||
|
private final MappingAdminService mappingAdminService;
|
||||||
|
|
||||||
|
public AdminMappingController(MappingAdminService mappingAdminService) {
|
||||||
|
this.mappingAdminService = mappingAdminService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pending-buckets")
|
||||||
|
public List<PendingMappingBucketDto> listPendingBuckets() {
|
||||||
|
// Simple: just delegate to service
|
||||||
|
return mappingAdminService.listPendingBuckets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApplyMappingRequest(
|
||||||
|
Integer merchantId,
|
||||||
|
String rawCategoryKey,
|
||||||
|
String mappedPartRole
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@PostMapping("/apply")
|
||||||
|
public ResponseEntity<Map<String, Object>> applyMapping(
|
||||||
|
@RequestBody ApplyMappingRequest request
|
||||||
|
) {
|
||||||
|
mappingAdminService.applyMapping(
|
||||||
|
request.merchantId(),
|
||||||
|
request.rawCategoryKey(),
|
||||||
|
request.mappedPartRole()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package group.goforward.ballistic.web.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.MerchantFeedImportService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/merchants")
|
||||||
|
public class AdminMerchantController {
|
||||||
|
|
||||||
|
private final MerchantFeedImportService merchantFeedImportService;
|
||||||
|
|
||||||
|
public AdminMerchantController(MerchantFeedImportService merchantFeedImportService) {
|
||||||
|
this.merchantFeedImportService = merchantFeedImportService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{merchantId}/import")
|
||||||
|
public ResponseEntity<Map<String, Object>> triggerFullImport(
|
||||||
|
@PathVariable Integer merchantId
|
||||||
|
) {
|
||||||
|
// Fire off the full import for this merchant.
|
||||||
|
// (Right now this is synchronous; later we can push to a queue if needed.)
|
||||||
|
merchantFeedImportService.importMerchantFeed(merchantId);
|
||||||
|
|
||||||
|
return ResponseEntity.accepted().body(
|
||||||
|
Map.of(
|
||||||
|
"ok", true,
|
||||||
|
"merchantId", merchantId,
|
||||||
|
"message", "Import triggered"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package group.goforward.ballistic.web.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.admin.AdminUserService;
|
||||||
|
import group.goforward.ballistic.web.dto.admin.AdminUserDto;
|
||||||
|
import group.goforward.ballistic.web.dto.admin.UpdateUserRoleRequest;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/users")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public class AdminUserController {
|
||||||
|
|
||||||
|
private final AdminUserService adminUserService;
|
||||||
|
|
||||||
|
public AdminUserController(AdminUserService adminUserService) {
|
||||||
|
this.adminUserService = adminUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<AdminUserDto> listUsers() {
|
||||||
|
return adminUserService.getAllUsersForAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/{uuid}/role")
|
||||||
|
public AdminUserDto updateRole(
|
||||||
|
@PathVariable("uuid") UUID uuid,
|
||||||
|
@RequestBody UpdateUserRoleRequest request,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
return adminUserService.updateUserRole(uuid, request.getRole(), auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package group.goforward.ballistic.web.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.services.MappingAdminService;
|
||||||
|
import group.goforward.ballistic.web.dto.PendingMappingBucketDto;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/mappings")
|
||||||
|
public class CategoryMappingAdminController {
|
||||||
|
|
||||||
|
private final MappingAdminService mappingAdminService;
|
||||||
|
|
||||||
|
public CategoryMappingAdminController(MappingAdminService mappingAdminService) {
|
||||||
|
this.mappingAdminService = mappingAdminService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pending")
|
||||||
|
public List<PendingMappingBucketDto> listPending() {
|
||||||
|
return mappingAdminService.listPendingBuckets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApplyMappingRequest(
|
||||||
|
Integer merchantId,
|
||||||
|
String rawCategoryKey,
|
||||||
|
String mappedPartRole
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@PostMapping("/apply")
|
||||||
|
public ResponseEntity<Void> apply(@RequestBody ApplyMappingRequest request) {
|
||||||
|
mappingAdminService.applyMapping(
|
||||||
|
request.merchantId(),
|
||||||
|
request.rawCategoryKey(),
|
||||||
|
request.mappedPartRole()
|
||||||
|
);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @RestController
|
||||||
|
// @RequestMapping("/api/admin/mapping")
|
||||||
|
// public static class AdminMappingController {
|
||||||
|
//
|
||||||
|
// private final MappingAdminService mappingAdminService;
|
||||||
|
//
|
||||||
|
// public AdminMappingController(MappingAdminService mappingAdminService) {
|
||||||
|
// this.mappingAdminService = mappingAdminService;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @GetMapping("/pending-buckets")
|
||||||
|
// public List<PendingMappingBucketDto> listPendingBuckets() {
|
||||||
|
// return mappingAdminService.listPendingBuckets();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @PostMapping("/apply")
|
||||||
|
// public ResponseEntity<?> applyMapping(@RequestBody Map<String, Object> body) {
|
||||||
|
// Integer merchantId = (Integer) body.get("merchantId");
|
||||||
|
// String rawCategoryKey = (String) body.get("rawCategoryKey");
|
||||||
|
// String mappedPartRole = (String) body.get("mappedPartRole");
|
||||||
|
//
|
||||||
|
// mappingAdminService.applyMapping(merchantId, rawCategoryKey, mappedPartRole);
|
||||||
|
// return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/web/dto/CategoryMappingRecommendationDto.java
|
||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
public record CategoryMappingRecommendationDto(
|
||||||
|
String merchantName,
|
||||||
|
String rawCategoryKey,
|
||||||
|
long productCount,
|
||||||
|
String recommendedPartRole,
|
||||||
|
String sampleProductName
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/web/dto/ImportStatusByMerchantDto.java
|
||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
|
||||||
|
public record ImportStatusByMerchantDto(
|
||||||
|
String merchantName,
|
||||||
|
String platform,
|
||||||
|
ImportStatus status,
|
||||||
|
long count
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/web/dto/ImportStatusSummaryDto.java
|
||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.ImportStatus;
|
||||||
|
|
||||||
|
public record ImportStatusSummaryDto(
|
||||||
|
ImportStatus status,
|
||||||
|
long count
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
public record PartRoleToCategoryDto(
|
||||||
|
String platform,
|
||||||
|
String partRole,
|
||||||
|
String categorySlug // e.g. "lower", "barrel", "optic"
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
public record PendingMappingBucketDto(
|
||||||
|
Integer merchantId,
|
||||||
|
String merchantName,
|
||||||
|
String rawCategoryKey,
|
||||||
|
String mappedPartRole,
|
||||||
|
long productCount
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package group.goforward.ballistic.web.dto;
|
||||||
|
|
||||||
|
public class AdminDashboardOverviewDto {
|
||||||
|
|
||||||
|
private long totalProducts;
|
||||||
|
private long mappedProducts;
|
||||||
|
private long unmappedProducts;
|
||||||
|
private long merchantCount;
|
||||||
|
private long categoryMappingCount;
|
||||||
|
|
||||||
|
public AdminDashboardOverviewDto(
|
||||||
|
long totalProducts,
|
||||||
|
long mappedProducts,
|
||||||
|
long unmappedProducts,
|
||||||
|
long merchantCount,
|
||||||
|
long categoryMappingCount
|
||||||
|
) {
|
||||||
|
this.totalProducts = totalProducts;
|
||||||
|
this.mappedProducts = mappedProducts;
|
||||||
|
this.unmappedProducts = unmappedProducts;
|
||||||
|
this.merchantCount = merchantCount;
|
||||||
|
this.categoryMappingCount = categoryMappingCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalProducts() {
|
||||||
|
return totalProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMappedProducts() {
|
||||||
|
return mappedProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUnmappedProducts() {
|
||||||
|
return unmappedProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMerchantCount() {
|
||||||
|
return merchantCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCategoryMappingCount() {
|
||||||
|
return categoryMappingCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.User;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class AdminUserDto {
|
||||||
|
|
||||||
|
// We'll expose the UUID as the "id" used by the frontend
|
||||||
|
private UUID id;
|
||||||
|
private String email;
|
||||||
|
private String displayName;
|
||||||
|
private String role;
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
private OffsetDateTime updatedAt;
|
||||||
|
private OffsetDateTime lastLoginAt;
|
||||||
|
|
||||||
|
public AdminUserDto(UUID id,
|
||||||
|
String email,
|
||||||
|
String displayName,
|
||||||
|
String role,
|
||||||
|
OffsetDateTime createdAt,
|
||||||
|
OffsetDateTime updatedAt,
|
||||||
|
OffsetDateTime lastLoginAt) {
|
||||||
|
this.id = id;
|
||||||
|
this.email = email;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.role = role;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
this.lastLoginAt = lastLoginAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AdminUserDto fromUser(User user) {
|
||||||
|
return new AdminUserDto(
|
||||||
|
user.getUuid(), // use UUID here (stable id)
|
||||||
|
user.getEmail(),
|
||||||
|
user.getDisplayName(),
|
||||||
|
user.getRole(), // String: "USER" / "ADMIN"
|
||||||
|
user.getCreatedAt(),
|
||||||
|
user.getUpdatedAt(),
|
||||||
|
user.getLastLoginAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters (and setters if you want Jackson to use them)
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getLastLoginAt() {
|
||||||
|
return lastLoginAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ public record PartRoleMappingDto(
|
|||||||
Integer id,
|
Integer id,
|
||||||
String platform,
|
String platform,
|
||||||
String partRole,
|
String partRole,
|
||||||
String categorySlug,
|
Integer partCategoryId,
|
||||||
String groupName,
|
String partCategorySlug,
|
||||||
|
String partCategoryName,
|
||||||
String notes
|
String notes
|
||||||
) {}
|
) {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
public class UpdateUserRoleRequest {
|
||||||
|
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
public UpdateUserRoleRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpdateUserRoleRequest(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package group.goforward.ballistic.web.mapper;
|
||||||
|
|
||||||
|
import group.goforward.ballistic.model.PartCategory;
|
||||||
|
import group.goforward.ballistic.model.PartRoleMapping;
|
||||||
|
import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto;
|
||||||
|
import group.goforward.ballistic.web.dto.PartRoleToCategoryDto;
|
||||||
|
|
||||||
|
public final class PartRoleMappingMapper {
|
||||||
|
|
||||||
|
private PartRoleMappingMapper() {
|
||||||
|
// utility class
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PartRoleMappingDto toDto(PartRoleMapping entity) {
|
||||||
|
PartCategory cat = entity.getPartCategory();
|
||||||
|
|
||||||
|
return new PartRoleMappingDto(
|
||||||
|
entity.getId(),
|
||||||
|
entity.getPlatform(),
|
||||||
|
entity.getPartRole(),
|
||||||
|
cat != null ? cat.getId() : null,
|
||||||
|
cat != null ? cat.getSlug() : null,
|
||||||
|
cat != null ? cat.getName() : null,
|
||||||
|
entity.getNotes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PartRoleToCategoryDto toRoleMapDto(PartRoleMapping entity) {
|
||||||
|
PartCategory cat = entity.getPartCategory();
|
||||||
|
|
||||||
|
return new PartRoleToCategoryDto(
|
||||||
|
entity.getPlatform(),
|
||||||
|
entity.getPartRole(),
|
||||||
|
cat != null ? cat.getSlug() : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
spring.application.name=ballistic
|
spring.application.name=BattlBuilderAPI
|
||||||
# Database connection properties
|
# Database connection properties
|
||||||
spring.datasource.url=jdbc:postgresql://r710.gofwd.group:5433/ss_builder
|
spring.datasource.url=jdbc:postgresql://r710.gofwd.group:5433/ss_builder
|
||||||
spring.datasource.username=postgres
|
spring.datasource.username=postgres
|
||||||
@@ -7,8 +7,16 @@ spring.datasource.driver-class-name=org.postgresql.Driver
|
|||||||
|
|
||||||
# Hibernate properties
|
# Hibernate properties
|
||||||
#spring.jpa.hibernate.ddl-auto=update
|
#spring.jpa.hibernate.ddl-auto=update
|
||||||
spring.jpa.show-sql=true
|
#spring.jpa.show-sql=true
|
||||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
|
||||||
security.jwt.secret=ballistic-test-secret-key-1234567890-ABCDEFGHIJKLNMOPQRST
|
security.jwt.secret=ballistic-test-secret-key-1234567890-ABCDEFGHIJKLNMOPQRST
|
||||||
security.jwt.access-token-minutes=2880
|
security.jwt.access-token-minutes=2880
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
|
||||||
|
spring.jpa.show-sql=true
|
||||||
|
logging.level.org.hibernate.SQL=INFO
|
||||||
|
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=warn
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user