From 3d1501cc87a71af86555e1ba7b707674855fdfd1 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Dec 2025 14:43:07 -0500 Subject: [PATCH 1/2] running finally.. --- .../configuration/SecurityConfig.java | 5 + .../admin/AdminCategoryMappingController.java | 154 +++++++++--------- .../repos/CategoryMappingRepository.java | 33 ++-- .../services/PartCategoryResolverService.java | 41 ++--- 4 files changed, 113 insertions(+), 120 deletions(-) diff --git a/src/main/java/group/goforward/ballistic/configuration/SecurityConfig.java b/src/main/java/group/goforward/ballistic/configuration/SecurityConfig.java index 3a0a3c4..2b3f98d 100644 --- a/src/main/java/group/goforward/ballistic/configuration/SecurityConfig.java +++ b/src/main/java/group/goforward/ballistic/configuration/SecurityConfig.java @@ -24,14 +24,19 @@ public class SecurityConfig { sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth + // Auth endpoints always open .requestMatchers("/api/auth/**").permitAll() + // Swagger / docs .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() + // Health .requestMatchers("/actuator/health", "/actuator/info").permitAll() + // Public product endpoints .requestMatchers("/api/products/gunbuilder/**").permitAll() + // Everything else (for now) also open – we can tighten later .anyRequest().permitAll() ); diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java index e7553cd..6a9e5e1 100644 --- a/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java +++ b/src/main/java/group/goforward/ballistic/controllers/admin/AdminCategoryMappingController.java @@ -1,13 +1,15 @@ package group.goforward.ballistic.controllers.admin; -import group.goforward.ballistic.model.AffiliateCategoryMap; +import group.goforward.ballistic.model.CategoryMapping; +import group.goforward.ballistic.model.Merchant; import group.goforward.ballistic.model.PartCategory; import group.goforward.ballistic.repos.CategoryMappingRepository; +import group.goforward.ballistic.repos.MerchantRepository; import group.goforward.ballistic.repos.PartCategoryRepository; -import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto; -import group.goforward.ballistic.web.dto.admin.PartRoleMappingRequest; +import group.goforward.ballistic.web.dto.admin.MerchantCategoryMappingDto; +import group.goforward.ballistic.web.dto.admin.SimpleMerchantDto; +import group.goforward.ballistic.web.dto.admin.UpdateMerchantCategoryMappingRequest; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -15,111 +17,101 @@ import java.util.List; @RestController @RequestMapping("/api/admin/category-mappings") -@CrossOrigin +@CrossOrigin // you can tighten origins later public class AdminCategoryMappingController { private final CategoryMappingRepository categoryMappingRepository; + private final MerchantRepository merchantRepository; private final PartCategoryRepository partCategoryRepository; public AdminCategoryMappingController( CategoryMappingRepository categoryMappingRepository, + MerchantRepository merchantRepository, PartCategoryRepository partCategoryRepository ) { this.categoryMappingRepository = categoryMappingRepository; + this.merchantRepository = merchantRepository; 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) + /** + * Merchants that have at least one category_mappings row. + * Used for the "All Merchants" dropdown in the UI. + */ + @GetMapping("/merchants") + public List listMerchantsWithMappings() { + List merchants = categoryMappingRepository.findDistinctMerchantsWithMappings(); + return merchants.stream() + .map(m -> new SimpleMerchantDto(m.getId(), m.getName())) .toList(); } - // POST /api/admin/category-mappings - @PostMapping - public ResponseEntity create( - @RequestBody PartRoleMappingRequest request + /** + * List mappings for a specific merchant, or all mappings if no merchantId is provided. + * GET /api/admin/category-mappings?merchantId=1 + */ + @GetMapping + public List listByMerchant( + @RequestParam(name = "merchantId", required = false) Integer merchantId ) { - if (request.platform() == null || request.partRole() == null || request.categorySlug() == null) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "platform, partRole, and categorySlug are required"); + List mappings; + + if (merchantId != null) { + mappings = categoryMappingRepository.findByMerchantIdOrderByRawCategoryPathAsc(merchantId); + } else { + // fall back to all mappings; you can add a more specific repository method later if desired + mappings = categoryMappingRepository.findAll(); } - 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)); + return mappings.stream() + .map(cm -> new MerchantCategoryMappingDto( + cm.getId(), + cm.getMerchant().getId(), + cm.getMerchant().getName(), + cm.getRawCategoryPath(), + cm.getPartCategory() != null ? cm.getPartCategory().getId() : null, + cm.getPartCategory() != null ? cm.getPartCategory().getName() : null + )) + .toList(); } - // PUT /api/admin/category-mappings/{id} - @PutMapping("/{id}") - public PartRoleMappingDto update( + /** + * Update a single mapping's part_category. + * POST /api/admin/category-mappings/{id} + * Body: { "partCategoryId": 24 } + */ + @PostMapping("/{id}") + public MerchantCategoryMappingDto updateMapping( @PathVariable Integer id, - @RequestBody PartRoleMappingRequest request + @RequestBody UpdateMerchantCategoryMappingRequest request ) { - AffiliateCategoryMap mapping = categoryMappingRepository.findById(id) + CategoryMapping 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()); + PartCategory partCategory = null; + if (request.partCategoryId() != null) { + partCategory = partCategoryRepository.findById(request.partCategoryId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Part category not found")); } - AffiliateCategoryMap saved = categoryMappingRepository.save(mapping); - return toDto(saved); - } + mapping.setPartCategory(partCategory); + mapping = categoryMappingRepository.save(mapping); - // 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() + return new MerchantCategoryMappingDto( + mapping.getId(), + mapping.getMerchant().getId(), + mapping.getMerchant().getName(), + mapping.getRawCategoryPath(), + mapping.getPartCategory() != null ? mapping.getPartCategory().getId() : null, + mapping.getPartCategory() != null ? mapping.getPartCategory().getName() : null ); } + @PutMapping("/{id}") + public MerchantCategoryMappingDto updateMappingPut( + @PathVariable Integer id, + @RequestBody UpdateMerchantCategoryMappingRequest request + ) { + // just delegate so POST & PUT behave the same + return updateMapping(id, request); + } } \ 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 d483e05..b6132c0 100644 --- a/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java +++ b/src/main/java/group/goforward/ballistic/repos/CategoryMappingRepository.java @@ -1,29 +1,22 @@ package group.goforward.ballistic.repos; -import group.goforward.ballistic.model.AffiliateCategoryMap; +import group.goforward.ballistic.model.CategoryMapping; +import group.goforward.ballistic.model.Merchant; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; -import java.util.Optional; -public interface CategoryMappingRepository extends JpaRepository { +public interface CategoryMappingRepository extends JpaRepository { - // Match by source_type + source_value + platform (case-insensitive) - Optional findBySourceTypeAndSourceValueAndPlatformIgnoreCase( - String sourceType, - String sourceValue, - String platform - ); + // All mappings for a merchant, ordered nicely + List findByMerchantIdOrderByRawCategoryPathAsc(Integer merchantId); - // 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 - ); + // Merchants that actually have mappings (for the dropdown) + @Query(""" + select distinct cm.merchant + from CategoryMapping cm + order by cm.merchant.name asc + """) + List findDistinctMerchantsWithMappings(); } \ 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 252ad6f..31dd63a 100644 --- a/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java +++ b/src/main/java/group/goforward/ballistic/services/PartCategoryResolverService.java @@ -1,8 +1,7 @@ package group.goforward.ballistic.services; -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 org.springframework.stereotype.Service; import java.util.Optional; @@ -10,29 +9,33 @@ import java.util.Optional; @Service public class PartCategoryResolverService { - private final CategoryMappingRepository categoryMappingRepository; + private final PartCategoryRepository partCategoryRepository; - public PartCategoryResolverService(CategoryMappingRepository categoryMappingRepository) { - this.categoryMappingRepository = categoryMappingRepository; + public PartCategoryResolverService(PartCategoryRepository partCategoryRepository) { + this.partCategoryRepository = partCategoryRepository; } /** - * Resolve a part category from a platform + partRole (what gunbuilder cares about). - * Returns Optional.empty() if we have no mapping yet. + * 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. */ public Optional resolveForPlatformAndPartRole(String platform, String partRole) { - // sourceType is a convention – you can also enum this - String sourceType = "PART_ROLE"; + if (partRole == null || partRole.isBlank()) { + return Optional.empty(); + } - // 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) - ); + String normalizedSlug = partRole + .trim() + .toLowerCase() + .replace(" ", "-"); + + return partCategoryRepository.findBySlug(normalizedSlug); } } \ No newline at end of file From 9096ddd165149b8064bd2a58d54c2b966fc889f8 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Dec 2025 15:06:29 -0500 Subject: [PATCH 2/2] running finally.. --- .../admin/AdminPartRoleMappingController.java | 123 ++++++++++++++++++ .../ballistic/model/PartRoleMapping.java | 65 +++++++++ .../repos/PartRoleMappingRepository.java | 12 ++ .../dto/admin/AdminPartRoleMappingDto.java | 10 ++ .../admin/CreatePartRoleMappingRequest.java | 8 ++ .../admin/UpdatePartRoleMappingRequest.java | 8 ++ 6 files changed, 226 insertions(+) create mode 100644 src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java create mode 100644 src/main/java/group/goforward/ballistic/model/PartRoleMapping.java create mode 100644 src/main/java/group/goforward/ballistic/repos/PartRoleMappingRepository.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/admin/AdminPartRoleMappingDto.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/admin/CreatePartRoleMappingRequest.java create mode 100644 src/main/java/group/goforward/ballistic/web/dto/admin/UpdatePartRoleMappingRequest.java diff --git a/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java b/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java new file mode 100644 index 0000000..7300129 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/controllers/admin/AdminPartRoleMappingController.java @@ -0,0 +1,123 @@ +package group.goforward.ballistic.controllers.admin; + +import group.goforward.ballistic.model.PartCategory; +import group.goforward.ballistic.model.PartRoleMapping; +import group.goforward.ballistic.repos.PartCategoryRepository; +import group.goforward.ballistic.repos.PartRoleMappingRepository; +import group.goforward.ballistic.web.dto.admin.AdminPartRoleMappingDto; +import group.goforward.ballistic.web.dto.admin.CreatePartRoleMappingRequest; +import group.goforward.ballistic.web.dto.admin.UpdatePartRoleMappingRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin/part-role-mappings") +@CrossOrigin +public class AdminPartRoleMappingController { + + private final PartRoleMappingRepository partRoleMappingRepository; + private final PartCategoryRepository partCategoryRepository; + + public AdminPartRoleMappingController( + PartRoleMappingRepository partRoleMappingRepository, + PartCategoryRepository partCategoryRepository + ) { + this.partRoleMappingRepository = partRoleMappingRepository; + this.partCategoryRepository = partCategoryRepository; + } + + // GET /api/admin/part-role-mappings?platform=AR-15 + @GetMapping + public List list( + @RequestParam(name = "platform", required = false) String platform + ) { + List mappings; + + if (platform != null && !platform.isBlank()) { + mappings = partRoleMappingRepository.findByPlatformOrderByPartRoleAsc(platform); + } else { + mappings = partRoleMappingRepository.findAll(); + } + + return mappings.stream() + .map(this::toDto) + .toList(); + } + + // POST /api/admin/part-role-mappings + @PostMapping + public AdminPartRoleMappingDto create( + @RequestBody CreatePartRoleMappingRequest request + ) { + PartCategory category = partCategoryRepository.findBySlug(request.categorySlug()) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "PartCategory not found for slug: " + request.categorySlug() + )); + + PartRoleMapping mapping = new PartRoleMapping(); + mapping.setPlatform(request.platform()); + mapping.setPartRole(request.partRole()); + mapping.setPartCategory(category); + mapping.setNotes(request.notes()); + + mapping = partRoleMappingRepository.save(mapping); + return toDto(mapping); + } + + // PUT /api/admin/part-role-mappings/{id} + @PutMapping("/{id}") + public AdminPartRoleMappingDto update( + @PathVariable Integer id, + @RequestBody UpdatePartRoleMappingRequest request + ) { + PartRoleMapping mapping = partRoleMappingRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found")); + + if (request.platform() != null) { + mapping.setPlatform(request.platform()); + } + if (request.partRole() != null) { + mapping.setPartRole(request.partRole()); + } + if (request.categorySlug() != null) { + PartCategory category = partCategoryRepository.findBySlug(request.categorySlug()) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "PartCategory not found for slug: " + request.categorySlug() + )); + mapping.setPartCategory(category); + } + if (request.notes() != null) { + mapping.setNotes(request.notes()); + } + + mapping = partRoleMappingRepository.save(mapping); + return toDto(mapping); + } + + // DELETE /api/admin/part-role-mappings/{id} + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Integer id) { + if (!partRoleMappingRepository.existsById(id)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found"); + } + partRoleMappingRepository.deleteById(id); + } + + private AdminPartRoleMappingDto toDto(PartRoleMapping mapping) { + PartCategory cat = mapping.getPartCategory(); + return new AdminPartRoleMappingDto( + mapping.getId(), + mapping.getPlatform(), + mapping.getPartRole(), + cat != null ? cat.getSlug() : null, + cat != null ? cat.getGroupName() : null, + mapping.getNotes() + ); + } +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java b/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java new file mode 100644 index 0000000..d336815 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/model/PartRoleMapping.java @@ -0,0 +1,65 @@ +package group.goforward.ballistic.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "part_role_mappings") +public class PartRoleMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private String platform; // e.g. "AR-15" + + @Column(name = "part_role", nullable = false) + private String partRole; // e.g. "UPPER", "BARREL", etc. + + @ManyToOne(optional = false) + @JoinColumn(name = "part_category_id") + private PartCategory partCategory; + + @Column(columnDefinition = "text") + private String notes; + + 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 getPartCategory() { + return partCategory; + } + + public void setPartCategory(PartCategory partCategory) { + this.partCategory = partCategory; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } +} \ 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 new file mode 100644 index 0000000..b64889e --- /dev/null +++ b/src/main/java/group/goforward/ballistic/repos/PartRoleMappingRepository.java @@ -0,0 +1,12 @@ +package group.goforward.ballistic.repos; + +import group.goforward.ballistic.model.PartRoleMapping; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PartRoleMappingRepository extends JpaRepository { + + // List mappings for a platform, ordered nicely for the UI + List findByPlatformOrderByPartRoleAsc(String platform); +} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/AdminPartRoleMappingDto.java b/src/main/java/group/goforward/ballistic/web/dto/admin/AdminPartRoleMappingDto.java new file mode 100644 index 0000000..1146848 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/AdminPartRoleMappingDto.java @@ -0,0 +1,10 @@ +package group.goforward.ballistic.web.dto.admin; + +public record AdminPartRoleMappingDto( + 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/CreatePartRoleMappingRequest.java b/src/main/java/group/goforward/ballistic/web/dto/admin/CreatePartRoleMappingRequest.java new file mode 100644 index 0000000..74445c9 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/CreatePartRoleMappingRequest.java @@ -0,0 +1,8 @@ +package group.goforward.ballistic.web.dto.admin; + +public record CreatePartRoleMappingRequest( + String platform, + String partRole, + String categorySlug, + String notes +) {} \ No newline at end of file diff --git a/src/main/java/group/goforward/ballistic/web/dto/admin/UpdatePartRoleMappingRequest.java b/src/main/java/group/goforward/ballistic/web/dto/admin/UpdatePartRoleMappingRequest.java new file mode 100644 index 0000000..b70b9f6 --- /dev/null +++ b/src/main/java/group/goforward/ballistic/web/dto/admin/UpdatePartRoleMappingRequest.java @@ -0,0 +1,8 @@ +package group.goforward.ballistic.web.dto.admin; + +public record UpdatePartRoleMappingRequest( + String platform, + String partRole, + String categorySlug, + String notes +) {} \ No newline at end of file