Payflow API — Automated JaCoCo Code Coverage (90% Line / 73% Branch) GlobalExceptionHandler.java
package com.payflow.exception;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import org.springframework.validation.FieldError;
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.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import jakarta.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(UserNotFoundException.class)
public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/user-not-found"));
problem.setTitle("User Not Found");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(TransactionNotFoundException.class)
public ProblemDetail handleTransactionNotFound(TransactionNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/transaction-not-found"));
problem.setTitle("Transaction Not Found");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(DuplicateUpiIdException.class)
public ProblemDetail handleDuplicateUpiId(DuplicateUpiIdException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/duplicate-upi-id"));
problem.setTitle("Duplicate UPI ID");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(InsufficientBalanceException.class)
public ProblemDetail handleInsufficientBalance(InsufficientBalanceException ex) {
HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY;
ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/insufficient-balance"));
problem.setTitle("Insufficient Balance");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(SelfTransferException.class)
public ProblemDetail handleSelfTransfer(SelfTransferException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/self-transfer-prohibited"));
problem.setTitle("Self Transfer Prohibited");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(ForbiddenOperationException.class)
public ProblemDetail handleForbiddenOperation(ForbiddenOperationException ex) {
LOG.warn("Forbidden operation: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/forbidden-operation"));
problem.setTitle("Forbidden Operation");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(AccessDeniedException.class)
public ProblemDetail handleAccessDenied(AccessDeniedException ex) {
LOG.warn("Access denied: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/access-denied"));
problem.setTitle("Access Denied");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(FeatureDisabledException.class)
public ProblemDetail handleFeatureDisabled(FeatureDisabledException ex) {
LOG.warn("Feature disabled access attempt: {}", ex.getMessage());
HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
problem.setType(URI.create("https://api.payflow.com/errors/feature-disabled"));
problem.setTitle("Feature Disabled");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(InvalidUpiException.class)
public ProblemDetail handleInvalidUpi(InvalidUpiException ex) {
LOG.warn("Invalid UPI: {}", ex.getMessage());
String detail = ex.getMessage();
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, detail);
problem.setType(URI.create("https://api.payflow.com/errors/invalid-upi-id"));
problem.setTitle("Invalid UPI ID");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationException(MethodArgumentNotValidException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
"Validation failed for request parameters");
problem.setType(URI.create("https://api.payflow.com/errors/validation-error"));
problem.setTitle("Validation Failure");
Map<String, String> fieldErrors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
fieldErrors.put(error.getField(), error.getDefaultMessage());
}
problem.setProperty("errors", fieldErrors);
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(HandlerMethodValidationException.class)
public ProblemDetail handleHandlerMethodValidationException(HandlerMethodValidationException ex) {
LOG.warn("Handler method validation failed: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
"Validation failed for request parameters");
problem.setType(URI.create("https://api.payflow.com/errors/validation-error"));
problem.setTitle("Validation Failure");
Map<String, String> paramErrors = new HashMap<>();
for (var result : ex.getParameterValidationResults()) {
String paramName = result.getMethodParameter().getParameterName();
String fieldName = paramName != null ? paramName : "parameter";
for (var resolvableError : result.getResolvableErrors()) {
paramErrors.put(fieldName, resolvableError.getDefaultMessage());
}
}
if (!paramErrors.isEmpty()) {
problem.setProperty("errors", paramErrors);
}
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(ConstraintViolationException.class)
public ProblemDetail handleConstraintViolationException(ConstraintViolationException ex) {
LOG.warn("Constraint violation: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
"Validation constraint violated");
problem.setType(URI.create("https://api.payflow.com/errors/validation-error"));
problem.setTitle("Validation Failure");
Map<String, String> violations = new HashMap<>();
for (var violation : ex.getConstraintViolations()) {
var propPath = violation.getPropertyPath();
String path = propPath != null ? propPath.toString() : "parameter";
violations.put(path, violation.getMessage());
}
problem.setProperty("errors", violations);
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ProblemDetail handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) {
LOG.warn("Parameter type mismatch for {}: {}", ex.getName(), ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST,
String.format("Invalid value '%s' for parameter '%s'", ex.getValue(), ex.getName()));
problem.setType(URI.create("https://api.payflow.com/errors/type-mismatch"));
problem.setTitle("Parameter Type Mismatch");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ProblemDetail handleDataIntegrityViolation(DataIntegrityViolationException ex) {
LOG.warn("Data integrity violation: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT,
"Database constraint violation occurred");
problem.setType(URI.create("https://api.payflow.com/errors/data-integrity-violation"));
problem.setTitle("Data Integrity Violation");
enrichProblemDetail(problem);
return problem;
}
@ExceptionHandler(RequestNotPermitted.class)
public ResponseEntity<ProblemDetail> handleRequestNotPermitted(RequestNotPermitted ex) {
LOG.warn("Rate limit exceeded: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.TOO_MANY_REQUESTS,
"Rate limit exceeded. Maximum 10 requests per second allowed per user.");
problem.setType(URI.create("https://api.payflow.com/errors/rate-limit-exceeded"));
problem.setTitle("Rate Limit Exceeded");
enrichProblemDetail(problem);
var response = ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).header(HttpHeaders.RETRY_AFTER, "1");
return response.body(problem);
}
@ExceptionHandler(CallNotPermittedException.class)
public ResponseEntity<ProblemDetail> handleCallNotPermitted(CallNotPermittedException ex) {
LOG.error("Circuit breaker is open: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.SERVICE_UNAVAILABLE,
"External service circuit breaker is open. Service temporarily unavailable.");
problem.setType(URI.create("https://api.payflow.com/errors/service-unavailable"));
problem.setTitle("Service Unavailable");
enrichProblemDetail(problem);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(problem);
}
@ExceptionHandler(Exception.class)
public ProblemDetail handleUncaughtException(Exception ex) {
LOG.error("Unhandled server exception: ", ex);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected internal error occurred");
problem.setType(URI.create("https://api.payflow.com/errors/internal-server-error"));
problem.setTitle("Internal Server Error");
enrichProblemDetail(problem);
return problem;
}
private void enrichProblemDetail(ProblemDetail problem) {
problem.setProperty("timestamp", Instant.now().toString());
String requestId = MDC.get("requestId");
if (requestId != null) {
problem.setProperty("requestId", requestId);
}
}
}