diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java b/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java new file mode 100644 index 0000000..79de96e --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/comment/controller/CommentController.java @@ -0,0 +1,37 @@ +package org.example.visacasemanagementsystem.comment.controller; + +import jakarta.validation.Valid; +import org.example.visacasemanagementsystem.comment.dto.CommentDTO; +import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; +import org.example.visacasemanagementsystem.comment.service.CommentService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/comments") +public class CommentController { + + private final CommentService commentService; + + public CommentController(CommentService commentService) { + this.commentService = commentService; + } + + // Create new comment + @PostMapping + public ResponseEntity createComment(@Valid @RequestBody CreateCommentDTO dto){ + CommentDTO createComment = commentService.createComment(dto); + return new ResponseEntity<>(createComment, HttpStatus.CREATED); + } + + // Get all comments for a selected visa case + @GetMapping("/visa/{visaId}") + public ResponseEntity> getCommentsByVisa(@PathVariable Long visaId){ + List comments = commentService.getCommentsByVisaId(visaId); + return new ResponseEntity<>(comments, HttpStatus.OK); + + } +} diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java b/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java index e02f81f..50fde2c 100644 --- a/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/comment/dto/CreateCommentDTO.java @@ -5,7 +5,7 @@ public record CreateCommentDTO( @NotNull(message = "Visa ID is required") Long visaId, - @NotNull(message = "Author ID is required") Long authorId, + @NotNull(message = "Author ID is required") Long authorId, // Todo: Remove this and let Spring Security handle it @NotBlank(message = "Comment text cannot be empty") String text ) { } diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/repository/CommentRepository.java b/src/main/java/org/example/visacasemanagementsystem/comment/repository/CommentRepository.java new file mode 100644 index 0000000..8c692ac --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/comment/repository/CommentRepository.java @@ -0,0 +1,11 @@ +package org.example.visacasemanagementsystem.comment.repository; + +import org.example.visacasemanagementsystem.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + + List findByVisaIdOrderByCreatedAtDesc(Long visaId); +} diff --git a/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java b/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java new file mode 100644 index 0000000..616799a --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java @@ -0,0 +1,82 @@ +package org.example.visacasemanagementsystem.comment.service; + +import org.example.visacasemanagementsystem.comment.dto.CommentDTO; +import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO; +import org.example.visacasemanagementsystem.comment.entity.Comment; +import org.example.visacasemanagementsystem.comment.mapper.CommentMapper; +import org.example.visacasemanagementsystem.comment.repository.CommentRepository; +import org.example.visacasemanagementsystem.exception.ResourceNotFoundException; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.repository.UserRepository; +import org.example.visacasemanagementsystem.visa.entity.Visa; +import org.example.visacasemanagementsystem.visa.repository.VisaRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class CommentService { + + private final CommentRepository commentRepository; + private final CommentMapper commentMapper; + private final UserRepository userRepository; + private final VisaRepository visaRepository; + + public CommentService(CommentRepository commentRepository, CommentMapper commentMapper , UserRepository userRepository, VisaRepository visaRepository) { + this.commentRepository = commentRepository; + this.commentMapper = commentMapper; + this.userRepository = userRepository; + this.visaRepository = visaRepository; + } + + + // Create Comment + // TODO: Security Risk - Replace authorId from DTO with authenticated user from + // SecurityContext once Spring Security is integrated to prevent IDOR vulnerabilities. + @Transactional + public CommentDTO createComment(CreateCommentDTO dto) { + if (dto == null) { + throw new IllegalArgumentException("Comment payload cannot be null"); + } + + if (dto.text() == null || dto.text().isBlank()) { + throw new IllegalArgumentException("Comment text cannot be empty"); + } + + // Get User and Visa from database + User author = userRepository.findById(dto.authorId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + dto.authorId())); + + Visa visa = visaRepository.findById(dto.visaId()) + .orElseThrow(() -> new ResourceNotFoundException("Visa case not found with id: " + dto.visaId())); + + // Map DTO to Entity + Comment comment = commentMapper.toEntity(dto); + + // Set author and visaCase + comment.setAuthor(author); + comment.setVisa(visa); + + // Save and return + Comment savedComment = commentRepository.save(comment); + return commentMapper.toDTO(savedComment); + } + + // Get all comments for a selected visa case + @Transactional (readOnly = true) + public List getCommentsByVisaId(Long visaId) { + if (visaId == null || visaId <= 0) { + throw new IllegalArgumentException("Visa ID must be a positive number"); + } + + List comments = commentRepository.findByVisaIdOrderByCreatedAtDesc(visaId); + if(comments.isEmpty() && !visaRepository.existsById(visaId)) { + throw new ResourceNotFoundException("Visa case not found with id: " + visaId); + } + + return comments.stream() + .map(commentMapper::toDTO) + .toList(); + } +} diff --git a/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java b/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java index 527c38b..7177ae0 100644 --- a/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java @@ -18,9 +18,16 @@ public String handleUnauthorizedException(UnauthorizedException exception) { } @ResponseStatus(value = HttpStatus.NOT_FOUND) - @ExceptionHandler(EntityNotFoundException.class) + @ExceptionHandler({EntityNotFoundException.class, ResourceNotFoundException.class}) @ResponseBody - public String handleNotFoundException(EntityNotFoundException exception) { + public String handleNotFoundException(RuntimeException exception) { return exception.getMessage(); } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + @ResponseBody + public String handleIllegalArgumentException(IllegalArgumentException exception) { + return "Invalid Request: " + exception.getMessage(); + } } diff --git a/src/main/java/org/example/visacasemanagementsystem/exception/ResourceNotFoundException.java b/src/main/java/org/example/visacasemanagementsystem/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..c9c8de9 --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package org.example.visacasemanagementsystem.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +}