diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java b/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java index 7300129..5b79c38 100644 --- a/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java +++ b/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java @@ -37,7 +37,8 @@ public class AdminPartRoleMappingController { List mappings; if (platform != null && !platform.isBlank()) { - mappings = partRoleMappingRepository.findByPlatformOrderByPartRoleAsc(platform); + mappings = partRoleMappingRepository + .findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(platform); } else { mappings = partRoleMappingRepository.findAll(); } diff --git a/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java b/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java index d336815..4d23d7e 100644 --- a/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java +++ b/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java @@ -1,6 +1,7 @@ package group.goforward.ballistic.model; import jakarta.persistence.*; +import java.time.OffsetDateTime; @Entity @Table(name = "part_role_mappings") @@ -14,15 +15,38 @@ public class PartRoleMapping { private String platform; // e.g. "AR-15" @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") private PartCategory partCategory; @Column(columnDefinition = "text") 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() { return id; } @@ -62,4 +86,20 @@ public class PartRoleMapping { public void setNotes(String 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; + } } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/repos/PartRoleMappingRepository.java b/src/main/java/group/goforward/ballistic/repos/PartRoleMappingRepository.java index b64889e..91b7eb9 100644 --- a/src/main/java/group/goforward/ballistic/repos/PartRoleMappingRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/PartRoleMappingRepository.java @@ -4,9 +4,29 @@ import group.goforward.ballistic.model.PartRoleMapping; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface PartRoleMappingRepository extends JpaRepository { - // List mappings for a platform, ordered nicely for the UI - List findByPlatformOrderByPartRoleAsc(String platform); + // For resolver: one mapping per platform + partRole + Optional findFirstByPlatformAndPartRoleAndDeletedAtIsNull( + String platform, + String partRole + ); + + // Optional: debug / inspection + List findAllByPlatformAndPartRoleAndDeletedAtIsNull( + String platform, + String partRole + ); + + // This is the one PartRoleMappingService needs + List findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc( + String platform + ); + + List findByPlatformAndPartCategory_SlugAndDeletedAtIsNull( + String platform, + String slug + ); } \ 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 ad91c1e..1801d49 100644 --- a/src/main/java/group/goforward/ballistic/repos/ProductRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/ProductRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Collection; import java.util.List; public interface ProductRepository extends JpaRepository { @@ -33,8 +34,8 @@ public interface ProductRepository extends JpaRepository { """) List findByPlatformWithBrand(@Param("platform") String platform); -@Query(name="Products.findByPlatformWithBrand") -List findByPlatformWithBrandNQ(@Param("platform") String platform); + @Query(name = "Products.findByPlatformWithBrand") + List findByPlatformWithBrandNQ(@Param("platform") String platform); @Query(""" SELECT p @@ -50,7 +51,21 @@ List findByPlatformWithBrandNQ(@Param("platform") String platform); ); // ------------------------------------------------- - // Used by Gunbuilder service (if you wired this) + // Used by /api/gunbuilder/test-products-db + // ------------------------------------------------- + + @Query(""" + SELECT p + FROM Product p + JOIN FETCH p.brand b + WHERE p.platform = :platform + AND p.deletedAt IS NULL + ORDER BY p.id + """) + List findTop5ByPlatformWithBrand(@Param("platform") String platform); + + // ------------------------------------------------- + // Used by GunbuilderProductService // ------------------------------------------------- @Query(""" @@ -59,7 +74,11 @@ List findByPlatformWithBrandNQ(@Param("platform") String platform); LEFT JOIN FETCH p.brand b LEFT JOIN FETCH p.offers o WHERE p.platform = :platform + AND p.partRole IN :partRoles AND p.deletedAt IS NULL """) - List findSomethingForGunbuilder(@Param("platform") String platform); + List findForGunbuilderByPlatformAndPartRoles( + @Param("platform") String platform, + @Param("partRoles") Collection partRoles + ); } \ 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 index 1e074fe..5c57802 100644 --- a/src/main/java/group/goforward/ballistic/services/GunbuilderProductService.java +++ b/src/main/java/group/goforward/ballistic/services/GunbuilderProductService.java @@ -5,38 +5,77 @@ 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 group.goforward.ballistic.model.PartRoleMapping; +import group.goforward.ballistic.repos.PartRoleMappingRepository; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.List; + @Service public class GunbuilderProductService { private final ProductRepository productRepository; private final PartCategoryResolverService partCategoryResolverService; + private final PartRoleMappingRepository partRoleMappingRepository; public GunbuilderProductService( ProductRepository productRepository, - PartCategoryResolverService partCategoryResolverService + PartCategoryResolverService partCategoryResolverService, + PartRoleMappingRepository partRoleMappingRepository ) { this.productRepository = productRepository; this.partCategoryResolverService = partCategoryResolverService; + this.partRoleMappingRepository = partRoleMappingRepository; } - public List 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 listGunbuilderProducts(String platform, List partRoles) { - List 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 products = + productRepository.findForGunbuilderByPlatformAndPartRoles(platform, partRoles); + + System.out.println(">>> GB: repo returned " + products.size() + " products"); + + Map categoryCache = new HashMap<>(); return products.stream() .map(p -> { - var maybeCategory = partCategoryResolverService - .resolveForPlatformAndPartRole(platform, p.getPartRole()); + PartCategory cat = categoryCache.computeIfAbsent( + p.getPartRole(), + role -> partCategoryResolverService + .resolveForPlatformAndPartRole(platform, role) + .orElse(null) + ); - if (maybeCategory.isEmpty()) { - // you can also log here - return null; + if (cat == null) { + System.out.println(">>> GB: NO CATEGORY for platform=" + platform + + ", 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( p.getId(), @@ -47,11 +86,54 @@ public class GunbuilderProductService { p.getBestOfferPrice(), p.getMainImageUrl(), p.getBestOfferBuyUrl(), - cat.getSlug(), - cat.getGroupName() + categorySlug, + categoryGroup ); }) - .filter(dto -> dto != null) .toList(); } + + public List 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 mappings = + partRoleMappingRepository.findByPlatformAndPartCategory_SlugAndDeletedAtIsNull( + platform, + categorySlug + ); + + List 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 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); + } } \ 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 index 31dd63a..12e0f52 100644 --- a/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java +++ b/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java @@ -1,7 +1,8 @@ package group.goforward.ballistic.services; 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 java.util.Optional; @@ -9,33 +10,29 @@ import java.util.Optional; @Service public class PartCategoryResolverService { - private final PartCategoryRepository partCategoryRepository; + private final PartRoleMappingRepository partRoleMappingRepository; - public PartCategoryResolverService(PartCategoryRepository partCategoryRepository) { - this.partCategoryRepository = partCategoryRepository; + public PartCategoryResolverService(PartRoleMappingRepository partRoleMappingRepository) { + this.partRoleMappingRepository = partRoleMappingRepository; } /** - * Resolve the canonical PartCategory for a given platform + partRole. - * - * 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. + * Resolve a PartCategory for a given platform + partRole. + * Example: ("AR-15", "LOWER_RECEIVER_STRIPPED") -> category "lower" */ public Optional resolveForPlatformAndPartRole(String platform, String partRole) { - if (partRole == null || partRole.isBlank()) { + + if (platform == null || partRole == null) { return Optional.empty(); } - String normalizedSlug = partRole - .trim() - .toLowerCase() - .replace(" ", "-"); + // Keep things case-sensitive since your DB values are already uppercase. + Optional mappingOpt = + partRoleMappingRepository.findFirstByPlatformAndPartRoleAndDeletedAtIsNull( + platform, + partRole + ); - return partCategoryRepository.findBySlug(normalizedSlug); + return mappingOpt.map(PartRoleMapping::getPartCategory); } } \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/services/PartRoleMappingService.java b/src/main/java/group/goforward/ballistic/services/PartRoleMappingService.java new file mode 100644 index 0000000..cd9f507 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/services/PartRoleMappingService.java @@ -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 getMappingsForPlatform(String platform) { + return repository + .findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(platform) + .stream() + .map(PartRoleMappingMapper::toDto) + .toList(); + } + + public List getRoleToCategoryMap(String platform) { + return repository + .findByPlatformAndDeletedAtIsNullOrderByPartRoleAsc(platform) + .stream() + .map(PartRoleMappingMapper::toRoleMapDto) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/GunbuilderProductController.java b/src/main/java/group/goforward/ballistic/web/GunbuilderProductController.java new file mode 100644 index 0000000..4fbd505 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/GunbuilderProductController.java @@ -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 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 testProducts(@RequestParam String platform) { + System.out.println(">>> /api/gunbuilder/test-products hit for platform=" + platform); + + Map 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 listProducts( + @RequestParam String platform, + @RequestParam(required = false) List partRoles + ) { + return gunbuilderProductService.listGunbuilderProducts(platform, partRoles); + } + + @GetMapping("/products/by-category") + public List 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> 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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/PartRoleMappingController.java b/src/main/java/group/goforward/ballistic/web/PartRoleMappingController.java new file mode 100644 index 0000000..d68568c --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/PartRoleMappingController.java @@ -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 getMappings(@PathVariable String platform) { + return service.getMappingsForPlatform(platform); + } + + // Thin mapping for the builder + @GetMapping("/{platform}/map") + public List getRoleMap(@PathVariable String platform) { + return service.getRoleToCategoryMap(platform); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/PartRoleToCategoryDto.java b/src/main/java/group/goforward/ballistic/web/dto/PartRoleToCategoryDto.java new file mode 100644 index 0000000..1dcd3ba --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/PartRoleToCategoryDto.java @@ -0,0 +1,7 @@ +package group.goforward.ballistic.web.dto; + +public record PartRoleToCategoryDto( + String platform, + String partRole, + String categorySlug // e.g. "lower", "barrel", "optic" +) {} \ 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 5082f89..64317e1 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 @@ -4,7 +4,8 @@ public record PartRoleMappingDto( Integer id, String platform, String partRole, - String categorySlug, - String groupName, + Integer partCategoryId, + String partCategorySlug, + String partCategoryName, String notes ) {} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/mapper/PartRoleMappingMapper.java b/src/main/java/group/goforward/ballistic/web/mapper/PartRoleMappingMapper.java new file mode 100644 index 0000000..ff9a850 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/mapper/PartRoleMappingMapper.java @@ -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 + ); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 87d55f9..06afe4b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,8 +7,13 @@ spring.datasource.driver-class-name=org.postgresql.Driver # Hibernate properties #spring.jpa.hibernate.ddl-auto=update -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +#spring.jpa.show-sql=true +#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect security.jwt.secret=ballistic-test-secret-key-1234567890-ABCDEFGHIJKLNMOPQRST -security.jwt.access-token-minutes=2880 \ No newline at end of file +security.jwt.access-token-minutes=2880 + +# Temp disabling logging to find what I fucked up +spring.jpa.show-sql=false +logging.level.org.hibernate.SQL=warn +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=warn \ No newline at end of file