Merge pull request #275 from RecentRunner/backend-fixes
Backend bug fixes
This commit is contained in:
@@ -8,6 +8,7 @@ import com.petshop.backend.repository.PetRepository;
|
|||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
import com.petshop.backend.service.OpenRouterService;
|
import com.petshop.backend.service.OpenRouterService;
|
||||||
import com.petshop.backend.util.AuthenticationHelper;
|
import com.petshop.backend.util.AuthenticationHelper;
|
||||||
|
import com.petshop.backend.util.ContentFilter;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -49,6 +50,7 @@ public class AiChatController {
|
|||||||
if (request.getMessage() == null || request.getMessage().isBlank()) {
|
if (request.getMessage() == null || request.getMessage().isBlank()) {
|
||||||
return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty"));
|
return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty"));
|
||||||
}
|
}
|
||||||
|
ContentFilter.validate(request.getMessage());
|
||||||
|
|
||||||
User user = getCurrentUser();
|
User user = getCurrentUser();
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ public class ServiceController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<Page<ServiceResponse>> getAllServices(
|
public ResponseEntity<Page<ServiceResponse>> getAllServices(
|
||||||
@RequestParam(required = false) String q,
|
@RequestParam(required = false) String q,
|
||||||
|
@RequestParam(required = false) String species,
|
||||||
Pageable pageable) {
|
Pageable pageable) {
|
||||||
return ResponseEntity.ok(serviceService.getAllServices(q, pageable));
|
return ResponseEntity.ok(serviceService.getAllServices(q, species, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
|||||||
@@ -11,21 +11,19 @@ public class PurchaseOrderResponse {
|
|||||||
private Long storeId;
|
private Long storeId;
|
||||||
private String storeName;
|
private String storeName;
|
||||||
private LocalDate orderDate;
|
private LocalDate orderDate;
|
||||||
private String status;
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
public PurchaseOrderResponse() {
|
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.purchaseOrderId = purchaseOrderId;
|
||||||
this.supId = supId;
|
this.supId = supId;
|
||||||
this.supplierName = supplierName;
|
this.supplierName = supplierName;
|
||||||
this.storeId = storeId;
|
this.storeId = storeId;
|
||||||
this.storeName = storeName;
|
this.storeName = storeName;
|
||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
this.status = status;
|
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -78,14 +76,6 @@ public class PurchaseOrderResponse {
|
|||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -107,12 +97,12 @@ public class PurchaseOrderResponse {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
PurchaseOrderResponse that = (PurchaseOrderResponse) o;
|
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
|
@Override
|
||||||
public int hashCode() {
|
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
|
@Override
|
||||||
@@ -124,7 +114,6 @@ public class PurchaseOrderResponse {
|
|||||||
", storeId=" + storeId +
|
", storeId=" + storeId +
|
||||||
", storeName='" + storeName + '\'' +
|
", storeName='" + storeName + '\'' +
|
||||||
", orderDate=" + orderDate +
|
", orderDate=" + orderDate +
|
||||||
", status='" + status + '\'' +
|
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ public class PurchaseOrder {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDate orderDate;
|
private LocalDate orderDate;
|
||||||
|
|
||||||
@Column(nullable = false, length = 50)
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -41,11 +38,10 @@ public class PurchaseOrder {
|
|||||||
public 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.purchaseOrderId = purchaseOrderId;
|
||||||
this.supplier = supplier;
|
this.supplier = supplier;
|
||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
this.status = status;
|
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -82,14 +78,6 @@ public class PurchaseOrder {
|
|||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -125,7 +113,6 @@ public class PurchaseOrder {
|
|||||||
"purchaseOrderId=" + purchaseOrderId +
|
"purchaseOrderId=" + purchaseOrderId +
|
||||||
", supplier=" + supplier +
|
", supplier=" + supplier +
|
||||||
", orderDate=" + orderDate +
|
", orderDate=" + orderDate +
|
||||||
", status='" + status + '\'' +
|
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface ServiceRepository extends JpaRepository<Service, Long> {
|
public interface ServiceRepository extends JpaRepository<Service, Long> {
|
||||||
|
|
||||||
@Query("SELECT s FROM Service s WHERE " +
|
@Query("SELECT DISTINCT s FROM Service s LEFT JOIN s.species sp WHERE " +
|
||||||
"LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
"(:q IS NULL OR LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(s.serviceDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||||
"LOWER(s.serviceDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
|
"(:species IS NULL OR LOWER(sp) = LOWER(:species))")
|
||||||
Page<Service> searchServices(@Param("q") String query, Pageable pageable);
|
Page<Service> searchServices(@Param("q") String q, @Param("species") String species, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AdoptionService {
|
public class AdoptionService {
|
||||||
@@ -159,15 +160,37 @@ public class AdoptionService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteAdoption(Long id) {
|
public void deleteAdoption(Long id) {
|
||||||
if (!adoptionRepository.existsById(id)) {
|
Adoption adoption = adoptionRepository.findById(id)
|
||||||
throw new ResourceNotFoundException("Adoption not found with id: " + id);
|
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id));
|
||||||
}
|
Pet pet = adoption.getPet();
|
||||||
adoptionRepository.deleteById(id);
|
String status = adoption.getAdoptionStatus();
|
||||||
|
adoptionRepository.delete(adoption);
|
||||||
|
resetPetIfPending(pet, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void bulkDeleteAdoptions(BulkDeleteRequest request) {
|
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) {
|
private String normalizeFilter(String value) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.petshop.backend.entity.Appointment;
|
|||||||
import com.petshop.backend.entity.Pet;
|
import com.petshop.backend.entity.Pet;
|
||||||
import com.petshop.backend.entity.StoreLocation;
|
import com.petshop.backend.entity.StoreLocation;
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.exception.BusinessException;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
import com.petshop.backend.repository.AdoptionRepository;
|
import com.petshop.backend.repository.AdoptionRepository;
|
||||||
import com.petshop.backend.repository.AppointmentRepository;
|
import com.petshop.backend.repository.AppointmentRepository;
|
||||||
@@ -28,6 +29,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -108,6 +110,7 @@ public class AppointmentService {
|
|||||||
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||||
|
|
||||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||||
|
validatePetServiceCompatibility(pet, service);
|
||||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
||||||
|
|
||||||
Appointment appointment = new Appointment();
|
Appointment appointment = new Appointment();
|
||||||
@@ -147,6 +150,7 @@ public class AppointmentService {
|
|||||||
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||||
|
|
||||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||||
|
validatePetServiceCompatibility(pet, service);
|
||||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
||||||
|
|
||||||
appointment.setCustomer(customer);
|
appointment.setCustomer(customer);
|
||||||
@@ -254,6 +258,17 @@ public class AppointmentService {
|
|||||||
return trimmed.isEmpty() ? null : trimmed;
|
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) {
|
private void validateAppointmentRequest(AppointmentRequest request) {
|
||||||
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
|
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
|
||||||
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
|
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.petshop.backend.entity.Conversation;
|
|||||||
import com.petshop.backend.entity.Message;
|
import com.petshop.backend.entity.Message;
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
|
import com.petshop.backend.util.ContentFilter;
|
||||||
import com.petshop.backend.repository.ConversationRepository;
|
import com.petshop.backend.repository.ConversationRepository;
|
||||||
import com.petshop.backend.repository.MessageRepository;
|
import com.petshop.backend.repository.MessageRepository;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
@@ -138,6 +139,8 @@ public class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentFilter.validate(request.getContent());
|
||||||
|
|
||||||
Message message = new Message();
|
Message message = new Message();
|
||||||
message.setConversationId(conversationId);
|
message.setConversationId(conversationId);
|
||||||
message.setSenderId(userId);
|
message.setSenderId(userId);
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ public class PurchaseOrderService {
|
|||||||
store != null ? store.getStoreId() : null,
|
store != null ? store.getStoreId() : null,
|
||||||
store != null ? store.getStoreName() : null,
|
store != null ? store.getStoreName() : null,
|
||||||
purchaseOrder.getOrderDate(),
|
purchaseOrder.getOrderDate(),
|
||||||
purchaseOrder.getStatus(),
|
|
||||||
purchaseOrder.getCreatedAt(),
|
purchaseOrder.getCreatedAt(),
|
||||||
purchaseOrder.getUpdatedAt()
|
purchaseOrder.getUpdatedAt()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,14 +20,10 @@ public class ServiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<ServiceResponse> getAllServices(String query, Pageable pageable) {
|
public Page<ServiceResponse> getAllServices(String query, String species, Pageable pageable) {
|
||||||
Page<com.petshop.backend.entity.Service> services;
|
String q = (query != null && !query.trim().isEmpty()) ? query.trim() : null;
|
||||||
if (query != null && !query.trim().isEmpty()) {
|
String sp = (species != null && !species.trim().isEmpty()) ? species.trim() : null;
|
||||||
services = serviceRepository.searchServices(query, pageable);
|
return serviceRepository.searchServices(q, sp, pageable).map(this::mapToResponse);
|
||||||
} else {
|
|
||||||
services = serviceRepository.findAll(pageable);
|
|
||||||
}
|
|
||||||
return services.map(this::mapToResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE purchaseOrder DROP COLUMN status;
|
||||||
Reference in New Issue
Block a user