From 5e3f7d5044019fc33e3e3f34bbe6c036d374a671 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 3 Dec 2025 19:13:43 -0500 Subject: [PATCH 1/2] expanded category grouping from db --- .../admin/AdminCategoryController.java | 40 +++++++++++ .../ballistic/model/AffiliateCategoryMap.java | 53 +++++++++----- .../ballistic/model/PartCategory.java | 70 ++++++++++++++++++- .../model/PartRoleCategoryMapping.java | 56 +++++++++++++++ .../goforward/ballistic/model/Product.java | 54 +++++++++++++- .../ballistic/model/ProductOffer.java | 36 +++++++--- .../repos/CategoryMappingRepository.java | 13 ++++ .../repos/PartCategoryRepository.java | 5 ++ .../PartRoleCategoryMappingRepository.java | 14 ++++ .../ballistic/repos/ProductRepository.java | 39 +++++++---- .../services/GunbuilderProductService.java | 57 +++++++++++++++ .../services/PartCategoryResolverService.java | 38 ++++++++++ .../web/dto/GunbuilderProductDto.java | 53 ++++++++++++++ .../web/dto/admin/PartCategoryDto.java | 35 ++++++++++ .../web/dto/admin/PartRoleMappingDto.java | 69 ++++++++++++++++++ 15 files changed, 584 insertions(+), 48 deletions(-) create mode 100644 src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java create mode 100644 src/main/java/group/goforward/ballistic/model/PartRoleCategoryMapping.java create mode 100644 src/main/java/group/goforward/ballistic/repos/PartRoleCategoryMappingRepository.java create mode 100644 src/main/java/group/goforward/ballistic/services/GunbuilderProductService.java create mode 100644 src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/GunbuilderProductDto.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java new file mode 100644 index 0000000..a5603ab --- /dev/null +++ b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java @@ -0,0 +1,40 @@ +package group.goforward.ballistic.controllers.admin; + +import group.goforward.ballistic.model.PartCategory; +import group.goforward.ballistic.repos.PartCategoryRepository; +import group.goforward.ballistic.web.dto.admin.PartCategoryDto; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/categories") +@CrossOrigin +public class AdminCategoryController { + + private final PartCategoryRepository partCategories; + + public AdminCategoryController(PartCategoryRepository partCategories) { + this.partCategories = partCategories; + } + + @GetMapping + public List listCategories() { + return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc() + .stream() + .map(this::toDto) + .toList(); + } + + private PartCategoryDto toDto(PartCategory entity) { + PartCategoryDto dto = new PartCategoryDto(); + dto.setId(entity.getId()); + dto.setSlug(entity.getSlug()); + dto.setName(entity.getName()); + dto.setDescription(entity.getDescription()); + dto.setGroupName(entity.getGroupName()); + dto.setSortOrder(entity.getSortOrder()); + dto.setUuid(entity.getUuid()); + return dto; + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java b/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java index 3e623dc..d3e6cf4 100644 --- a/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java +++ b/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java @@ -5,19 +5,27 @@ import jakarta.persistence.*; @Entity @Table(name = "affiliate_category_map") public class AffiliateCategoryMap { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Integer id; - @Column(name = "feedname", nullable = false, length = 100) - private String feedname; + // e.g. "PART_ROLE" + @Column(name = "source_type", nullable = false) + private String sourceType; - @Column(name = "affiliatecategory", nullable = false) - private String affiliatecategory; + // e.g. "suppressor" + @Column(name = "source_value", nullable = false) + private String sourceValue; - @Column(name = "buildercategoryid", nullable = false) - private Integer buildercategoryid; + // e.g. "AR-15", nullable + @Column(name = "platform") + private String platform; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "part_category_id", nullable = false) + private PartCategory partCategory; @Column(name = "notes") private String notes; @@ -30,28 +38,36 @@ public class AffiliateCategoryMap { this.id = id; } - public String getFeedname() { - return feedname; + public String getSourceType() { + return sourceType; } - public void setFeedname(String feedname) { - this.feedname = feedname; + public void setSourceType(String sourceType) { + this.sourceType = sourceType; } - public String getAffiliatecategory() { - return affiliatecategory; + public String getSourceValue() { + return sourceValue; } - public void setAffiliatecategory(String affiliatecategory) { - this.affiliatecategory = affiliatecategory; + public void setSourceValue(String sourceValue) { + this.sourceValue = sourceValue; } - public Integer getBuildercategoryid() { - return buildercategoryid; + public String getPlatform() { + return platform; } - public void setBuildercategoryid(Integer buildercategoryid) { - this.buildercategoryid = buildercategoryid; + public void setPlatform(String platform) { + this.platform = platform; + } + + public PartCategory getPartCategory() { + return partCategory; + } + + public void setPartCategory(PartCategory partCategory) { + this.partCategory = partCategory; } public String getNotes() { @@ -61,5 +77,4 @@ public class AffiliateCategoryMap { public void setNotes(String notes) { this.notes = notes; } - } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/model/PartCategory.java b/src/main/java/group/goforward/ballistic/model/PartCategory.java index 79cd38d..a129b37 100644 --- a/src/main/java/group/goforward/ballistic/model/PartCategory.java +++ b/src/main/java/group/goforward/ballistic/model/PartCategory.java @@ -1,24 +1,49 @@ package group.goforward.ballistic.model; import jakarta.persistence.*; +import org.hibernate.annotations.ColumnDefault; + +import java.time.OffsetDateTime; +import java.util.UUID; @Entity @Table(name = "part_categories") public class PartCategory { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Integer id; - @Column(name = "slug", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "slug", nullable = false, unique = true) private String slug; - @Column(name = "name", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "name", nullable = false) private String name; - @Column(name = "description", length = Integer.MAX_VALUE) + @Column(name = "description") private String description; + @ColumnDefault("gen_random_uuid()") + @Column(name = "uuid", nullable = false) + private UUID uuid; + + @Column(name = "group_name") + private String groupName; + + @Column(name = "sort_order") + private Integer sortOrder; + + @ColumnDefault("now()") + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @ColumnDefault("now()") + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; + + // --- Getters & Setters --- + public Integer getId() { return id; } @@ -51,4 +76,43 @@ public class PartCategory { this.description = description; } + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/model/PartRoleCategoryMapping.java b/src/main/java/group/goforward/ballistic/model/PartRoleCategoryMapping.java new file mode 100644 index 0000000..07bdea8 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/model/PartRoleCategoryMapping.java @@ -0,0 +1,56 @@ +package group.goforward.ballistic.model; + +import jakarta.persistence.*; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "part_role_category_mappings", + uniqueConstraints = @UniqueConstraint(columnNames = {"platform", "part_role"})) +public class PartRoleCategoryMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "platform", nullable = false) + private String platform; + + @Column(name = "part_role", nullable = false) + private String partRole; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_slug", referencedColumnName = "slug", nullable = false) + private PartCategory category; + + @Column(name = "notes") + private String notes; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private OffsetDateTime updatedAt; + + // getters/setters… + + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + + 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 PartCategory getCategory() { return category; } + public void setCategory(PartCategory category) { this.category = category; } + + public String getNotes() { return notes; } + public void setNotes(String notes) { this.notes = notes; } + + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + + public OffsetDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/model/Product.java b/src/main/java/group/goforward/ballistic/model/Product.java index 785f928..90cbbaa 100644 --- a/src/main/java/group/goforward/ballistic/model/Product.java +++ b/src/main/java/group/goforward/ballistic/model/Product.java @@ -3,7 +3,13 @@ package group.goforward.ballistic.model; import jakarta.persistence.*; import java.time.Instant; import java.util.UUID; +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.Objects; +import java.util.Set; +import java.util.HashSet; +import group.goforward.ballistic.model.ProductOffer; import group.goforward.ballistic.model.ProductConfiguration; @Entity @@ -68,7 +74,16 @@ public class Product { @Column(name = "platform_locked", nullable = false) private Boolean platformLocked = false; - + @OneToMany(mappedBy = "product", fetch = FetchType.LAZY) + private Set offers = new HashSet<>(); + + public Set getOffers() { + return offers; + } + + public void setOffers(Set offers) { + this.offers = offers; + } // --- lifecycle hooks --- @@ -236,4 +251,41 @@ public class Product { public void setConfiguration(ProductConfiguration configuration) { this.configuration = configuration; } + // Convenience: best offer price for Gunbuilder +public BigDecimal getBestOfferPrice() { + if (offers == null || offers.isEmpty()) { + return BigDecimal.ZERO; + } + + return offers.stream() + // pick sale_price if present, otherwise retail_price + .map(offer -> { + if (offer.getSalePrice() != null) { + return offer.getSalePrice(); + } + return offer.getRetailPrice(); + }) + .filter(Objects::nonNull) + .min(BigDecimal::compareTo) + .orElse(BigDecimal.ZERO); +} + + // Convenience: URL for the best-priced offer + public String getBestOfferBuyUrl() { + if (offers == null || offers.isEmpty()) { + return null; + } + + return offers.stream() + .sorted(Comparator.comparing(offer -> { + if (offer.getSalePrice() != null) { + return offer.getSalePrice(); + } + return offer.getRetailPrice(); + }, Comparator.nullsLast(BigDecimal::compareTo))) + .map(ProductOffer::getAffiliateUrl) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } } diff --git a/src/main/java/group/goforward/ballistic/model/ProductOffer.java b/src/main/java/group/goforward/ballistic/model/ProductOffer.java index d91f32b..dace70a 100644 --- a/src/main/java/group/goforward/ballistic/model/ProductOffer.java +++ b/src/main/java/group/goforward/ballistic/model/ProductOffer.java @@ -7,11 +7,11 @@ import org.hibernate.annotations.OnDeleteAction; import java.math.BigDecimal; import java.time.OffsetDateTime; -import java.util.UUID; @Entity @Table(name = "product_offers") public class ProductOffer { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) @@ -26,16 +26,16 @@ public class ProductOffer { @JoinColumn(name = "merchant_id", nullable = false) private Merchant merchant; - @Column(name = "avantlink_product_id", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "avantlink_product_id", nullable = false) private String avantlinkProductId; - @Column(name = "sku", length = Integer.MAX_VALUE) + @Column(name = "sku") private String sku; - @Column(name = "upc", length = Integer.MAX_VALUE) + @Column(name = "upc") private String upc; - @Column(name = "buy_url", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "buy_url", nullable = false) private String buyUrl; @Column(name = "price", nullable = false, precision = 10, scale = 2) @@ -45,7 +45,7 @@ public class ProductOffer { private BigDecimal originalPrice; @ColumnDefault("'USD'") - @Column(name = "currency", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "currency", nullable = false) private String currency; @ColumnDefault("true") @@ -60,6 +60,10 @@ public class ProductOffer { @Column(name = "first_seen_at", nullable = false) private OffsetDateTime firstSeenAt; + // ----------------------------------------------------- + // Getters & setters + // ----------------------------------------------------- + public Integer getId() { return id; } @@ -164,14 +168,26 @@ public class ProductOffer { this.firstSeenAt = firstSeenAt; } + // ----------------------------------------------------- + // Helper Methods (used by Product entity) + // ----------------------------------------------------- + + public BigDecimal getSalePrice() { + return price; + } + + public BigDecimal getRetailPrice() { + return originalPrice != null ? originalPrice : price; + } + + public String getAffiliateUrl() { + return buyUrl; + } + public BigDecimal getEffectivePrice() { - // Prefer a true sale price when it's lower than the original if (price != null && originalPrice != null && price.compareTo(originalPrice) < 0) { return price; } - - // Otherwise, use whatever is available return price != null ? price : originalPrice; } - } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java b/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java index cbaa5ee..cbdbc13 100644 --- a/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java @@ -3,5 +3,18 @@ package group.goforward.ballistic.repos; import group.goforward.ballistic.model.AffiliateCategoryMap; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CategoryMappingRepository extends JpaRepository { + + Optional findBySourceTypeAndSourceValueAndPlatformIgnoreCase( + String sourceType, + String sourceValue, + String platform + ); + + Optional findBySourceTypeAndSourceValueIgnoreCase( + String sourceType, + String sourceValue + ); } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java b/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java index 32e41af..aff326e 100644 --- a/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java @@ -2,8 +2,13 @@ package group.goforward.ballistic.repos; import group.goforward.ballistic.model.PartCategory; import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; import java.util.Optional; public interface PartCategoryRepository extends JpaRepository { + Optional findBySlug(String slug); + + List findAllByOrderByGroupNameAscSortOrderAscNameAsc(); } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/repos/PartRoleCategoryMappingRepository.java b/src/main/java/group/goforward/ballistic/repos/PartRoleCategoryMappingRepository.java new file mode 100644 index 0000000..eef8305 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/repos/PartRoleCategoryMappingRepository.java @@ -0,0 +1,14 @@ +package group.goforward.ballistic.repos; + +import group.goforward.ballistic.model.PartRoleCategoryMapping; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface PartRoleCategoryMappingRepository extends JpaRepository { + + List findAllByPlatformOrderByPartRoleAsc(String platform); + + Optional findByPlatformAndPartRole(String platform, String partRole); +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/repos/ProductRepository.java b/src/main/java/group/goforward/ballistic/repos/ProductRepository.java index ff601f1..24f821c 100644 --- a/src/main/java/group/goforward/ballistic/repos/ProductRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/ProductRepository.java @@ -1,33 +1,28 @@ package group.goforward.ballistic.repos; -import group.goforward.ballistic.model.Product; import group.goforward.ballistic.model.Brand; +import group.goforward.ballistic.model.Product; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.Optional; -import java.util.UUID; import java.util.List; -import java.util.Collection; public interface ProductRepository extends JpaRepository { - Optional findByUuid(UUID uuid); - - boolean existsBySlug(String slug); + // ------------------------------------------------- + // Used by MerchantFeedImportServiceImpl + // ------------------------------------------------- List findAllByBrandAndMpn(Brand brand, String mpn); List findAllByBrandAndUpc(Brand brand, String upc); - // All products for a given platform (e.g. "AR-15") - List findByPlatform(String platform); + boolean existsBySlug(String slug); - // Products filtered by platform + part roles (e.g. upper-receiver, barrel, etc.) - List findByPlatformAndPartRoleIn(String platform, Collection partRoles); - - // ---------- Optimized variants for Gunbuilder (fetch brand to avoid N+1) ---------- + // ------------------------------------------------- + // Used by ProductController for platform views + // ------------------------------------------------- @Query(""" SELECT p @@ -43,11 +38,25 @@ public interface ProductRepository extends JpaRepository { FROM Product p JOIN FETCH p.brand b WHERE p.platform = :platform - AND p.partRole IN :partRoles + AND p.partRole IN :roles AND p.deletedAt IS NULL """) List findByPlatformAndPartRoleInWithBrand( @Param("platform") String platform, - @Param("partRoles") Collection partRoles + @Param("roles") List roles ); + + // ------------------------------------------------- + // Used by Gunbuilder service (if you wired this) + // ------------------------------------------------- + + @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.deletedAt IS NULL + """) + List findSomethingForGunbuilder(@Param("platform") String platform); } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/services/GunbuilderProductService.java b/src/main/java/group/goforward/ballistic/services/GunbuilderProductService.java new file mode 100644 index 0000000..1e074fe --- /dev/null +++ b/src/main/java/group/goforward/ballistic/services/GunbuilderProductService.java @@ -0,0 +1,57 @@ +package group.goforward.ballistic.services; + +import group.goforward.ballistic.model.PartCategory; +import group.goforward.ballistic.model.Product; +import group.goforward.ballistic.repos.ProductRepository; +import group.goforward.ballistic.web.dto.GunbuilderProductDto; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GunbuilderProductService { + + private final ProductRepository productRepository; + private final PartCategoryResolverService partCategoryResolverService; + + public GunbuilderProductService( + ProductRepository productRepository, + PartCategoryResolverService partCategoryResolverService + ) { + this.productRepository = productRepository; + this.partCategoryResolverService = partCategoryResolverService; + } + + public List listGunbuilderProducts(String platform) { + + List products = productRepository.findSomethingForGunbuilder(platform); + + return products.stream() + .map(p -> { + var maybeCategory = partCategoryResolverService + .resolveForPlatformAndPartRole(platform, p.getPartRole()); + + if (maybeCategory.isEmpty()) { + // you can also log here + return null; + } + + PartCategory cat = maybeCategory.get(); + + return new GunbuilderProductDto( + p.getId(), + p.getName(), + p.getBrand().getName(), + platform, + p.getPartRole(), + p.getBestOfferPrice(), + p.getMainImageUrl(), + p.getBestOfferBuyUrl(), + cat.getSlug(), + cat.getGroupName() + ); + }) + .filter(dto -> dto != null) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java b/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java new file mode 100644 index 0000000..252ad6f --- /dev/null +++ b/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java @@ -0,0 +1,38 @@ +package group.goforward.ballistic.services; + +import group.goforward.ballistic.model.AffiliateCategoryMap; +import group.goforward.ballistic.model.PartCategory; +import group.goforward.ballistic.repos.CategoryMappingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class PartCategoryResolverService { + + private final CategoryMappingRepository categoryMappingRepository; + + public PartCategoryResolverService(CategoryMappingRepository categoryMappingRepository) { + this.categoryMappingRepository = categoryMappingRepository; + } + + /** + * Resolve a part category from a platform + partRole (what gunbuilder cares about). + * Returns Optional.empty() if we have no mapping yet. + */ + public Optional resolveForPlatformAndPartRole(String platform, String partRole) { + // sourceType is a convention – you can also enum this + String sourceType = "PART_ROLE"; + + // First try with platform + return categoryMappingRepository + .findBySourceTypeAndSourceValueAndPlatformIgnoreCase(sourceType, partRole, platform) + .map(AffiliateCategoryMap::getPartCategory) + // if that fails, fall back to ANY platform + .or(() -> + categoryMappingRepository + .findBySourceTypeAndSourceValueIgnoreCase(sourceType, partRole) + .map(AffiliateCategoryMap::getPartCategory) + ); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/GunbuilderProductDto.java b/src/main/java/group/goforward/ballistic/web/dto/GunbuilderProductDto.java new file mode 100644 index 0000000..624f9dc --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/GunbuilderProductDto.java @@ -0,0 +1,53 @@ +package group.goforward.ballistic.web.dto; + +import java.math.BigDecimal; + +public class GunbuilderProductDto { + + private Integer id; + private String name; + private String brand; + private String platform; + private String partRole; + private BigDecimal price; + private String imageUrl; + private String buyUrl; + private String categorySlug; + private String categoryGroup; + + public GunbuilderProductDto( + Integer id, + String name, + String brand, + String platform, + String partRole, + BigDecimal price, + String imageUrl, + String buyUrl, + String categorySlug, + String categoryGroup + ) { + this.id = id; + this.name = name; + this.brand = brand; + this.platform = platform; + this.partRole = partRole; + this.price = price; + this.imageUrl = imageUrl; + this.buyUrl = buyUrl; + this.categorySlug = categorySlug; + this.categoryGroup = categoryGroup; + } + + // --- Getters only (DTOs are read-only in most cases) --- + public Integer getId() { return id; } + public String getName() { return name; } + public String getBrand() { return brand; } + public String getPlatform() { return platform; } + public String getPartRole() { return partRole; } + public BigDecimal getPrice() { return price; } + public String getImageUrl() { return imageUrl; } + public String getBuyUrl() { return buyUrl; } + public String getCategorySlug() { return categorySlug; } + public String getCategoryGroup() { return categoryGroup; } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java b/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java new file mode 100644 index 0000000..69ba09d --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java @@ -0,0 +1,35 @@ +package group.goforward.ballistic.web.dto.admin; + +import java.util.UUID; + +public class PartCategoryDto { + private Integer id; + private String slug; + private String name; + private String description; + private String groupName; + private Integer sortOrder; + private UUID uuid; + + // getters + setters + public Integer getId() { return id; } + public void setId(Integer id) { this.id = id; } + + public String getSlug() { return slug; } + public void setSlug(String slug) { this.slug = slug; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getGroupName() { return groupName; } + public void setGroupName(String groupName) { this.groupName = groupName; } + + public Integer getSortOrder() { return sortOrder; } + public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } + + public UUID getUuid() { return uuid; } + public void setUuid(UUID uuid) { this.uuid = uuid; } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java new file mode 100644 index 0000000..93756e4 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java @@ -0,0 +1,69 @@ +package group.goforward.ballistic.web.dto.admin; + +public class PartRoleMappingDto { + private Integer id; + private String platform; + private String partRole; + private String categorySlug; + private String categoryName; + private String groupName; + private String notes; + + // getters + setters... + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + 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 getCategorySlug() { + return categorySlug; + } + + public void setCategorySlug(String categorySlug) { + this.categorySlug = categorySlug; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } +} \ No newline at end of file From 74a5c42e261e8f6ece98667249ac4098b7ee16ad Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 3 Dec 2025 21:50:00 -0500 Subject: [PATCH 2/2] dynamic category mapping and updating from admin. --- .../admin/AdminCategoryController.java | 20 +-- .../admin/AdminCategoryMappingController.java | 125 ++++++++++++++++++ .../admin/PartCategoryAdminController.java | 35 +++++ .../ballistic/model/AffiliateCategoryMap.java | 9 +- .../repos/CategoryMappingRepository.java | 9 ++ .../web/dto/admin/PartCategoryDto.java | 41 ++---- .../web/dto/admin/PartRoleMappingDto.java | 75 ++--------- .../web/dto/admin/PartRoleMappingRequest.java | 8 ++ 8 files changed, 208 insertions(+), 114 deletions(-) create mode 100644 src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java create mode 100644 src/main/java/group/goforward/ballistic/controllers/admin/PartCategoryAdminController.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingRequest.java diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java index a5603ab..3f9be99 100644 --- a/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java +++ b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryController.java @@ -20,21 +20,21 @@ public class AdminCategoryController { @GetMapping public List listCategories() { - return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc() + return partCategories + .findAllByOrderByGroupNameAscSortOrderAscNameAsc() .stream() .map(this::toDto) .toList(); } private PartCategoryDto toDto(PartCategory entity) { - PartCategoryDto dto = new PartCategoryDto(); - dto.setId(entity.getId()); - dto.setSlug(entity.getSlug()); - dto.setName(entity.getName()); - dto.setDescription(entity.getDescription()); - dto.setGroupName(entity.getGroupName()); - dto.setSortOrder(entity.getSortOrder()); - dto.setUuid(entity.getUuid()); - return dto; + return new PartCategoryDto( + entity.getId(), + entity.getSlug(), + entity.getName(), + entity.getDescription(), + entity.getGroupName(), + entity.getSortOrder() + ); } } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java new file mode 100644 index 0000000..e7553cd --- /dev/null +++ b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java @@ -0,0 +1,125 @@ +package group.goforward.ballistic.controllers.admin; + +import group.goforward.ballistic.model.AffiliateCategoryMap; +import group.goforward.ballistic.model.PartCategory; +import group.goforward.ballistic.repos.CategoryMappingRepository; +import group.goforward.ballistic.repos.PartCategoryRepository; +import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto; +import group.goforward.ballistic.web.dto.admin.PartRoleMappingRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/category-mappings") +@CrossOrigin +public class AdminCategoryMappingController { + + private final CategoryMappingRepository categoryMappingRepository; + private final PartCategoryRepository partCategoryRepository; + + public AdminCategoryMappingController( + CategoryMappingRepository categoryMappingRepository, + PartCategoryRepository partCategoryRepository + ) { + this.categoryMappingRepository = categoryMappingRepository; + this.partCategoryRepository = partCategoryRepository; + } + + // GET /api/admin/category-mappings?platform=AR-15 + @GetMapping + public List list( + @RequestParam(name = "platform", defaultValue = "AR-15") String platform + ) { + List mappings = + categoryMappingRepository.findBySourceTypeAndPlatformOrderById("PART_ROLE", platform); + + return mappings.stream() + .map(this::toDto) + .toList(); + } + + // POST /api/admin/category-mappings + @PostMapping + public ResponseEntity create( + @RequestBody PartRoleMappingRequest request + ) { + if (request.platform() == null || request.partRole() == null || request.categorySlug() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "platform, partRole, and categorySlug are required"); + } + + PartCategory category = partCategoryRepository.findBySlug(request.categorySlug()) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Unknown category slug: " + request.categorySlug() + )); + + AffiliateCategoryMap mapping = new AffiliateCategoryMap(); + mapping.setSourceType("PART_ROLE"); + mapping.setSourceValue(request.partRole()); + mapping.setPlatform(request.platform()); + mapping.setPartCategory(category); + mapping.setNotes(request.notes()); + + AffiliateCategoryMap saved = categoryMappingRepository.save(mapping); + + return ResponseEntity.status(HttpStatus.CREATED).body(toDto(saved)); + } + + // PUT /api/admin/category-mappings/{id} + @PutMapping("/{id}") + public PartRoleMappingDto update( + @PathVariable Integer id, + @RequestBody PartRoleMappingRequest request + ) { + AffiliateCategoryMap mapping = categoryMappingRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found")); + + if (request.platform() != null) { + mapping.setPlatform(request.platform()); + } + if (request.partRole() != null) { + mapping.setSourceValue(request.partRole()); + } + if (request.categorySlug() != null) { + PartCategory category = partCategoryRepository.findBySlug(request.categorySlug()) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Unknown category slug: " + request.categorySlug() + )); + mapping.setPartCategory(category); + } + if (request.notes() != null) { + mapping.setNotes(request.notes()); + } + + AffiliateCategoryMap saved = categoryMappingRepository.save(mapping); + return toDto(saved); + } + + // DELETE /api/admin/category-mappings/{id} + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Integer id) { + if (!categoryMappingRepository.existsById(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found"); + } + categoryMappingRepository.deleteById(id); + } + + private PartRoleMappingDto toDto(AffiliateCategoryMap map) { + PartCategory cat = map.getPartCategory(); + + return new PartRoleMappingDto( + map.getId(), + map.getPlatform(), + map.getSourceValue(), // partRole + cat != null ? cat.getSlug() : null, // categorySlug + cat != null ? cat.getGroupName() : null, + map.getNotes() + ); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/PartCategoryAdminController.java b/src/main/java/group/goforward/ballistic/controllers/admin/PartCategoryAdminController.java new file mode 100644 index 0000000..511a56f --- /dev/null +++ b/src/main/java/group/goforward/ballistic/controllers/admin/PartCategoryAdminController.java @@ -0,0 +1,35 @@ +package group.goforward.ballistic.controllers.admin; + +import group.goforward.ballistic.model.PartCategory; +import group.goforward.ballistic.repos.PartCategoryRepository; +import group.goforward.ballistic.web.dto.admin.PartCategoryDto; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/part-categories") +@CrossOrigin // keep it loose for now, you can tighten origins later +public class PartCategoryAdminController { + + private final PartCategoryRepository partCategories; + + public PartCategoryAdminController(PartCategoryRepository partCategories) { + this.partCategories = partCategories; + } + + @GetMapping + public List list() { + return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc() + .stream() + .map(pc -> new PartCategoryDto( + pc.getId(), + pc.getSlug(), + pc.getName(), + pc.getDescription(), + pc.getGroupName(), + pc.getSortOrder() + )) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java b/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java index d3e6cf4..eee49ec 100644 --- a/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java +++ b/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java @@ -8,18 +8,17 @@ public class AffiliateCategoryMap { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", nullable = false) private Integer id; - // e.g. "PART_ROLE" + // e.g. "PART_ROLE", "RAW_CATEGORY", etc. @Column(name = "source_type", nullable = false) private String sourceType; - // e.g. "suppressor" + // the value we’re mapping from (e.g. "suppressor", "TRIGGER") @Column(name = "source_value", nullable = false) private String sourceValue; - // e.g. "AR-15", nullable + // optional platform ("AR-15", "PRECISION", etc.) @Column(name = "platform") private String platform; @@ -30,6 +29,8 @@ public class AffiliateCategoryMap { @Column(name = "notes") private String notes; + // --- getters / setters --- + public Integer getId() { return id; } diff --git a/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java b/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java index cbdbc13..d483e05 100644 --- a/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java @@ -3,18 +3,27 @@ package group.goforward.ballistic.repos; import group.goforward.ballistic.model.AffiliateCategoryMap; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface CategoryMappingRepository extends JpaRepository { + // Match by source_type + source_value + platform (case-insensitive) Optional findBySourceTypeAndSourceValueAndPlatformIgnoreCase( String sourceType, String sourceValue, String platform ); + // Fallback: match by source_type + source_value when platform is null/ignored Optional findBySourceTypeAndSourceValueIgnoreCase( String sourceType, String sourceValue ); + + // Used by AdminCategoryMappingController: list mappings for a given source_type + platform + List findBySourceTypeAndPlatformOrderById( + String sourceType, + String platform + ); } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java b/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java index 69ba09d..17e5963 100644 --- a/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java @@ -1,35 +1,10 @@ package group.goforward.ballistic.web.dto.admin; -import java.util.UUID; - -public class PartCategoryDto { - private Integer id; - private String slug; - private String name; - private String description; - private String groupName; - private Integer sortOrder; - private UUID uuid; - - // getters + setters - public Integer getId() { return id; } - public void setId(Integer id) { this.id = id; } - - public String getSlug() { return slug; } - public void setSlug(String slug) { this.slug = slug; } - - public String getName() { return name; } - public void setName(String name) { this.name = name; } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public String getGroupName() { return groupName; } - public void setGroupName(String groupName) { this.groupName = groupName; } - - public Integer getSortOrder() { return sortOrder; } - public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } - - public UUID getUuid() { return uuid; } - public void setUuid(UUID uuid) { this.uuid = uuid; } -} \ No newline at end of file +public record PartCategoryDto( + Integer id, + String slug, + String name, + String description, + String groupName, + Integer sortOrder +) {} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java index 93756e4..5082f89 100644 --- a/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java @@ -1,69 +1,10 @@ package group.goforward.ballistic.web.dto.admin; -public class PartRoleMappingDto { - private Integer id; - private String platform; - private String partRole; - private String categorySlug; - private String categoryName; - private String groupName; - private String notes; - - // getters + setters... - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - 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 getCategorySlug() { - return categorySlug; - } - - public void setCategorySlug(String categorySlug) { - this.categorySlug = categorySlug; - } - - public String getCategoryName() { - return categoryName; - } - - public void setCategoryName(String categoryName) { - this.categoryName = categoryName; - } - - public String getGroupName() { - return groupName; - } - - public void setGroupName(String groupName) { - this.groupName = groupName; - } - - public String getNotes() { - return notes; - } - - public void setNotes(String notes) { - this.notes = notes; - } -} \ No newline at end of file +public record PartRoleMappingDto( + Integer id, + String platform, + String partRole, + String categorySlug, + String groupName, + String notes +) {} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingRequest.java b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingRequest.java new file mode 100644 index 0000000..45a4074 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingRequest.java @@ -0,0 +1,8 @@ +package group.goforward.ballistic.web.dto.admin; + +public record PartRoleMappingRequest( + String platform, + String partRole, + String categorySlug, + String notes +) {} \ No newline at end of file