mirror of
https://gitea.gofwd.group/Forward_Group/ballistic-builder-spring.git
synced 2025-12-06 02:56:44 -05:00
Compare commits
7 Commits
756a6791fc
...
sean
| Author | SHA1 | Date | |
|---|---|---|---|
| 2969cdfa23 | |||
| e986fa97ca | |||
| c283ec15b6 | |||
| 9096ddd165 | |||
| 3d1501cc87 | |||
| 3ae68f30c0 | |||
| f3626af709 |
31
.idea/dataSources.xml
generated
Normal file
31
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="r710" uuid="e6a29f5c-71d9-45f0-931b-554bcf8a94ba">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://r710.dev.gofwd.group:5433/postgres</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
<data-source source="LOCAL" name="ss_builder@r710.gofwd.group" uuid="e0fa459b-2f6c-45f1-9c41-66423c870df9">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<imported>true</imported>
|
||||||
|
<remarks>$PROJECT_DIR$/src/main/resources/application.properties</remarks>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://r710.gofwd.group:5433/ss_builder</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -24,14 +24,19 @@ public class SecurityConfig {
|
|||||||
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
)
|
)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
|
||||||
// Auth endpoints always open
|
// Auth endpoints always open
|
||||||
.requestMatchers("/api/auth/**").permitAll()
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
|
||||||
// Swagger / docs
|
// Swagger / docs
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
|
|
||||||
// Health
|
// Health
|
||||||
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
|
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
|
||||||
|
|
||||||
// Public product endpoints
|
// Public product endpoints
|
||||||
.requestMatchers("/api/products/gunbuilder/**").permitAll()
|
.requestMatchers("/api/products/gunbuilder/**").permitAll()
|
||||||
|
|
||||||
// Everything else (for now) also open – we can tighten later
|
// Everything else (for now) also open – we can tighten later
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package group.goforward.ballistic.controllers;
|
||||||
|
|
||||||
|
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/categories")
|
||||||
|
@CrossOrigin // you can tighten origins later
|
||||||
|
public class CategoryController {
|
||||||
|
|
||||||
|
private final PartCategoryRepository partCategories;
|
||||||
|
|
||||||
|
public CategoryController(PartCategoryRepository partCategories) {
|
||||||
|
this.partCategories = partCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PartCategoryDto> list() {
|
||||||
|
return partCategories.findAllByOrderByGroupNameAscSortOrderAscNameAsc()
|
||||||
|
.stream()
|
||||||
|
.map(pc -> new PartCategoryDto(
|
||||||
|
pc.getId(),
|
||||||
|
pc.getSlug(),
|
||||||
|
pc.getName(),
|
||||||
|
pc.getDescription(),
|
||||||
|
pc.getGroupName(),
|
||||||
|
pc.getSortOrder()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
package group.goforward.ballistic.controllers.admin;
|
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.model.PartCategory;
|
||||||
import group.goforward.ballistic.repos.CategoryMappingRepository;
|
import group.goforward.ballistic.repos.CategoryMappingRepository;
|
||||||
|
import group.goforward.ballistic.repos.MerchantRepository;
|
||||||
import group.goforward.ballistic.repos.PartCategoryRepository;
|
import group.goforward.ballistic.repos.PartCategoryRepository;
|
||||||
import group.goforward.ballistic.web.dto.admin.PartRoleMappingDto;
|
import group.goforward.ballistic.web.dto.admin.MerchantCategoryMappingDto;
|
||||||
import group.goforward.ballistic.web.dto.admin.PartRoleMappingRequest;
|
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.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
@@ -15,111 +17,101 @@ import java.util.List;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/admin/category-mappings")
|
@RequestMapping("/api/admin/category-mappings")
|
||||||
@CrossOrigin
|
@CrossOrigin // you can tighten origins later
|
||||||
public class AdminCategoryMappingController {
|
public class AdminCategoryMappingController {
|
||||||
|
|
||||||
private final CategoryMappingRepository categoryMappingRepository;
|
private final CategoryMappingRepository categoryMappingRepository;
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
private final PartCategoryRepository partCategoryRepository;
|
private final PartCategoryRepository partCategoryRepository;
|
||||||
|
|
||||||
public AdminCategoryMappingController(
|
public AdminCategoryMappingController(
|
||||||
CategoryMappingRepository categoryMappingRepository,
|
CategoryMappingRepository categoryMappingRepository,
|
||||||
|
MerchantRepository merchantRepository,
|
||||||
PartCategoryRepository partCategoryRepository
|
PartCategoryRepository partCategoryRepository
|
||||||
) {
|
) {
|
||||||
this.categoryMappingRepository = categoryMappingRepository;
|
this.categoryMappingRepository = categoryMappingRepository;
|
||||||
|
this.merchantRepository = merchantRepository;
|
||||||
this.partCategoryRepository = partCategoryRepository;
|
this.partCategoryRepository = partCategoryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/admin/category-mappings?platform=AR-15
|
/**
|
||||||
@GetMapping
|
* Merchants that have at least one category_mappings row.
|
||||||
public List<PartRoleMappingDto> list(
|
* Used for the "All Merchants" dropdown in the UI.
|
||||||
@RequestParam(name = "platform", defaultValue = "AR-15") String platform
|
*/
|
||||||
) {
|
@GetMapping("/merchants")
|
||||||
List<AffiliateCategoryMap> mappings =
|
public List<SimpleMerchantDto> listMerchantsWithMappings() {
|
||||||
categoryMappingRepository.findBySourceTypeAndPlatformOrderById("PART_ROLE", platform);
|
List<Merchant> merchants = categoryMappingRepository.findDistinctMerchantsWithMappings();
|
||||||
|
return merchants.stream()
|
||||||
return mappings.stream()
|
.map(m -> new SimpleMerchantDto(m.getId(), m.getName()))
|
||||||
.map(this::toDto)
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/admin/category-mappings
|
/**
|
||||||
@PostMapping
|
* List mappings for a specific merchant, or all mappings if no merchantId is provided.
|
||||||
public ResponseEntity<PartRoleMappingDto> create(
|
* GET /api/admin/category-mappings?merchantId=1
|
||||||
@RequestBody PartRoleMappingRequest request
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public List<MerchantCategoryMappingDto> listByMerchant(
|
||||||
|
@RequestParam(name = "merchantId", required = false) Integer merchantId
|
||||||
) {
|
) {
|
||||||
if (request.platform() == null || request.partRole() == null || request.categorySlug() == null) {
|
List<CategoryMapping> mappings;
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "platform, partRole, and categorySlug are required");
|
|
||||||
|
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())
|
return mappings.stream()
|
||||||
.orElseThrow(() -> new ResponseStatusException(
|
.map(cm -> new MerchantCategoryMappingDto(
|
||||||
HttpStatus.BAD_REQUEST,
|
cm.getId(),
|
||||||
"Unknown category slug: " + request.categorySlug()
|
cm.getMerchant().getId(),
|
||||||
));
|
cm.getMerchant().getName(),
|
||||||
|
cm.getRawCategoryPath(),
|
||||||
AffiliateCategoryMap mapping = new AffiliateCategoryMap();
|
cm.getPartCategory() != null ? cm.getPartCategory().getId() : null,
|
||||||
mapping.setSourceType("PART_ROLE");
|
cm.getPartCategory() != null ? cm.getPartCategory().getName() : null
|
||||||
mapping.setSourceValue(request.partRole());
|
))
|
||||||
mapping.setPlatform(request.platform());
|
.toList();
|
||||||
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}")
|
* Update a single mapping's part_category.
|
||||||
public PartRoleMappingDto update(
|
* POST /api/admin/category-mappings/{id}
|
||||||
|
* Body: { "partCategoryId": 24 }
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}")
|
||||||
|
public MerchantCategoryMappingDto updateMapping(
|
||||||
@PathVariable Integer id,
|
@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"));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found"));
|
||||||
|
|
||||||
if (request.platform() != null) {
|
PartCategory partCategory = null;
|
||||||
mapping.setPlatform(request.platform());
|
if (request.partCategoryId() != null) {
|
||||||
}
|
partCategory = partCategoryRepository.findById(request.partCategoryId())
|
||||||
if (request.partRole() != null) {
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Part category not found"));
|
||||||
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);
|
mapping.setPartCategory(partCategory);
|
||||||
return toDto(saved);
|
mapping = categoryMappingRepository.save(mapping);
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/admin/category-mappings/{id}
|
return new MerchantCategoryMappingDto(
|
||||||
@DeleteMapping("/{id}")
|
mapping.getId(),
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
mapping.getMerchant().getId(),
|
||||||
public void delete(@PathVariable Integer id) {
|
mapping.getMerchant().getName(),
|
||||||
if (!categoryMappingRepository.existsById(id)) {
|
mapping.getRawCategoryPath(),
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Mapping not found");
|
mapping.getPartCategory() != null ? mapping.getPartCategory().getId() : null,
|
||||||
}
|
mapping.getPartCategory() != null ? mapping.getPartCategory().getName() : null
|
||||||
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()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public MerchantCategoryMappingDto updateMappingPut(
|
||||||
|
@PathVariable Integer id,
|
||||||
|
@RequestBody UpdateMerchantCategoryMappingRequest request
|
||||||
|
) {
|
||||||
|
// just delegate so POST & PUT behave the same
|
||||||
|
return updateMapping(id, request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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<AdminPartRoleMappingDto> list(
|
||||||
|
@RequestParam(name = "platform", required = false) String platform
|
||||||
|
) {
|
||||||
|
List<PartRoleMapping> 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/model/CategoryMapping.java
|
||||||
|
package group.goforward.ballistic.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "category_mappings")
|
||||||
|
public class CategoryMapping {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "merchant_id", nullable = false)
|
||||||
|
private Merchant merchant;
|
||||||
|
|
||||||
|
@Column(name = "raw_category_path", nullable = false)
|
||||||
|
private String rawCategoryPath;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "part_category_id")
|
||||||
|
private PartCategory partCategory;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void onCreate() {
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
if (createdAt == null) {
|
||||||
|
createdAt = now;
|
||||||
|
}
|
||||||
|
if (updatedAt == null) {
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void onUpdate() {
|
||||||
|
this.updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- getters & setters ---
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Merchant getMerchant() {
|
||||||
|
return merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMerchant(Merchant merchant) {
|
||||||
|
this.merchant = merchant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRawCategoryPath() {
|
||||||
|
return rawCategoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRawCategoryPath(String rawCategoryPath) {
|
||||||
|
this.rawCategoryPath = rawCategoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PartCategory getPartCategory() {
|
||||||
|
return partCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPartCategory(PartCategory partCategory) {
|
||||||
|
this.partCategory = partCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,25 @@ import group.goforward.ballistic.model.ProductConfiguration;
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "products")
|
@Table(name = "products")
|
||||||
|
@NamedQuery(name="Products.findByPlatformWithBrand", query= "" +
|
||||||
|
"SELECT p FROM Product p" +
|
||||||
|
" JOIN FETCH p.brand b" +
|
||||||
|
" WHERE p.platform = :platform" +
|
||||||
|
" AND p.deletedAt IS NULL")
|
||||||
|
|
||||||
|
@NamedQuery(name="Product.findByPlatformAndPartRoleInWithBrand", 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")
|
||||||
|
|
||||||
|
@NamedQuery(name="Product.findProductsbyBrandByOffers", 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")
|
||||||
|
|
||||||
public class Product {
|
public class Product {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
|||||||
@@ -1,29 +1,22 @@
|
|||||||
package group.goforward.ballistic.repos;
|
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.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
import java.util.List;
|
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)
|
// All mappings for a merchant, ordered nicely
|
||||||
Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueAndPlatformIgnoreCase(
|
List<CategoryMapping> findByMerchantIdOrderByRawCategoryPathAsc(Integer merchantId);
|
||||||
String sourceType,
|
|
||||||
String sourceValue,
|
|
||||||
String platform
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fallback: match by source_type + source_value when platform is null/ignored
|
// Merchants that actually have mappings (for the dropdown)
|
||||||
Optional<AffiliateCategoryMap> findBySourceTypeAndSourceValueIgnoreCase(
|
@Query("""
|
||||||
String sourceType,
|
select distinct cm.merchant
|
||||||
String sourceValue
|
from CategoryMapping cm
|
||||||
);
|
order by cm.merchant.name asc
|
||||||
|
""")
|
||||||
// Used by AdminCategoryMappingController: list mappings for a given source_type + platform
|
List<Merchant> findDistinctMerchantsWithMappings();
|
||||||
List<AffiliateCategoryMap> findBySourceTypeAndPlatformOrderById(
|
|
||||||
String sourceType,
|
|
||||||
String platform
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -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<PartRoleMapping, Integer> {
|
||||||
|
|
||||||
|
// List mappings for a platform, ordered nicely for the UI
|
||||||
|
List<PartRoleMapping> findByPlatformOrderByPartRoleAsc(String platform);
|
||||||
|
}
|
||||||
@@ -33,6 +33,9 @@ public interface ProductRepository extends JpaRepository<Product, Integer> {
|
|||||||
""")
|
""")
|
||||||
List<Product> findByPlatformWithBrand(@Param("platform") String platform);
|
List<Product> findByPlatformWithBrand(@Param("platform") String platform);
|
||||||
|
|
||||||
|
@Query(name="Products.findByPlatformWithBrand")
|
||||||
|
List<Product> findByPlatformWithBrandNQ(@Param("platform") String platform);
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT p
|
SELECT p
|
||||||
FROM Product p
|
FROM Product p
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package group.goforward.ballistic.services;
|
package group.goforward.ballistic.services;
|
||||||
|
|
||||||
import group.goforward.ballistic.model.AffiliateCategoryMap;
|
|
||||||
import group.goforward.ballistic.model.PartCategory;
|
import group.goforward.ballistic.model.PartCategory;
|
||||||
import group.goforward.ballistic.repos.CategoryMappingRepository;
|
import group.goforward.ballistic.repos.PartCategoryRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -10,29 +9,33 @@ import java.util.Optional;
|
|||||||
@Service
|
@Service
|
||||||
public class PartCategoryResolverService {
|
public class PartCategoryResolverService {
|
||||||
|
|
||||||
private final CategoryMappingRepository categoryMappingRepository;
|
private final PartCategoryRepository partCategoryRepository;
|
||||||
|
|
||||||
public PartCategoryResolverService(CategoryMappingRepository categoryMappingRepository) {
|
public PartCategoryResolverService(PartCategoryRepository partCategoryRepository) {
|
||||||
this.categoryMappingRepository = categoryMappingRepository;
|
this.partCategoryRepository = partCategoryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a part category from a platform + partRole (what gunbuilder cares about).
|
* Resolve the canonical PartCategory for a given platform + partRole.
|
||||||
* Returns Optional.empty() if we have no mapping yet.
|
*
|
||||||
|
* 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) {
|
public Optional<PartCategory> resolveForPlatformAndPartRole(String platform, String partRole) {
|
||||||
// sourceType is a convention – you can also enum this
|
if (partRole == null || partRole.isBlank()) {
|
||||||
String sourceType = "PART_ROLE";
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
// First try with platform
|
String normalizedSlug = partRole
|
||||||
return categoryMappingRepository
|
.trim()
|
||||||
.findBySourceTypeAndSourceValueAndPlatformIgnoreCase(sourceType, partRole, platform)
|
.toLowerCase()
|
||||||
.map(AffiliateCategoryMap::getPartCategory)
|
.replace(" ", "-");
|
||||||
// if that fails, fall back to ANY platform
|
|
||||||
.or(() ->
|
return partCategoryRepository.findBySlug(normalizedSlug);
|
||||||
categoryMappingRepository
|
|
||||||
.findBySourceTypeAndSourceValueIgnoreCase(sourceType, partRole)
|
|
||||||
.map(AffiliateCategoryMap::getPartCategory)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
public record CreatePartRoleMappingRequest(
|
||||||
|
String platform,
|
||||||
|
String partRole,
|
||||||
|
String categorySlug,
|
||||||
|
String notes
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// src/main/java/group/goforward/ballistic/web/dto/admin/MerchantCategoryMappingDto.java
|
||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
public record MerchantCategoryMappingDto(
|
||||||
|
Integer id,
|
||||||
|
Integer merchantId,
|
||||||
|
String merchantName,
|
||||||
|
String rawCategoryPath,
|
||||||
|
Integer partCategoryId,
|
||||||
|
String partCategoryName
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
public record SimpleMerchantDto(
|
||||||
|
Integer id,
|
||||||
|
String name
|
||||||
|
) { }
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
public record UpdateMerchantCategoryMappingRequest(
|
||||||
|
Integer partCategoryId
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package group.goforward.ballistic.web.dto.admin;
|
||||||
|
|
||||||
|
public record UpdatePartRoleMappingRequest(
|
||||||
|
String platform,
|
||||||
|
String partRole,
|
||||||
|
String categorySlug,
|
||||||
|
String notes
|
||||||
|
) {}
|
||||||
Reference in New Issue
Block a user