running finally..

This commit is contained in:
2025-12-04 14:43:07 -05:00
parent 74a5c42e26
commit 3d1501cc87
4 changed files with 113 additions and 120 deletions

View File

@@ -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()
);

View File

@@ -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<PartRoleMappingDto> list(
@RequestParam(name = "platform", defaultValue = "AR-15") String platform
) {
List<AffiliateCategoryMap> 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<SimpleMerchantDto> listMerchantsWithMappings() {
List<Merchant> merchants = categoryMappingRepository.findDistinctMerchantsWithMappings();
return merchants.stream()
.map(m -> new SimpleMerchantDto(m.getId(), m.getName()))
.toList();
}
// POST /api/admin/category-mappings
@PostMapping
public ResponseEntity<PartRoleMappingDto> 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<MerchantCategoryMappingDto> 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<CategoryMapping> 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);
}
}

View File

@@ -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<AffiliateCategoryMap, Integer> {
public interface CategoryMappingRepository extends JpaRepository<CategoryMapping, Integer> {
// Match by source_type + source_value + platform (case-insensitive)
Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueAndPlatformIgnoreCase(
String sourceType,
String sourceValue,
String platform
);
// All mappings for a merchant, ordered nicely
List<CategoryMapping> findByMerchantIdOrderByRawCategoryPathAsc(Integer merchantId);
// Fallback: match by source_type + source_value when platform is null/ignored
Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueIgnoreCase(
String sourceType,
String sourceValue
);
// Used by AdminCategoryMappingController: list mappings for a given source_type + platform
List<AffiliateCategoryMap> 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<Merchant> findDistinctMerchantsWithMappings();
}

View File

@@ -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<PartCategory> 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);
}
}