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..3f9be99 --- /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) { + 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 3e623dc..eee49ec 100644 --- a/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java +++ b/src/main/java/group/goforward/ballistic/model/AffiliateCategoryMap.java @@ -5,23 +5,32 @@ 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", "RAW_CATEGORY", etc. + @Column(name = "source_type", nullable = false) + private String sourceType; - @Column(name = "affiliatecategory", nullable = false) - private String affiliatecategory; + // the value we’re mapping from (e.g. "suppressor", "TRIGGER") + @Column(name = "source_value", nullable = false) + private String sourceValue; - @Column(name = "buildercategoryid", nullable = false) - private Integer buildercategoryid; + // optional platform ("AR-15", "PRECISION", etc.) + @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; + // --- getters / setters --- + public Integer getId() { return id; } @@ -30,28 +39,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 +78,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 cea3afc..d483e05 100644 --- a/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java @@ -1,7 +1,29 @@ -package group.goforward.ballistic.repos; - -import group.goforward.ballistic.model.AffiliateCategoryMap; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CategoryMappingRepository extends JpaRepository { +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/repos/PartCategoryRepository.java b/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java index 78e2c2e..aff326e 100644 --- a/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/PartCategoryRepository.java @@ -1,9 +1,14 @@ -package group.goforward.ballistic.repos; - -import group.goforward.ballistic.model.PartCategory; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface PartCategoryRepository extends JpaRepository { - Optional findBySlug(String slug); +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 179f1a6..24f821c 100644 --- a/src/main/java/group/goforward/ballistic/repos/ProductRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/ProductRepository.java @@ -1,53 +1,62 @@ -package group.goforward.ballistic.repos; - -import group.goforward.ballistic.model.Product; -import group.goforward.ballistic.model.Brand; -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); - - 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); - - // 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) ---------- - - @Query(""" - SELECT p - FROM Product p - JOIN FETCH p.brand b - WHERE p.platform = :platform - AND p.deletedAt IS NULL - """) - List findByPlatformWithBrand(@Param("platform") String platform); - - @Query(""" - SELECT p - FROM Product p - JOIN FETCH p.brand b - WHERE p.platform = :platform - AND p.partRole IN :partRoles - AND p.deletedAt IS NULL - """) - List findByPlatformAndPartRoleInWithBrand( - @Param("platform") String platform, - @Param("partRoles") Collection partRoles - ); +package group.goforward.ballistic.repos; + +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.List; + +public interface ProductRepository extends JpaRepository { + + // ------------------------------------------------- + // Used by MerchantFeedImportServiceImpl + // ------------------------------------------------- + + List findAllByBrandAndMpn(Brand brand, String mpn); + + List findAllByBrandAndUpc(Brand brand, String upc); + + boolean existsBySlug(String slug); + + // ------------------------------------------------- + // Used by ProductController for platform views + // ------------------------------------------------- + + @Query(""" + SELECT p + FROM Product p + JOIN FETCH p.brand b + WHERE p.platform = :platform + AND p.deletedAt IS NULL + """) + List findByPlatformWithBrand(@Param("platform") String platform); + + @Query(""" + SELECT p + FROM Product p + JOIN FETCH p.brand b + WHERE p.platform = :platform + AND p.partRole IN :roles + AND p.deletedAt IS NULL + """) + List findByPlatformAndPartRoleInWithBrand( + @Param("platform") String platform, + @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..17e5963 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartCategoryDto.java @@ -0,0 +1,10 @@ +package group.goforward.ballistic.web.dto.admin; + +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 new file mode 100644 index 0000000..5082f89 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/PartRoleMappingDto.java @@ -0,0 +1,10 @@ +package group.goforward.ballistic.web.dto.admin; + +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