fix audit report mismatches across backend and android

This commit is contained in:
2026-04-07 16:06:44 -06:00
parent 0173123898
commit 4500b213c6
22 changed files with 586 additions and 157 deletions

View File

@@ -153,6 +153,16 @@ public class DropdownController {
);
}
@GetMapping("/customers/{customerId}/pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomerPets(@PathVariable Long customerId) {
return ResponseEntity.ok(
petRepository.findAllByOwner_IdOrderByPetNameAsc(customerId).stream()
.map(p -> new DropdownOption(p.getPetId(), p.getPetName()))
.collect(Collectors.toList())
);
}
@GetMapping("/suppliers")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<DropdownOption>> getSuppliers() {

View File

@@ -48,12 +48,8 @@ public class PetImageController {
@GetMapping("/{id}/image")
public ResponseEntity<Resource> getPetImage(@PathVariable Long id) {
try {
PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole());
return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource());
} catch (PetService.ForbiddenImageAccessException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole());
return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource());
}
@DeleteMapping("/{id}/image")

View File

@@ -15,9 +15,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/refunds")
@@ -33,27 +31,20 @@ public class RefundController {
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> createRefund(@Valid @RequestBody RefundRequest request) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
public ResponseEntity<RefundResponse> createRefund(@Valid @RequestBody RefundRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId();
}
RefundResponse refund = refundService.createRefund(request, customerId);
return ResponseEntity.status(HttpStatus.CREATED).body(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId();
}
return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId));
}
@GetMapping
@@ -77,54 +68,32 @@ public class RefundController {
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> getRefundById(@PathVariable Long id) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
public ResponseEntity<RefundResponse> getRefundById(@PathVariable Long id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId();
}
RefundResponse refund = refundService.getRefundById(id, customerId);
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId();
}
return ResponseEntity.ok(refundService.getRefundById(id, customerId));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<?> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
try {
RefundResponse refund = refundService.updateRefundStatus(id, request.getStatus());
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
public ResponseEntity<RefundResponse> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus()));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteRefund(@PathVariable Long id) {
try {
refundService.deleteRefund(id);
Map<String, String> response = new HashMap<>();
response.put("message", "Refund deleted successfully");
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
public ResponseEntity<Void> deleteRefund(@PathVariable Long id) {
refundService.deleteRefund(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
@@ -13,7 +14,10 @@ import java.time.LocalDateTime;
@Component
public class ApiErrorResponder {
private final ObjectMapper objectMapper = JsonMapper.builder().findAndAddModules().build();
private final ObjectMapper objectMapper = JsonMapper.builder()
.findAndAddModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
public void write(HttpServletResponse response, HttpStatus status, String message, String details, String path) throws IOException {
response.setStatus(status.value());

View File

@@ -1,15 +1,19 @@
package com.petshop.backend.exception;
import com.petshop.backend.service.PetService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.core.PropertyReferenceException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.time.LocalDateTime;
import java.util.HashMap;
@@ -78,6 +82,26 @@ public class GlobalExceptionHandler {
return buildErrorResponse(HttpStatus.valueOf(ex.getStatusCode().value()), message, ex, request);
}
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNoResourceFound(NoResourceFoundException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.NOT_FOUND, "Route not found", ex, request);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiErrorResponse> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), ex, request);
}
@ExceptionHandler(PropertyReferenceException.class)
public ResponseEntity<ApiErrorResponse> handleBadSortProperty(PropertyReferenceException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.BAD_REQUEST, "Invalid sort field: " + ex.getPropertyName(), ex, request);
}
@ExceptionHandler(PetService.ForbiddenImageAccessException.class)
public ResponseEntity<ApiErrorResponse> handleForbiddenImageAccess(PetService.ForbiddenImageAccessException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.FORBIDDEN, "Access to this pet image is not allowed", ex, request);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleGenericException(Exception ex, HttpServletRequest request) {
String message = ex.getMessage() == null || ex.getMessage().isBlank()

View File

@@ -8,6 +8,8 @@ import com.petshop.backend.entity.Product;
import com.petshop.backend.entity.Refund;
import com.petshop.backend.entity.RefundItem;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.ProductRepository;
import com.petshop.backend.repository.RefundRepository;
import com.petshop.backend.repository.SaleRepository;
@@ -40,14 +42,14 @@ public class RefundService {
@Transactional
public RefundResponse createRefund(RefundRequest request, Long customerId) {
Sale sale = saleRepository.findById(request.getSaleId())
.orElseThrow(() -> new RuntimeException("Sale not found"));
.orElseThrow(() -> new BusinessException("Sale not found"));
if (sale.getCustomer() == null) {
throw new RuntimeException("Sale has no associated customer");
throw new BusinessException("Sale has no associated customer");
}
if (customerId != null && !sale.getCustomer().getId().equals(customerId)) {
throw new RuntimeException("You can only create refunds for your own purchases");
throw new BusinessException("You can only create refunds for your own purchases");
}
Refund refund = new Refund();
@@ -59,13 +61,13 @@ public class RefundService {
BigDecimal totalAmount = BigDecimal.ZERO;
for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProdId())
.orElseThrow(() -> new RuntimeException("Product not found: " + itemRequest.getProdId()));
.orElseThrow(() -> new BusinessException("Product not found: " + itemRequest.getProdId()));
BigDecimal unitPrice = sale.getItems().stream()
.filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId()))
.findFirst()
.map(item -> item.getUnitPrice())
.orElseThrow(() -> new RuntimeException("Product " + itemRequest.getProdId() + " was not in original sale"));
.orElseThrow(() -> new BusinessException("Product " + itemRequest.getProdId() + " was not in original sale"));
RefundItem refundItem = new RefundItem();
refundItem.setProduct(product);
@@ -84,10 +86,10 @@ public class RefundService {
@Transactional(readOnly = true)
public RefundResponse getRefundById(Long id, Long customerId) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
.orElseThrow(() -> new ResourceNotFoundException("Refund not found"));
if (customerId != null && !refund.getCustomerId().equals(customerId)) {
throw new RuntimeException("You can only view your own refunds");
throw new ResourceNotFoundException("You can only view your own refunds");
}
return toResponse(refund);
@@ -111,18 +113,18 @@ public class RefundService {
@Transactional
public RefundResponse updateRefundStatus(Long id, String status) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
.orElseThrow(() -> new ResourceNotFoundException("Refund not found"));
Refund.RefundStatus newStatus;
try {
newStatus = Refund.RefundStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid status: " + status);
throw new BusinessException("Invalid status: " + status);
}
if (refund.getStatus() == Refund.RefundStatus.PENDING && newStatus == Refund.RefundStatus.APPROVED) {
Sale originalSale = saleRepository.findById(refund.getSaleId())
.orElseThrow(() -> new RuntimeException("Original sale not found"));
.orElseThrow(() -> new ResourceNotFoundException("Original sale not found"));
SaleRequest saleRequest = new SaleRequest();
saleRequest.setStoreId(originalSale.getStore().getStoreId());
@@ -150,7 +152,7 @@ public class RefundService {
@Transactional
public void deleteRefund(Long id) {
if (!refundRepository.existsById(id)) {
throw new RuntimeException("Refund not found");
throw new ResourceNotFoundException("Refund not found");
}
refundRepository.deleteById(id);
}