Backend bug fixes #275

Merged
RecentRunner merged 5 commits from backend-fixes into main 2026-04-14 20:03:32 -06:00
18 changed files with 113 additions and 51 deletions

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.OpenRouterService;
import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.ContentFilter;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -49,6 +50,7 @@ public class AiChatController {
if (request.getMessage() == null || request.getMessage().isBlank()) {
return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty"));
}
ContentFilter.validate(request.getMessage());
User user = getCurrentUser();

View File

@@ -30,7 +30,7 @@ public class RefundController {
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')")
public ResponseEntity<RefundResponse> createRefund(@Valid @RequestBody RefundRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
@@ -85,7 +85,7 @@ public class RefundController {
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
@PreAuthorize("hasRole('STAFF')")
public ResponseEntity<RefundResponse> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus()));
}

View File

@@ -40,7 +40,7 @@ public class SaleController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
@PreAuthorize("hasRole('STAFF')")
public ResponseEntity<SaleResponse> createSale(@Valid @RequestBody SaleRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request));
}

View File

@@ -25,8 +25,9 @@ public class ServiceController {
@GetMapping
public ResponseEntity<Page<ServiceResponse>> getAllServices(
@RequestParam(required = false) String q,
@RequestParam(required = false) String species,
Pageable pageable) {
return ResponseEntity.ok(serviceService.getAllServices(q, pageable));
return ResponseEntity.ok(serviceService.getAllServices(q, species, pageable));
}
@GetMapping("/{id}")

View File

@@ -11,21 +11,19 @@ public class PurchaseOrderResponse {
private Long storeId;
private String storeName;
private LocalDate orderDate;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public PurchaseOrderResponse() {
}
public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, Long storeId, String storeName, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, Long storeId, String storeName, LocalDate orderDate, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.purchaseOrderId = purchaseOrderId;
this.supId = supId;
this.supplierName = supplierName;
this.storeId = storeId;
this.storeName = storeName;
this.orderDate = orderDate;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
@@ -78,14 +76,6 @@ public class PurchaseOrderResponse {
this.orderDate = orderDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
@@ -107,12 +97,12 @@ public class PurchaseOrderResponse {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PurchaseOrderResponse that = (PurchaseOrderResponse) o;
return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(status, that.status) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
}
@Override
public int hashCode() {
return Objects.hash(purchaseOrderId, supId, supplierName, storeId, storeName, orderDate, status, createdAt, updatedAt);
return Objects.hash(purchaseOrderId, supId, supplierName, storeId, storeName, orderDate, createdAt, updatedAt);
}
@Override
@@ -124,7 +114,6 @@ public class PurchaseOrderResponse {
", storeId=" + storeId +
", storeName='" + storeName + '\'' +
", orderDate=" + orderDate +
", status='" + status + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';

View File

@@ -27,9 +27,6 @@ public class PurchaseOrder {
@Column(nullable = false)
private LocalDate orderDate;
@Column(nullable = false, length = 50)
private String status;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -41,11 +38,10 @@ public class PurchaseOrder {
public PurchaseOrder() {
}
public PurchaseOrder(Long purchaseOrderId, Supplier supplier, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
public PurchaseOrder(Long purchaseOrderId, Supplier supplier, LocalDate orderDate, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.purchaseOrderId = purchaseOrderId;
this.supplier = supplier;
this.orderDate = orderDate;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
@@ -82,14 +78,6 @@ public class PurchaseOrder {
this.orderDate = orderDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
@@ -125,7 +113,6 @@ public class PurchaseOrder {
"purchaseOrderId=" + purchaseOrderId +
", supplier=" + supplier +
", orderDate=" + orderDate +
", status='" + status + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';

View File

@@ -42,9 +42,10 @@ public class GlobalExceptionHandler {
errors.put(fieldName, errorMessage);
});
String firstMessage = errors.values().stream().findFirst().orElse("Validation failed");
Map<String, Object> response = new HashMap<>();
response.put("status", HttpStatus.BAD_REQUEST.value());
response.put("message", "Validation failed");
response.put("message", firstMessage);
response.put("errors", errors);
response.put("details", buildDetails(ex));
response.put("path", request.getRequestURI());

View File

@@ -11,6 +11,10 @@ import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
boolean existsByProdNameIgnoreCase(String prodName);
boolean existsByProdNameIgnoreCaseAndProdIdNot(String prodName, Long prodId);
@Query("SELECT p FROM Product p WHERE " +
"(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:categoryId IS NULL OR p.category.categoryId = :categoryId)")

View File

@@ -11,8 +11,8 @@ import org.springframework.stereotype.Repository;
@Repository
public interface ServiceRepository extends JpaRepository<Service, Long> {
@Query("SELECT s FROM Service s WHERE " +
"LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(s.serviceDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Service> searchServices(@Param("q") String query, Pageable pageable);
@Query("SELECT DISTINCT s FROM Service s LEFT JOIN s.species sp WHERE " +
"(:q IS NULL OR LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(s.serviceDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(sp) = LOWER(:species))")
Page<Service> searchServices(@Param("q") String q, @Param("species") String species, Pageable pageable);
}

View File

@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class AdoptionService {
@@ -159,15 +160,37 @@ public class AdoptionService {
@Transactional
public void deleteAdoption(Long id) {
if (!adoptionRepository.existsById(id)) {
throw new ResourceNotFoundException("Adoption not found with id: " + id);
}
adoptionRepository.deleteById(id);
Adoption adoption = adoptionRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id));
Pet pet = adoption.getPet();
String status = adoption.getAdoptionStatus();
adoptionRepository.delete(adoption);
resetPetIfPending(pet, status);
}
@Transactional
public void bulkDeleteAdoptions(BulkDeleteRequest request) {
adoptionRepository.deleteAllById(request.getIds());
List<Adoption> adoptions = adoptionRepository.findAllById(request.getIds());
adoptionRepository.deleteAll(adoptions);
for (Adoption adoption : adoptions) {
resetPetIfPending(adoption.getPet(), adoption.getAdoptionStatus());
}
}
private void resetPetIfPending(Pet pet, String deletedAdoptionStatus) {
if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(deletedAdoptionStatus)) {
return;
}
if (!PET_STATUS_PENDING.equalsIgnoreCase(pet.getPetStatus())) {
return;
}
boolean completedElsewhere = adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(
pet.getPetId(), ADOPTION_STATUS_COMPLETED);
if (!completedElsewhere) {
pet.setPetStatus(PET_STATUS_AVAILABLE);
pet.setOwner(null);
petRepository.save(pet);
}
}
private String normalizeFilter(String value) {

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.entity.Appointment;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.AppointmentRepository;
@@ -28,6 +29,7 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@@ -108,6 +110,7 @@ public class AppointmentService {
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
validateStoreAccess(store.getStoreId(), authenticatedUser);
validatePetServiceCompatibility(pet, service);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
Appointment appointment = new Appointment();
@@ -147,6 +150,7 @@ public class AppointmentService {
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
validateStoreAccess(store.getStoreId(), authenticatedUser);
validatePetServiceCompatibility(pet, service);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
appointment.setCustomer(customer);
@@ -254,6 +258,17 @@ public class AppointmentService {
return trimmed.isEmpty() ? null : trimmed;
}
private void validatePetServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
if (pet == null) return;
Set<String> allowed = service.getSpecies();
if (allowed == null || allowed.isEmpty()) return;
boolean compatible = allowed.stream().anyMatch(s -> s.equalsIgnoreCase(pet.getPetSpecies()));
if (!compatible) {
throw new BusinessException(
"Service \"" + service.getServiceName() + "\" is not available for " + pet.getPetSpecies());
}
}
private void validateAppointmentRequest(AppointmentRequest request) {
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());

View File

@@ -9,6 +9,7 @@ import com.petshop.backend.entity.Conversation;
import com.petshop.backend.entity.Message;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.util.ContentFilter;
import com.petshop.backend.repository.ConversationRepository;
import com.petshop.backend.repository.MessageRepository;
import com.petshop.backend.repository.UserRepository;
@@ -138,6 +139,8 @@ public class ChatService {
}
}
ContentFilter.validate(request.getContent());
Message message = new Message();
message.setConversationId(conversationId);
message.setSenderId(userId);

View File

@@ -5,6 +5,7 @@ import com.petshop.backend.dto.product.ProductRequest;
import com.petshop.backend.dto.product.ProductResponse;
import com.petshop.backend.entity.Category;
import com.petshop.backend.entity.Product;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.CategoryRepository;
import com.petshop.backend.repository.ProductRepository;
@@ -45,6 +46,10 @@ public class ProductService {
@Transactional
public ProductResponse createProduct(ProductRequest request) {
if (productRepository.existsByProdNameIgnoreCase(request.getProdName())) {
throw new BusinessException("A product with this name already exists");
}
Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId()));
@@ -63,6 +68,10 @@ public class ProductService {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
if (productRepository.existsByProdNameIgnoreCaseAndProdIdNot(request.getProdName(), id)) {
throw new BusinessException("A product with this name already exists");
}
Category category = categoryRepository.findById(request.getCategoryId())
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId()));

View File

@@ -47,7 +47,6 @@ public class PurchaseOrderService {
store != null ? store.getStoreId() : null,
store != null ? store.getStoreName() : null,
purchaseOrder.getOrderDate(),
purchaseOrder.getStatus(),
purchaseOrder.getCreatedAt(),
purchaseOrder.getUpdatedAt()
);

View File

@@ -20,14 +20,10 @@ public class ServiceService {
}
@Transactional(readOnly = true)
public Page<ServiceResponse> getAllServices(String query, Pageable pageable) {
Page<com.petshop.backend.entity.Service> services;
if (query != null && !query.trim().isEmpty()) {
services = serviceRepository.searchServices(query, pageable);
} else {
services = serviceRepository.findAll(pageable);
}
return services.map(this::mapToResponse);
public Page<ServiceResponse> getAllServices(String query, String species, Pageable pageable) {
String q = (query != null && !query.trim().isEmpty()) ? query.trim() : null;
String sp = (species != null && !species.trim().isEmpty()) ? species.trim() : null;
return serviceRepository.searchServices(q, sp, pageable).map(this::mapToResponse);
}
@Transactional(readOnly = true)

View File

@@ -0,0 +1,30 @@
package com.petshop.backend.util;
import com.petshop.backend.exception.BusinessException;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
public class ContentFilter {
private static final Pattern SCRIPT_PATTERN = Pattern.compile(
"<script|javascript:|on\\w+\\s*=", Pattern.CASE_INSENSITIVE);
private static final Set<String> PROFANITY = Set.of(
"profanityOne", "profanityTwo", "profanityThree"
);
public static void validate(String input) {
if (input == null || input.isBlank()) return;
if (SCRIPT_PATTERN.matcher(input).find()) {
throw new BusinessException("Message contains prohibited content");
}
String lower = input.toLowerCase(Locale.ROOT);
for (String word : PROFANITY) {
if (lower.contains(word)) {
throw new BusinessException("Message contains prohibited language");
}
}
}
}

View File

@@ -68,6 +68,8 @@ openrouter:
model: ${OPENROUTER_MODEL:openrouter/free}
logging:
file:
name: ${LOG_FILE:log.txt}
level:
com.petshop: ${LOG_LEVEL:INFO}
org.springframework.security: ${LOG_LEVEL_SECURITY:WARN}

View File

@@ -0,0 +1 @@
ALTER TABLE purchaseOrder DROP COLUMN status;