From 88b923c12ba1f6c5b89ad155f6269a7719664443 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Fri, 10 Apr 2026 15:07:08 +0200 Subject: [PATCH 01/21] feat: implement endpoints for VisaViewController and basic Thymeleaf templates - Added endpoints for to show and manage visa applications - Added missing fields from Visa Entity to DTO and mapper - Added start of thymeleaf templates to display visa cases, apply for a new visa and show further details about a case --- .../visa/controller/VisaViewController.java | 129 ++++++++++++++++++ .../visa/dto/VisaDTO.java | 4 + .../visa/mapper/VisaMapper.java | 2 + .../visa/repository/VisaRepository.java | 2 + .../visa/service/VisaService.java | 14 ++ .../resources/templates/visa/apply-form.html | 63 +++++++++ .../resources/templates/visa/dashboard.html | 92 +++++++++++++ .../resources/templates/visa/details.html | 104 ++++++++++++++ 8 files changed, 410 insertions(+) create mode 100644 src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java create mode 100644 src/main/resources/templates/visa/apply-form.html create mode 100644 src/main/resources/templates/visa/dashboard.html create mode 100644 src/main/resources/templates/visa/details.html diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java new file mode 100644 index 0000000..530cb8a --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -0,0 +1,129 @@ +package org.example.visacasemanagementsystem.visa.controller; + +import jakarta.persistence.EntityNotFoundException; +import org.example.visacasemanagementsystem.comment.service.CommentService; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.dto.UserDTO; +import org.example.visacasemanagementsystem.user.service.UserService; +import org.example.visacasemanagementsystem.visa.VisaType; +import org.example.visacasemanagementsystem.visa.dto.CreateVisaDTO; +import org.example.visacasemanagementsystem.visa.dto.VisaDTO; +import org.example.visacasemanagementsystem.visa.service.VisaService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Controller +@RequestMapping("/visas") +public class VisaViewController { + + private final VisaService visaService; + private final CommentService commentService; + private final UserService userService; + + public VisaViewController(VisaService visaService, CommentService commentService, UserService userService) { + this.visaService = visaService; + this.commentService = commentService; + this.userService = userService; + } + + // ENDPOINTS + + // GET/visas/dashboard -> Huvudsidan. Visa listan på visum (alla för admin, egna för användaren) + + @GetMapping("/dashboard") + public String showDashboard(@RequestParam Long currentUserId, Model model) { + // Find current user + UserDTO user = userService.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("User not found.")); + + List visas; + + // If the current user = ADMIN/SYSADMIN show everything + if (user.userAuthorization() == UserAuthorization.ADMIN || + user.userAuthorization() == UserAuthorization.SYSADMIN) { + visas = visaService.findAll(); + } else { + // Normal user/applicant can only se their own visa applications + visas = visaService.findVisasByApplicant(currentUserId); + } + + // Send data to Thymeleaf + model.addAttribute("visas", visas); + model.addAttribute("currentUser", user); + + return "visa/dashboard"; + + } + + // GET/visas/{id} -> Detaljvyn. Visa all info om ett ärende + dess kommentarer +@GetMapping("/{id}") + public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserId , Model model) { + // Get visa + VisaDTO visa = visaService.findVisaDtoById(id); + + // Get comments + var comments = commentService.getCommentsByVisaId(id); + + // Get user + UserDTO user = userService.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("User not found.")); + + model.addAttribute("visa", visa); + model.addAttribute("comments", comments); + model.addAttribute("currentUser", user); + + return "visa/details"; + } + + // GET/visas/apply -> Ansökningssidan. Visa formuläret för att söka ett nytt visum + @GetMapping("/apply") + public String showApplyForm(@RequestParam Long currentUserId, Model model) { + UserDTO user = userService.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("User not found.")); + + model.addAttribute("currentUser", user); + model.addAttribute("visaTypes", VisaType.values()); + return "visa/apply-form"; + } + + // POST/visas/apply -> Skicka ansökan. Tar emot förmulärdatan och sparar via VisaService + @PostMapping("/apply") + public String submitApplication(@ModelAttribute CreateVisaDTO createVisaDTO, + @RequestParam Long currentUserId) { + visaService.applyForVisa(createVisaDTO, currentUserId); + return "redirect:/visas/dashboard?currentUserId=" + currentUserId; + } + + // POST/visas/{id}/approve -> Beslut. (Admin) Godkänner visumet via en knapp i detaljvyn + @PostMapping("/{id}/approve") + public String approveVisa(@PathVariable Long id, @RequestParam Long currentUserId) { + visaService.approveVisa(id, currentUserId); + return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; + } + + // POST/visas/{id}/reject -> Beslut (Admin) Avvisar visumet med en motivering + @PostMapping("/{id}/reject") + public String rejectVisa(@PathVariable Long id, + @RequestParam Long currentUserId, + @RequestParam String reason) { + visaService.rejectVisa(id, currentUserId, reason); + return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; + } + + // ASSIGN - Admin tar på sig ett ärende + @PostMapping("/{id}/assign") + public String assignCaseToHandler(@PathVariable Long id, @RequestParam Long currentUserId) { + visaService.assignHandler(id, currentUserId); + return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; + } + + + + + + + +} diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java b/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java index 4df5019..7d5adc2 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java @@ -3,6 +3,8 @@ import org.example.visacasemanagementsystem.visa.VisaStatus; import org.example.visacasemanagementsystem.visa.VisaType; +import java.time.LocalDateTime; + public record VisaDTO( Long id, VisaType visaType, @@ -11,5 +13,7 @@ public record VisaDTO( Long applicantId, String applicantName, Long handlerId, + LocalDateTime createdAt, + LocalDateTime updatedAt, String rejectionReason ) {} diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java index cee6827..162c73b 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java @@ -21,6 +21,8 @@ public VisaDTO toDTO(Visa visa) { visa.getApplicant() != null ? visa.getApplicant().getId() : null, visa.getApplicant() != null ? visa.getApplicant().getFullName() : "Unknown", visa.getHandler() != null ? visa.getHandler().getId() : null, + visa.getCreatedAt(), + visa.getUpdatedAt(), visa.getRejectionReason() ); diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java b/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java index 63a23fc..2ee1542 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java @@ -24,6 +24,8 @@ public interface VisaRepository extends JpaRepository { List findByHandler_IdAndVisaStatus(Long handlerId, VisaStatus visaStatus, Sort sort); + List findByApplicantId(Long applicantId, Sort sort); + } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index e0937fc..74bc87a 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -54,6 +54,20 @@ private Visa findVisaById(Long id) { .orElseThrow(() -> new EntityNotFoundException(NOT_FOUND_MESSAGE)); } + public VisaDTO findVisaDtoById(Long id) { + Visa visa = visaRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(NOT_FOUND_MESSAGE)); + + return visaMapper.toDTO(visa); + } + + public List findVisasByApplicant(Long applicantId) { + return visaRepository.findByApplicantId(applicantId, Sort.by("createdAt").descending()) + .stream() + .map(visaMapper::toDTO) + .toList(); + } + public List findVisaByType(VisaType visaType) { return visaRepository.findByVisaType(visaType, Sort.by("visaType").descending()) .stream() diff --git a/src/main/resources/templates/visa/apply-form.html b/src/main/resources/templates/visa/apply-form.html new file mode 100644 index 0000000..dece78f --- /dev/null +++ b/src/main/resources/templates/visa/apply-form.html @@ -0,0 +1,63 @@ + + + + + New Visa Application + + + + +
+
+

New Application

+

Logged in as User

+
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+ + + + Cancel +
+
+ + + diff --git a/src/main/resources/templates/visa/dashboard.html b/src/main/resources/templates/visa/dashboard.html new file mode 100644 index 0000000..c081b85 --- /dev/null +++ b/src/main/resources/templates/visa/dashboard.html @@ -0,0 +1,92 @@ + + + + + Visa Dashboard + + + + +
+
+
+

Visa Overview

+ +
+ + + New Application + +
+ +
+ + + + + + + + + + + + + + + + + + + +
ReferenceVisa TypeStatusSubmitted
#101STUDENT + + SUBMITTED + + Current date + + Details → + +
+ +
+ No applications found. +
+
+
+ + + diff --git a/src/main/resources/templates/visa/details.html b/src/main/resources/templates/visa/details.html new file mode 100644 index 0000000..9a815a7 --- /dev/null +++ b/src/main/resources/templates/visa/details.html @@ -0,0 +1,104 @@ + + + + + Visa Case Details + + + + +
+ ← Back to Dashboard + +
+

Case #101

+ +
+ +
+
+ Applicant + John Doe +
+
+ Status + SUBMITTED +
+
+ Visa Type + STUDENT +
+
+ Submitted + 2023-10-27 +
+
+
+ +
+ +
+ +
+ +
+ + +
+ + +
+

Comments

+ +
+
+ Admin • + Oct 27, 14:00 +
+
This is a comment
+
+ +
+ No comments yet. +
+ +
+
+ + + + +
+
+
+
+ + + From 30ce174954709eaf940133e21d03995cc838220c Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sat, 11 Apr 2026 17:41:42 +0200 Subject: [PATCH 02/21] feat: extend Global exception handler to handle Internal Server Error --- .../exception/GlobalExceptionHandler.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java b/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java index 7177ae0..ac3351c 100644 --- a/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package org.example.visacasemanagementsystem.exception; import jakarta.persistence.EntityNotFoundException; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -8,6 +9,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice +@Slf4j public class GlobalExceptionHandler { @ResponseStatus(value = HttpStatus.FORBIDDEN) @@ -30,4 +32,12 @@ public String handleNotFoundException(RuntimeException exception) { public String handleIllegalArgumentException(IllegalArgumentException exception) { return "Invalid Request: " + exception.getMessage(); } + + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + @ResponseBody + public String handleAllUncaughtErrors(Exception exception) { + log.error("Unexpected Error: ", exception); + return exception.getMessage(); + } } From a25a98447b9cc5fdf0f2c612f01dd0a121ea8dc4 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sat, 11 Apr 2026 17:49:44 +0200 Subject: [PATCH 03/21] feat: update visa workflow with assigned status, handler mapping, and role-based views - Removed UNASSIGNED from VisaStatus for improved flow - Replaced Reject reason with statusInformation - Added logic for admin users to request further information from applicants --- .../visa/VisaStatus.java | 1 - .../visa/controller/VisaViewController.java | 7 --- .../visa/dto/VisaDTO.java | 4 +- .../visa/entity/Visa.java | 6 +- .../visa/mapper/VisaMapper.java | 3 +- .../visa/service/VisaService.java | 63 +++++++++++++++---- .../resources/templates/visa/dashboard.html | 36 ++++++++--- .../resources/templates/visa/details.html | 46 +++++++++----- 8 files changed, 113 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/VisaStatus.java b/src/main/java/org/example/visacasemanagementsystem/visa/VisaStatus.java index d2e01ac..8401752 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/VisaStatus.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/VisaStatus.java @@ -2,7 +2,6 @@ public enum VisaStatus { SUBMITTED, - UNASSIGNED, ASSIGNED, INCOMPLETE, GRANTED, diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index 530cb8a..f0a5675 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -119,11 +119,4 @@ public String assignCaseToHandler(@PathVariable Long id, @RequestParam Long curr visaService.assignHandler(id, currentUserId); return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; } - - - - - - - } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java b/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java index 7d5adc2..4becf08 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java @@ -13,7 +13,7 @@ public record VisaDTO( Long applicantId, String applicantName, Long handlerId, + String handlerName, LocalDateTime createdAt, LocalDateTime updatedAt, - String rejectionReason -) {} + String statusInformation) {} diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java index 31905b0..2170678 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java @@ -54,15 +54,13 @@ public class Visa { @Column(name = "updated_at") private LocalDateTime updatedAt; - private String rejectionReason; + private String statusInformation; @ElementCollection @CollectionTable(name = "visa_documents", joinColumns = @JoinColumn(name = "visa_id")) @Column(name = "s3_key") private List s3Keys = new ArrayList<>(); - //ToDo: Comments in the form of a discussion between applicant and administrator. - @Override public boolean equals(Object o) { @@ -86,7 +84,7 @@ public String toString() { ", handlerId=" + (handler != null ? handler.getId() : "null") + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + - ", rejectionReason='" + rejectionReason + '\'' + + ", rejectionReason='" + statusInformation + '\'' + ", s3Keys=" + s3Keys + '}'; } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java index 162c73b..31ba49e 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java @@ -21,9 +21,10 @@ public VisaDTO toDTO(Visa visa) { visa.getApplicant() != null ? visa.getApplicant().getId() : null, visa.getApplicant() != null ? visa.getApplicant().getFullName() : "Unknown", visa.getHandler() != null ? visa.getHandler().getId() : null, + visa.getHandler() != null ? visa.getHandler().getFullName() : "Unassigned", visa.getCreatedAt(), visa.getUpdatedAt(), - visa.getRejectionReason() + visa.getStatusInformation() ); } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index 74bc87a..9f70488 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -62,7 +62,7 @@ public VisaDTO findVisaDtoById(Long id) { } public List findVisasByApplicant(Long applicantId) { - return visaRepository.findByApplicantId(applicantId, Sort.by("createdAt").descending()) + return visaRepository.findByApplicantId(applicantId, Sort.by("id").ascending()) .stream() .map(visaMapper::toDTO) .toList(); @@ -127,6 +127,7 @@ public List findVisaByDateUpdated(LocalDateTime since){ // ----- + // Todo: Denna metod används aldrig just nu --> Få in via en drop-down meny? @Transactional public VisaDTO updateVisaStatus(Long visaId, VisaStatus newStatus, Long adminId) { if (newStatus == null) { @@ -142,10 +143,10 @@ public VisaDTO updateVisaStatus(Long visaId, VisaStatus newStatus, Long adminId) // Update visa status visa.setVisaStatus(newStatus); - visa.setRejectionReason(null); + visa.setStatusInformation(null); Visa savedVisa = visaRepository.save(visa); - // Create log + // Create log in database auditService.createAuditLog( adminId, visaId, @@ -170,7 +171,7 @@ public VisaDTO applyForVisa(CreateVisaDTO dto, Long userId) { visa.setVisaStatus(VisaStatus.SUBMITTED); Visa savedVisa = visaRepository.save(visa); - // Create log + // Create log in database auditService.createAuditLog( userId, savedVisa.getId(), @@ -182,14 +183,19 @@ public VisaDTO applyForVisa(CreateVisaDTO dto, Long userId) { @Transactional public VisaDTO approveVisa(Long visaId, Long adminId) { - validateHandler(adminId); + User admin = validateHandler(adminId); Visa visa = findVisaById(visaId); + if (visa.getHandler() == null) { + visa.setHandler(admin); + } + + visa.setVisaStatus(VisaStatus.GRANTED); - visa.setRejectionReason(null); + visa.setStatusInformation(null); Visa savedVisa = visaRepository.save(visa); - // Create log + // Create log in database auditService.createAuditLog( adminId, visaId, @@ -202,18 +208,22 @@ public VisaDTO approveVisa(Long visaId, Long adminId) { @Transactional public VisaDTO rejectVisa(Long visaId, Long adminId, String reason) { - if(reason == null || reason.isBlank()) { + if (reason == null || reason.isBlank()) { throw new IllegalArgumentException("Reason for rejection cannot be null or blank"); } - validateHandler(adminId); + User admin = validateHandler(adminId); Visa visa = findVisaById(visaId); + if (visa.getHandler() == null) { + visa.setHandler(admin); + } + visa.setVisaStatus(VisaStatus.REJECTED); - visa.setRejectionReason(reason); + visa.setStatusInformation(reason); Visa savedVisa = visaRepository.save(visa); - // Create log + // Create log in database auditService.createAuditLog( adminId, visaId, @@ -223,6 +233,31 @@ public VisaDTO rejectVisa(Long visaId, Long adminId, String reason) { return visaMapper.toDTO(savedVisa); } + @Transactional + public VisaDTO requestInformation (Long visaId, Long adminId, String infoText) { + User admin = validateHandler(adminId); + Visa visa = findVisaById(visaId); + + if (visa.getHandler() == null) { + visa.setHandler(admin); + } + + visa.setVisaStatus(VisaStatus.INCOMPLETE); + visa.setStatusInformation(infoText); + Visa savedVisa = visaRepository.save(visa); + + // Create log in database + auditService.createAuditLog( + adminId, + visaId, + AuditEventType.UPDATED, + "Information requested: " + infoText + ); + + return visaMapper.toDTO(savedVisa); + } + + // Assign Handler @Transactional public VisaDTO assignHandler(Long visaId, Long adminId) { @@ -230,14 +265,16 @@ public VisaDTO assignHandler(Long visaId, Long adminId) { Visa visa = findVisaById(visaId); visa.setHandler(admin); // Connects handler + visa.setVisaStatus(VisaStatus.ASSIGNED); + Visa savedVisa = visaRepository.save(visa); - // Create log + // Create log in database auditService.createAuditLog( adminId, visaId, AuditEventType.ASSIGNED, - "Admin has been assigned to case." + "Admin" + admin.getFullName() + "has been assigned to case and status is now ASSIGNED." ); return visaMapper.toDTO(savedVisa); } diff --git a/src/main/resources/templates/visa/dashboard.html b/src/main/resources/templates/visa/dashboard.html index c081b85..24b2395 100644 --- a/src/main/resources/templates/visa/dashboard.html +++ b/src/main/resources/templates/visa/dashboard.html @@ -25,12 +25,16 @@ table { width: 100%; border-spacing: 0; } th { text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: #999; padding: 10px 0; border-bottom: 1px solid #eee; } - td { padding: 20px 0; border-bottom: 1px solid #f9f9f9; font-size: 15px; } + td { padding: 10px 0; border-bottom: 1px solid #f9f9f9; font-size: 15px; } /* Minimalist Status Indicators */ .status { font-size: 12px; padding: 4px 8px; border-radius: 12px; background: #f0f0f0; } - .status-granted { background: #e6f4ea; color: #1e7e34; } - .status-rejected { background: #fce8e6; color: #d93025; } + .status-submitted { background: #e8f0fe; color: #1967d2; } /* Ljusblå */ + .status-unassigned { background: #f1f3f4; color: #5f6368; } /* Grå */ + .status-assigned { background: #e7f3ff; color: #0061c1; } /* Blå */ + .status-incomplete { background: #fef7e0; color: #b06000; } /* Orange/Gul */ + .status-granted { background: #e6f4ea; color: #1e7e34; } /* Grön */ + .status-rejected { background: #fce8e6; color: #d93025; } /* Röd */ .view-link { color: #007bff; text-decoration: none; font-weight: 500; } .view-link:hover { text-decoration: underline; } @@ -41,12 +45,16 @@
-

Visa Overview

- +

Visa Dashboard

+
- + + New Application + + Sign Out +
@@ -54,25 +62,35 @@

Visa Overview

Reference + Applicant + Handler Visa Type Status Submitted + Updated #101 + + Anna Andersson + + - + STUDENT SUBMITTED - Current date + - + - Details → diff --git a/src/main/resources/templates/visa/details.html b/src/main/resources/templates/visa/details.html index 9a815a7..52045db 100644 --- a/src/main/resources/templates/visa/details.html +++ b/src/main/resources/templates/visa/details.html @@ -39,11 +39,13 @@

Case #101

- +
-
+
Applicant John Doe
@@ -56,22 +58,34 @@

Case #101

STUDENT
- Submitted - 2023-10-27 + Nationality + - +
+
+ Important Information / Notes + -
-
- -
- -
- -
- -
- - -
+ +
+

Case Management

+ +
+
+ +
+ +
+ +
+
+ +
+ + +
+
From 0568740dd659523fedb7ae186f671c721baa50dc Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sat, 11 Apr 2026 20:15:36 +0200 Subject: [PATCH 04/21] fix: finalize parameter binding for info requests and add controller endpoint - Added missing @PostMapping for request-info in VisaViewController - Fixed HTML form issue where the reason parameter was not correctly sent to the server --- .../visa/controller/VisaViewController.java | 10 +++++++ .../visa/service/VisaService.java | 4 +-- .../resources/templates/visa/details.html | 28 ++++++++++++++++--- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index f0a5675..68a387b 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -104,6 +104,16 @@ public String approveVisa(@PathVariable Long id, @RequestParam Long currentUserI return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; } + @PostMapping("/{id}/request-info") + public String requestMoreInformation(@PathVariable Long id, + @RequestParam Long currentUserId, + @RequestParam String reason) { + + visaService.requestInformation(id, currentUserId, reason); + + return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; + } + // POST/visas/{id}/reject -> Beslut (Admin) Avvisar visumet med en motivering @PostMapping("/{id}/reject") public String rejectVisa(@PathVariable Long id, diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index 9f70488..c48d8d0 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -127,7 +127,7 @@ public List findVisaByDateUpdated(LocalDateTime since){ // ----- - // Todo: Denna metod används aldrig just nu --> Få in via en drop-down meny? + // Todo: Ta bort denna metod? --> då visa status sätts genom andra metoder/knappar @Transactional public VisaDTO updateVisaStatus(Long visaId, VisaStatus newStatus, Long adminId) { if (newStatus == null) { @@ -274,7 +274,7 @@ public VisaDTO assignHandler(Long visaId, Long adminId) { adminId, visaId, AuditEventType.ASSIGNED, - "Admin" + admin.getFullName() + "has been assigned to case and status is now ASSIGNED." + "Admin" + admin.getFullName() + " has been assigned to case and status is now ASSIGNED." ); return visaMapper.toDTO(savedVisa); } diff --git a/src/main/resources/templates/visa/details.html b/src/main/resources/templates/visa/details.html index 52045db..5845a72 100644 --- a/src/main/resources/templates/visa/details.html +++ b/src/main/resources/templates/visa/details.html @@ -81,12 +81,32 @@

Case Management

-
- - -
+
+ + +
+
+ + +
+ +
+ + +
+
+
+

Comments

From 723782928d5ec967cce80dfc37d8fb825da7bd26 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sun, 12 Apr 2026 10:25:24 +0200 Subject: [PATCH 05/21] feat: implement role-based sorting for applications - Updated logic to sort applications by last updated for admins - Implemented chronological sorting for regular users --- .../visacasemanagementsystem/visa/service/VisaService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index c48d8d0..b7d810f 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -43,7 +43,7 @@ public VisaService(VisaRepository visaRepository, UserRepository userRepository // --- For filtering in Frontend list-view --- public List findAll() { - return visaRepository.findAll() + return visaRepository.findAll(Sort.by(Sort.Direction.DESC, "updatedAt")) .stream() .map(visaMapper::toDTO) .toList(); @@ -62,7 +62,7 @@ public VisaDTO findVisaDtoById(Long id) { } public List findVisasByApplicant(Long applicantId) { - return visaRepository.findByApplicantId(applicantId, Sort.by("id").ascending()) + return visaRepository.findByApplicantId(applicantId, Sort.by(Sort.Direction.DESC, "updatedAt")) .stream() .map(visaMapper::toDTO) .toList(); From 9717435ac2f1befc62e5f34686d6a35d6c06e54f Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sun, 12 Apr 2026 13:09:52 +0200 Subject: [PATCH 06/21] feat: implement dark theme and extend visa application with travel details - UI: Updated Dashboard, Details and Apply-form to dark/minimal themed design. - Added passportNumber and travelDate to Visa entity and DTOs. - Added travel date validation in VisaService --- .../visa/dto/CreateVisaDTO.java | 7 +- .../visa/dto/UpdateVisaDTO.java | 7 +- .../visa/dto/VisaDTO.java | 3 + .../visa/entity/Visa.java | 7 + .../visa/mapper/VisaMapper.java | 6 + .../visa/service/VisaService.java | 5 + .../resources/templates/visa/apply-form.html | 103 ++++++++++-- .../resources/templates/visa/dashboard.html | 154 +++++++++++++----- .../resources/templates/visa/details.html | 134 ++++++++++++--- 9 files changed, 355 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/dto/CreateVisaDTO.java b/src/main/java/org/example/visacasemanagementsystem/visa/dto/CreateVisaDTO.java index da56ee4..e43f17c 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/dto/CreateVisaDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/dto/CreateVisaDTO.java @@ -3,10 +3,15 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import org.example.visacasemanagementsystem.visa.VisaType; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; public record CreateVisaDTO( @NotNull(message = "Visa type must be specified") VisaType visaType, @NotBlank(message = "Nationality must be specified") String nationality, - @NotNull Long applicantId // Användaren som ansöker om visa + @NotBlank(message = "Passport number must be provided") String passportNumber, + @NotNull(message = "Travel date must be provided") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate travelDate, + @NotNull Long applicantId ) { } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/dto/UpdateVisaDTO.java b/src/main/java/org/example/visacasemanagementsystem/visa/dto/UpdateVisaDTO.java index 5500499..62a3724 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/dto/UpdateVisaDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/dto/UpdateVisaDTO.java @@ -4,12 +4,17 @@ import jakarta.validation.constraints.NotNull; import org.example.visacasemanagementsystem.visa.VisaStatus; import org.example.visacasemanagementsystem.visa.VisaType; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; public record UpdateVisaDTO( @NotNull Long id, @NotBlank(message = "Visa type must be specified") VisaType visaType, @NotNull(message = "Application must have a status") VisaStatus visaStatus, @NotBlank(message = "Nationality must be specified") String nationality, - Long handlerId // Handläggaren som uppdaterar visat eller om användaren gör en komplettering? + @NotBlank(message = "Passport number must be provided") String passportNumber, + @NotNull (message = "Travel date must be provided") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate travelDate, + Long handlerId ) { } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java b/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java index 4becf08..c2369bf 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/dto/VisaDTO.java @@ -3,6 +3,7 @@ import org.example.visacasemanagementsystem.visa.VisaStatus; import org.example.visacasemanagementsystem.visa.VisaType; +import java.time.LocalDate; import java.time.LocalDateTime; public record VisaDTO( @@ -10,6 +11,8 @@ public record VisaDTO( VisaType visaType, VisaStatus visaStatus, String nationality, + String passportNumber, + LocalDate travelDate, Long applicantId, String applicantName, Long handlerId, diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java index 2170678..a0d09f5 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java @@ -12,7 +12,9 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -38,6 +40,11 @@ public class Visa { @NotBlank private String nationality; + @NotBlank private String passportNumber; + + @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate travelDate; + @ManyToOne @JoinColumn(name = "applicant_id", nullable = false) private User applicant; // Den som söker visumet diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java index 31ba49e..a614f6a 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java @@ -18,6 +18,8 @@ public VisaDTO toDTO(Visa visa) { visa.getVisaType(), visa.getVisaStatus(), visa.getNationality(), + visa.getPassportNumber(), + visa.getTravelDate(), visa.getApplicant() != null ? visa.getApplicant().getId() : null, visa.getApplicant() != null ? visa.getApplicant().getFullName() : "Unknown", visa.getHandler() != null ? visa.getHandler().getId() : null, @@ -36,6 +38,8 @@ public Visa toEntity(CreateVisaDTO dto) { Visa visa = new Visa(); visa.setVisaType(dto.visaType()); visa.setNationality(dto.nationality()); + visa.setPassportNumber(dto.passportNumber()); + visa.setTravelDate(dto.travelDate()); return visa; } @@ -46,6 +50,8 @@ public void updateEntityFromDTO(UpdateVisaDTO dto, Visa visa) { visa.setVisaType(dto.visaType()); visa.setVisaStatus(dto.visaStatus()); visa.setNationality(dto.nationality()); + visa.setPassportNumber(dto.passportNumber()); + visa.setTravelDate(dto.travelDate()); } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index b7d810f..68200fd 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -160,6 +160,11 @@ public VisaDTO updateVisaStatus(Long visaId, VisaStatus newStatus, Long adminId) @Transactional public VisaDTO applyForVisa(CreateVisaDTO dto, Long userId) { + // Validate travel date + if (dto.travelDate().isBefore(LocalDate.now())) { + throw new IllegalArgumentException("Travel date cannot be in the past"); + } + // Maps data Visa visa = visaMapper.toEntity(dto); diff --git a/src/main/resources/templates/visa/apply-form.html b/src/main/resources/templates/visa/apply-form.html index dece78f..5d55cf2 100644 --- a/src/main/resources/templates/visa/apply-form.html +++ b/src/main/resources/templates/visa/apply-form.html @@ -4,24 +4,97 @@ New Visa Application @@ -48,6 +121,16 @@

New Application

+
+ + +
+ +
+ + +
+
diff --git a/src/main/resources/templates/visa/dashboard.html b/src/main/resources/templates/visa/dashboard.html index 24b2395..66358d2 100644 --- a/src/main/resources/templates/visa/dashboard.html +++ b/src/main/resources/templates/visa/dashboard.html @@ -4,40 +4,113 @@ Visa Dashboard @@ -48,13 +121,14 @@

Visa Dashboard

-
- + New Application - - - Sign Out - +
@@ -66,14 +140,14 @@

Visa Dashboard

Handler Visa Type Status - Submitted - Updated + Travel Date + Last updated - #101 + #101 Anna Andersson @@ -88,10 +162,10 @@

Visa Dashboard

th:text="${visa.visaStatus}"> SUBMITTED + 2026-05-20 - - - - - + - + Details → diff --git a/src/main/resources/templates/visa/details.html b/src/main/resources/templates/visa/details.html index 5845a72..e598b2e 100644 --- a/src/main/resources/templates/visa/details.html +++ b/src/main/resources/templates/visa/details.html @@ -4,32 +4,104 @@ Visa Case Details @@ -39,7 +111,7 @@

Case #101

-
@@ -49,22 +121,46 @@

Case #101

Applicant John Doe +
Status SUBMITTED
+ +
+ Last Updated + 2023-10-28 09:30 +
+
Visa Type STUDENT
+ +
+ Passport Number + AB123456 +
+
+ Expected Travel Date + 2026-05-20 +
+ +
+ Submitted + 2023-10-27 +
+
Nationality -
+
Important Information / Notes -
+
From 47dfe1a3335e333f33b711b957a613e88e2dfb12 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sun, 12 Apr 2026 14:15:21 +0200 Subject: [PATCH 07/21] feat: add field validation to visa application form - Implement server-side validation using @Valid and BindingResult - Add error message handling in VisaViewController - Map Service-layer exceptions to specific form fields --- .../visa/controller/VisaViewController.java | 66 +++++++++++++------ .../visa/entity/Visa.java | 8 ++- .../resources/templates/visa/apply-form.html | 59 +++++++++++++---- 3 files changed, 100 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index 68a387b..08af0b7 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -1,6 +1,7 @@ package org.example.visacasemanagementsystem.visa.controller; import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; import org.example.visacasemanagementsystem.comment.service.CommentService; import org.example.visacasemanagementsystem.user.UserAuthorization; import org.example.visacasemanagementsystem.user.dto.UserDTO; @@ -11,7 +12,9 @@ import org.example.visacasemanagementsystem.visa.service.VisaService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; import java.util.List; @@ -29,9 +32,6 @@ public VisaViewController(VisaService visaService, CommentService commentService this.userService = userService; } - // ENDPOINTS - - // GET/visas/dashboard -> Huvudsidan. Visa listan på visum (alla för admin, egna för användaren) @GetMapping("/dashboard") public String showDashboard(@RequestParam Long currentUserId, Model model) { @@ -43,9 +43,9 @@ public String showDashboard(@RequestParam Long currentUserId, Model model) { // If the current user = ADMIN/SYSADMIN show everything if (user.userAuthorization() == UserAuthorization.ADMIN || - user.userAuthorization() == UserAuthorization.SYSADMIN) { + user.userAuthorization() == UserAuthorization.SYSADMIN) { visas = visaService.findAll(); - } else { + } else { // Normal user/applicant can only se their own visa applications visas = visaService.findVisasByApplicant(currentUserId); } @@ -58,9 +58,9 @@ public String showDashboard(@RequestParam Long currentUserId, Model model) { } - // GET/visas/{id} -> Detaljvyn. Visa all info om ett ärende + dess kommentarer -@GetMapping("/{id}") - public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserId , Model model) { + + @GetMapping("/{id}") + public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserId, Model model) { // Get visa VisaDTO visa = visaService.findVisaDtoById(id); @@ -68,7 +68,7 @@ public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserI var comments = commentService.getCommentsByVisaId(id); // Get user - UserDTO user = userService.findById(currentUserId) + UserDTO user = userService.findById(currentUserId) .orElseThrow(() -> new EntityNotFoundException("User not found.")); model.addAttribute("visa", visa); @@ -78,7 +78,6 @@ public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserI return "visa/details"; } - // GET/visas/apply -> Ansökningssidan. Visa formuläret för att söka ett nytt visum @GetMapping("/apply") public String showApplyForm(@RequestParam Long currentUserId, Model model) { UserDTO user = userService.findById(currentUserId) @@ -86,22 +85,42 @@ public String showApplyForm(@RequestParam Long currentUserId, Model model) { model.addAttribute("currentUser", user); model.addAttribute("visaTypes", VisaType.values()); + + if (!model.containsAttribute("CreateVisaDTO")) { + model.addAttribute("createVisaDTO", new CreateVisaDTO(null, "", "", null, currentUserId)); + } + return "visa/apply-form"; } - // POST/visas/apply -> Skicka ansökan. Tar emot förmulärdatan och sparar via VisaService @PostMapping("/apply") - public String submitApplication(@ModelAttribute CreateVisaDTO createVisaDTO, - @RequestParam Long currentUserId) { - visaService.applyForVisa(createVisaDTO, currentUserId); + public String submitApplication( + @Valid @ModelAttribute("createVisaDTO") CreateVisaDTO createVisaDTO, + BindingResult bindingResult, + @RequestParam Long currentUserId, + RedirectAttributes redirectAttributes, + Model model) { + + if (bindingResult.hasErrors()) { + prepareApplyModel(currentUserId, model); + return "visa/apply-form"; + } + + try { + visaService.applyForVisa(createVisaDTO, currentUserId); + } catch (IllegalArgumentException e) { + bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage()); + prepareApplyModel(currentUserId, model); + return "visa/apply-form"; + } + return "redirect:/visas/dashboard?currentUserId=" + currentUserId; } - // POST/visas/{id}/approve -> Beslut. (Admin) Godkänner visumet via en knapp i detaljvyn @PostMapping("/{id}/approve") public String approveVisa(@PathVariable Long id, @RequestParam Long currentUserId) { - visaService.approveVisa(id, currentUserId); - return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; + visaService.approveVisa(id, currentUserId); + return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; } @PostMapping("/{id}/request-info") @@ -114,7 +133,7 @@ public String requestMoreInformation(@PathVariable Long id, return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; } - // POST/visas/{id}/reject -> Beslut (Admin) Avvisar visumet med en motivering + @PostMapping("/{id}/reject") public String rejectVisa(@PathVariable Long id, @RequestParam Long currentUserId, @@ -123,10 +142,19 @@ public String rejectVisa(@PathVariable Long id, return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; } - // ASSIGN - Admin tar på sig ett ärende + @PostMapping("/{id}/assign") public String assignCaseToHandler(@PathVariable Long id, @RequestParam Long currentUserId) { visaService.assignHandler(id, currentUserId); return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; } + + // Helper methods + + public void prepareApplyModel(Long currentUserId, Model model) { + UserDTO user = userService.findById(currentUserId) + .orElseThrow(() -> new EntityNotFoundException("User not found.")); + model.addAttribute("currentUser", user); + model.addAttribute("visaTypes", VisaType.values()); + } } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java index a0d09f5..c53ea00 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java @@ -87,11 +87,13 @@ public String toString() { ", visaType=" + visaType + ", visaStatus=" + visaStatus + ", nationality='" + nationality + '\'' + - ", applicantId=" + (applicant != null ? applicant.getId() : "null") + - ", handlerId=" + (handler != null ? handler.getId() : "null") + + ", passportNumber='" + passportNumber + '\'' + + ", travelDate=" + travelDate + + ", applicant=" + applicant + + ", handler=" + handler + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + - ", rejectionReason='" + statusInformation + '\'' + + ", statusInformation='" + statusInformation + '\'' + ", s3Keys=" + s3Keys + '}'; } diff --git a/src/main/resources/templates/visa/apply-form.html b/src/main/resources/templates/visa/apply-form.html index 5d55cf2..c04fbb2 100644 --- a/src/main/resources/templates/visa/apply-form.html +++ b/src/main/resources/templates/visa/apply-form.html @@ -95,6 +95,18 @@ transition: 0.2s; } .cancel-link:hover { color: #fff; } + + .is-invalid { + border-color: #ff4d4d !important; + } + + .error-text { + color: #ff4d4d; + font-size: 0.75rem; + margin-top: 8px; + display: block; + + } @@ -109,31 +121,56 @@

New Application

- + + th:text="${type}" + th:selected="${type == createVisaDTO?.visaType}">STUDENT + Visa type must be specified
+
- + + Nationality must be specified
- + + Passport number must be provided
- -
- -
- - + + Travel date must be provided
From 64a70cbfbb9129150364ab49920bc02da97b2998 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Sun, 12 Apr 2026 17:55:50 +0200 Subject: [PATCH 08/21] test: Implement tests for VisaMapper --- .../resources/templates/visa/apply-form.html | 3 - .../resources/templates/visa/dashboard.html | 22 ++-- .../visa/mapper/VisaMapperTest.java | 102 ++++++++++++++++++ 3 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapperTest.java diff --git a/src/main/resources/templates/visa/apply-form.html b/src/main/resources/templates/visa/apply-form.html index c04fbb2..fe2aadc 100644 --- a/src/main/resources/templates/visa/apply-form.html +++ b/src/main/resources/templates/visa/apply-form.html @@ -4,7 +4,6 @@ New Visa Application + + + +
+
+

Edit Application

+

Updating case for User

+
+ +
+ +

+ Reason for incomplete status goes here. +

+
+ +
+ + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + Back to Details +
+
+ + From 378519f65b49c8100a9a73d30508f484822905b2 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Mon, 13 Apr 2026 13:59:22 +0200 Subject: [PATCH 11/21] fix: relocate visaMapper --- .../visacasemanagementsystem/visa/{ => mapper}/VisaMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/org/example/visacasemanagementsystem/visa/{ => mapper}/VisaMapper.java (97%) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/VisaMapper.java b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java similarity index 97% rename from src/main/java/org/example/visacasemanagementsystem/visa/VisaMapper.java rename to src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java index 11591f8..a614f6a 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/VisaMapper.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapper.java @@ -1,4 +1,4 @@ -package org.example.visacasemanagementsystem.visa; +package org.example.visacasemanagementsystem.visa.mapper; import org.example.visacasemanagementsystem.visa.dto.CreateVisaDTO; import org.example.visacasemanagementsystem.visa.dto.UpdateVisaDTO; From 1322f3c67bea98528806b327eec0d3b51688c450 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Mon, 13 Apr 2026 14:26:32 +0200 Subject: [PATCH 12/21] fix: resolve compilation errors in pipeline due to package mismatch --- .../visacasemanagementsystem/visa/service/VisaService.java | 2 +- .../visa/{mapper => }/VisaMapperTest.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename src/test/java/org/example/visacasemanagementsystem/visa/{mapper => }/VisaMapperTest.java (94%) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index 5ee9632..d014c0b 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -12,7 +12,7 @@ import org.example.visacasemanagementsystem.visa.dto.UpdateVisaDTO; import org.example.visacasemanagementsystem.visa.dto.VisaDTO; import org.example.visacasemanagementsystem.visa.entity.Visa; -import org.example.visacasemanagementsystem.visa.VisaMapper; +import org.example.visacasemanagementsystem.visa.mapper.VisaMapper; import org.example.visacasemanagementsystem.visa.repository.VisaRepository; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapperTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java similarity index 94% rename from src/test/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapperTest.java rename to src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java index 042e72b..03866e7 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/mapper/VisaMapperTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaMapperTest.java @@ -1,12 +1,11 @@ -package org.example.visacasemanagementsystem.visa.mapper; +package org.example.visacasemanagementsystem.visa; import org.example.visacasemanagementsystem.user.entity.User; -import org.example.visacasemanagementsystem.visa.VisaStatus; -import org.example.visacasemanagementsystem.visa.VisaType; import org.example.visacasemanagementsystem.visa.dto.CreateVisaDTO; import org.example.visacasemanagementsystem.visa.dto.UpdateVisaDTO; import org.example.visacasemanagementsystem.visa.dto.VisaDTO; import org.example.visacasemanagementsystem.visa.entity.Visa; +import org.example.visacasemanagementsystem.visa.mapper.VisaMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From 2fd457a53a6d79cef69a742590e0e7240256060b Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Mon, 13 Apr 2026 14:33:58 +0200 Subject: [PATCH 13/21] test: Initial start for VisaServiceTest --- .../visa/VisaServiceTest.java | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java new file mode 100644 index 0000000..e8043c5 --- /dev/null +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java @@ -0,0 +1,210 @@ +package org.example.visacasemanagementsystem.visa; + +import jakarta.persistence.EntityNotFoundException; +import org.example.visacasemanagementsystem.audit.AuditEventType; +import org.example.visacasemanagementsystem.audit.service.AuditService; +import org.example.visacasemanagementsystem.exception.UnauthorizedException; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.repository.UserRepository; +import org.example.visacasemanagementsystem.visa.dto.CreateVisaDTO; +import org.example.visacasemanagementsystem.visa.entity.Visa; +import org.example.visacasemanagementsystem.visa.mapper.VisaMapper; +import org.example.visacasemanagementsystem.visa.repository.VisaRepository; +import org.example.visacasemanagementsystem.visa.service.VisaService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class VisaServiceTest { + + @Mock + private VisaRepository visaRepository; + @Mock + private UserRepository userRepository; + @Mock + private VisaMapper visaMapper; + @Mock + private AuditService auditService; + + @InjectMocks + private VisaService visaService; + + // Alla find metoder? + + // applyForVisa 3x + @Test + void applyForVisa_shouldSaveVisa_WhenDataIsValid() { + // Arrange + Long userId = 1L; + CreateVisaDTO dto = new CreateVisaDTO( + VisaType.STUDY, "Swedish","AB123", + LocalDate.now(), + userId + ); + + User user = new User(); + user.setId(userId); + + Visa visa = new Visa(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(visaMapper.toEntity(dto)).thenReturn(visa); + when(visaRepository.save(any(Visa.class))).thenAnswer(i -> i.getArgument(0)); + + // Act + visaService.applyForVisa(dto,userId); + + // Assert + verify(visaRepository, times(1)).save(any(Visa.class)); + verify(auditService, times(1)).createAuditLog(eq(userId), any(), any(),any()); + assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.SUBMITTED); + assertThat(visa.getApplicant()).isEqualTo(user); + } + + @Test + void applyForVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInThePast() { + // Arrange + Long userId = 1L; + CreateVisaDTO dto = new CreateVisaDTO( + VisaType.STUDY, "Swedish","AB123", + LocalDate.now().minusDays(1), + userId + ); + + // Act & Assert + assertThatThrownBy(()-> visaService.applyForVisa(dto, userId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Travel date cannot be in the past."); + + // Verify + verifyNoInteractions(visaRepository); + verifyNoInteractions(auditService); + } + + @Test + void applyForVisa_shouldThrowException_WhenUserIsNotFoundInDb() { + Long userId = 99L; + CreateVisaDTO dto = new CreateVisaDTO( + VisaType.STUDY, "Swedish","AB123", + LocalDate.now(), + userId + ); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThatThrownBy(()-> visaService.applyForVisa(dto, userId)) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found"); + } + + // approveVisa_shouldApproveVisa_andCreateLog + + + + // rejectVisa 2x + @Test + void rejectVisa_shouldUpdateStatus_AndCreateLog() { + // Arrange + Long visaId = 1L; + Long adminId = 2L; + String reason = "Missing documents"; + + User admin = new User(); + admin.setUserAuthorization(UserAuthorization.ADMIN); + + Visa visa = new Visa(); + visa.setVisaStatus(VisaStatus.SUBMITTED); + + when(userRepository.findById(adminId)).thenReturn(Optional.of(admin)); + when(visaRepository.findById(visaId)).thenReturn(Optional.of(visa)); + when(visaRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + // Act + visaService.rejectVisa(visaId, adminId, reason); + + // Assert + assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.REJECTED); + assertThat(visa.getStatusInformation()).isEqualTo(reason); + verify(auditService).createAuditLog(eq(adminId), eq(visaId), eq(AuditEventType.REJECTED), contains(reason)); + + } + + @Test + void rejectVisa_shouldThrowIllegalArgumentException_WhenRejectReasonIsMissing() { + // Arrange + Long visaId = 1L; + Long adminId = 2L; + String missingReason = " "; + + // Act & Assert + assertThatThrownBy(() -> visaService.rejectVisa(visaId, adminId, missingReason)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Reason for rejection cannot be null or blank"); + + verifyNoInteractions(visaRepository); + verifyNoInteractions(auditService); + } + + // requestMoreInformation_shouldUpdateStatusAndCreateLog + // assignHandler_shouldAssignHandlerToVisaCase_AndCreateLog + + // ValidateHandler 3x + @Test + void validateHandler_shouldReturnUser_whenUserIsAdmin() { + // Arrange + Long adminId = 1L; + User admin = new User(); + admin.setUserAuthorization(UserAuthorization.ADMIN); + when(userRepository.findById(adminId)).thenReturn(Optional.of(admin)); + + // Act + User result = visaService.validateHandler(adminId); + + // Assert + assertThat(result).isEqualTo(admin); + } + + @Test + void validateHandler_shouldThrowUnauthorizedException_WhenUserIsApplicant() { + // Arrange + Long userId = 1L; + User applicant = new User(); + applicant.setUserAuthorization(UserAuthorization.USER); + when(userRepository.findById(userId)).thenReturn(Optional.of(applicant)); + + assertThatThrownBy(()-> visaService.validateHandler(userId)) + .isInstanceOf(UnauthorizedException.class) + .hasMessageContaining("User is not authorized to perform this action."); + } + + @Test + void validateHandler_shouldThrowEntityNotFoundException_WhenUserIsApplicant() { + // Arrange + Long nonExistingUserId = 999L; + + when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThatThrownBy(()-> visaService.validateHandler(nonExistingUserId)) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found."); + + verifyNoInteractions(visaRepository); + } + + + + + +} From 77e4e58695cc292b3202f8bf8176951eb95abef8 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Mon, 13 Apr 2026 19:13:30 +0200 Subject: [PATCH 14/21] test: extend test suite for VisaServiceTest --- .../visa/VisaServiceTest.java | 214 ++++++++++++++++-- 1 file changed, 194 insertions(+), 20 deletions(-) diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java index e8043c5..e576767 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java @@ -8,6 +8,7 @@ import org.example.visacasemanagementsystem.user.entity.User; import org.example.visacasemanagementsystem.user.repository.UserRepository; import org.example.visacasemanagementsystem.visa.dto.CreateVisaDTO; +import org.example.visacasemanagementsystem.visa.dto.UpdateVisaDTO; import org.example.visacasemanagementsystem.visa.entity.Visa; import org.example.visacasemanagementsystem.visa.mapper.VisaMapper; import org.example.visacasemanagementsystem.visa.repository.VisaRepository; @@ -39,7 +40,7 @@ class VisaServiceTest { @InjectMocks private VisaService visaService; - // Alla find metoder? + // Find metoder? // applyForVisa 3x @Test @@ -47,7 +48,7 @@ void applyForVisa_shouldSaveVisa_WhenDataIsValid() { // Arrange Long userId = 1L; CreateVisaDTO dto = new CreateVisaDTO( - VisaType.STUDY, "Swedish","AB123", + VisaType.STUDY, "Swedish", "AB123", LocalDate.now(), userId ); @@ -62,11 +63,11 @@ void applyForVisa_shouldSaveVisa_WhenDataIsValid() { when(visaRepository.save(any(Visa.class))).thenAnswer(i -> i.getArgument(0)); // Act - visaService.applyForVisa(dto,userId); + visaService.applyForVisa(dto, userId); // Assert verify(visaRepository, times(1)).save(any(Visa.class)); - verify(auditService, times(1)).createAuditLog(eq(userId), any(), any(),any()); + verify(auditService, times(1)).createAuditLog(eq(userId), any(), any(), any()); assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.SUBMITTED); assertThat(visa.getApplicant()).isEqualTo(user); } @@ -76,14 +77,14 @@ void applyForVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInThePast( // Arrange Long userId = 1L; CreateVisaDTO dto = new CreateVisaDTO( - VisaType.STUDY, "Swedish","AB123", + VisaType.STUDY, "Swedish", "AB123", LocalDate.now().minusDays(1), userId ); // Act & Assert - assertThatThrownBy(()-> visaService.applyForVisa(dto, userId)) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> visaService.applyForVisa(dto, userId)) + .isInstanceOf(IllegalArgumentException.class) .hasMessage("Travel date cannot be in the past."); // Verify @@ -95,7 +96,7 @@ void applyForVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInThePast( void applyForVisa_shouldThrowException_WhenUserIsNotFoundInDb() { Long userId = 99L; CreateVisaDTO dto = new CreateVisaDTO( - VisaType.STUDY, "Swedish","AB123", + VisaType.STUDY, "Swedish", "AB123", LocalDate.now(), userId ); @@ -103,14 +104,129 @@ void applyForVisa_shouldThrowException_WhenUserIsNotFoundInDb() { when(userRepository.findById(userId)).thenReturn(Optional.empty()); // Act & Assert - assertThatThrownBy(()-> visaService.applyForVisa(dto, userId)) - .isInstanceOf(EntityNotFoundException.class) + assertThatThrownBy(() -> visaService.applyForVisa(dto, userId)) + .isInstanceOf(EntityNotFoundException.class) .hasMessage("User not found"); } - // approveVisa_shouldApproveVisa_andCreateLog + // updateVisa 3x + @Test + void updateVisa_shouldUpdateVisaAndResetStatus_WhenUserIsAuthorized() { + // Arrange + Long userId = 1L; + Long visaId = 100L; + + UpdateVisaDTO dto = new UpdateVisaDTO( + visaId, VisaType.TOURIST, VisaStatus.SUBMITTED, + "Swedish", "XYZ789", LocalDate.now().plusMonths(1), null + ); + + User applicant = new User(); + applicant.setId(userId); + + Visa visa = new Visa(); + visa.setId(visaId); + visa.setApplicant(applicant); + visa.setVisaStatus(VisaStatus.INCOMPLETE); + visa.setStatusInformation("PLease clarify travel purpose"); + + when(visaRepository.findById(visaId)).thenReturn(Optional.of(visa)); + when(visaRepository.save(any(Visa.class))).thenAnswer(i -> i.getArgument(0)); + + // Act + visaService.updateVisa(dto, userId); + + // Assert + assertThat(visa.getVisaType()).isEqualTo(VisaType.TOURIST); + assertThat(visa.getPassportNumber()).isEqualTo("XYZ789"); + assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.SUBMITTED); + assertThat(visa.getStatusInformation()).isNull(); + + verify(auditService).createAuditLog(eq(userId), eq(visaId), eq(AuditEventType.UPDATED), anyString()); + + } + + @Test + void updateVisa_shouldThrowUnauthorizedException_WhenUserIsNotTheApplicant() { + // Arrange + Long actualApplicantId = 3L; + Long unauthorizedUserId = 99L; + UpdateVisaDTO dto = new UpdateVisaDTO(100L, VisaType.EMPLOYMENT, + VisaStatus.SUBMITTED, "US", "123", + LocalDate.now().plusDays(1), null); + User actualApplicant = new User(); + actualApplicant.setId(actualApplicantId); + + Visa visa = new Visa(); + visa.setApplicant(actualApplicant); + + when(visaRepository.findById(anyLong())).thenReturn(Optional.of(visa)); + + // Act & Assert + assertThatThrownBy(() -> visaService.updateVisa(dto, unauthorizedUserId)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("You are not authorized to update this application."); + + } + + @Test + void updateVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInPast() { + // Arrange + Long userId = 1L; + LocalDate pastDate = LocalDate.now().minusDays(1); + + UpdateVisaDTO dto = new UpdateVisaDTO(100L, VisaType.STUDY, + VisaStatus.SUBMITTED, "Swedish", "CDE789", pastDate, null); + + User actualUser = new User(); + actualUser.setId(userId); + + Visa visa = new Visa(); + visa.setApplicant(actualUser); + + when(visaRepository.findById(anyLong())).thenReturn(Optional.of(visa)); + + // Act & Assert + assertThatThrownBy(() -> visaService.updateVisa(dto, userId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Travel date cannot be in the past."); + + } + + @Test + void approveVisa_shouldApproveVisa_andCreateLog() { + // Arrange + Long visaId = 1L; + Long adminId = 2L; + + User admin = new User(); + admin.setId(adminId); + admin.setUserAuthorization(UserAuthorization.ADMIN); + + Visa visa = new Visa(); + visa.setId(visaId); + visa.setVisaStatus(VisaStatus.SUBMITTED); + visa.setApplicant(null); + + when(userRepository.findById(adminId)).thenReturn(Optional.of(admin)); + when(visaRepository.findById(visaId)).thenReturn(Optional.of(visa)); + when(visaRepository.save(any(Visa.class))).thenAnswer(i -> i.getArgument(0)); + + // Act + visaService.approveVisa(visaId, adminId); + + // Assert + assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.GRANTED); + assertThat(visa.getStatusInformation()).isNull(); + assertThat(visa.getHandler()).isEqualTo(admin); + + verify(visaRepository, times(1)).save(visa); + verify(auditService).createAuditLog(eq(adminId), eq(visaId), eq(AuditEventType.GRANTED), + contains("granted")); + + } // rejectVisa 2x @Test @@ -156,8 +272,69 @@ void rejectVisa_shouldThrowIllegalArgumentException_WhenRejectReasonIsMissing() verifyNoInteractions(auditService); } - // requestMoreInformation_shouldUpdateStatusAndCreateLog - // assignHandler_shouldAssignHandlerToVisaCase_AndCreateLog + @Test + void requestMoreInformation_shouldUpdateStatusAndInfoText_AndCreateLog() { + // Arrange + Long visaId = 1L; + Long adminId = 2L; + String infoText = "Please upload a clearer picture of your passport"; + + User admin = new User(); + admin.setUserAuthorization(UserAuthorization.ADMIN); + + Visa visa = new Visa(); + visa.setVisaStatus(VisaStatus.SUBMITTED); + visa.setHandler(null); + + when(userRepository.findById(adminId)).thenReturn(Optional.of(admin)); + when(visaRepository.findById(visaId)).thenReturn(Optional.of(visa)); + when(visaRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + // Act + visaService.requestMoreInformation(visaId, adminId, infoText); + + // Assert + assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.INCOMPLETE); + assertThat(visa.getStatusInformation()).isEqualTo(infoText); + assertThat(visa.getHandler()).isEqualTo(admin); + + verify(auditService).createAuditLog( + eq(adminId), + eq(visaId), + eq(AuditEventType.UPDATED), + contains(infoText)); + } + + @Test + void assignHandler_shouldAssignAdminToVisa_AndChangeStatusToAssigned() { + // Arrange + Long visaId = 1L; + Long adminId = 2L; + + User admin = new User(); + admin.setFullName("Test Admin"); + admin.setUserAuthorization(UserAuthorization.ADMIN); + + Visa visa = new Visa(); + visa.setVisaStatus(VisaStatus.SUBMITTED); + + when(userRepository.findById(adminId)).thenReturn(Optional.of(admin)); + when(visaRepository.findById(visaId)).thenReturn(Optional.of(visa)); + when(visaRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + // Act + visaService.assignHandler(visaId, adminId); + + // Assert + assertThat(visa.getHandler()).isEqualTo(admin); + assertThat(visa.getVisaStatus()).isEqualTo(VisaStatus.ASSIGNED); + + verify(auditService).createAuditLog( + eq(adminId), + eq(visaId), + eq(AuditEventType.ASSIGNED), + contains("Test Admin")); + } // ValidateHandler 3x @Test @@ -183,8 +360,8 @@ void validateHandler_shouldThrowUnauthorizedException_WhenUserIsApplicant() { applicant.setUserAuthorization(UserAuthorization.USER); when(userRepository.findById(userId)).thenReturn(Optional.of(applicant)); - assertThatThrownBy(()-> visaService.validateHandler(userId)) - .isInstanceOf(UnauthorizedException.class) + assertThatThrownBy(() -> visaService.validateHandler(userId)) + .isInstanceOf(UnauthorizedException.class) .hasMessageContaining("User is not authorized to perform this action."); } @@ -196,15 +373,12 @@ void validateHandler_shouldThrowEntityNotFoundException_WhenUserIsApplicant() { when(userRepository.findById(nonExistingUserId)).thenReturn(Optional.empty()); // Act & Assert - assertThatThrownBy(()-> visaService.validateHandler(nonExistingUserId)) - .isInstanceOf(EntityNotFoundException.class) + assertThatThrownBy(() -> visaService.validateHandler(nonExistingUserId)) + .isInstanceOf(EntityNotFoundException.class) .hasMessage("User not found."); verifyNoInteractions(visaRepository); } - - - } From d16cec6f61a5c4112949ed20e6aa2a85364c2680 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 14:45:59 +0200 Subject: [PATCH 15/21] fix: suggested fixes from CodeRabbit --- .../exception/GlobalExceptionHandler.java | 2 +- .../visa/controller/VisaViewController.java | 16 ++++++++++++---- .../visa/entity/Visa.java | 6 +++--- .../visa/repository/VisaRepository.java | 2 +- .../visa/service/VisaService.java | 8 ++++++-- src/main/resources/templates/visa/dashboard.html | 1 + 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java b/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java index ac3351c..6bf68f9 100644 --- a/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/visacasemanagementsystem/exception/GlobalExceptionHandler.java @@ -38,6 +38,6 @@ public String handleIllegalArgumentException(IllegalArgumentException exception) @ResponseBody public String handleAllUncaughtErrors(Exception exception) { log.error("Unexpected Error: ", exception); - return exception.getMessage(); + return "Unexpected server error."; } } diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index b3544c8..2e555e1 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -105,7 +105,7 @@ public String showEditForm(@PathVariable Long id, @RequestParam Long currentUser VisaDTO visa = visaService.findVisaDtoById(id); if (!visa.applicantId().equals(currentUserId)) { - throw new UnauthorizedException("You can only edit ypur own applications."); + throw new UnauthorizedException("You can only edit your own applications."); } UpdateVisaDTO updateDto = new UpdateVisaDTO( @@ -138,6 +138,8 @@ public String processUpdate( if (bindingResult.hasErrors()) { prepareApplyModel(currentUserId, model); model.addAttribute("isEdit", true); + VisaDTO visa = visaService.findVisaDtoById(id); + model.addAttribute("statusInformation", visa.statusInformation()); return "visa/edit-form"; } @@ -189,13 +191,19 @@ public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserI // Get visa VisaDTO visa = visaService.findVisaDtoById(id); - // Get comments - var comments = commentService.getCommentsByVisaId(id); - // Get user UserDTO user = userService.findById(currentUserId) .orElseThrow(() -> new EntityNotFoundException("User not found.")); + // Authorization check: regular users can only view their own applications + if (user.userAuthorization() == UserAuthorization.USER && !visa.applicantId().equals(currentUserId)) { + throw new UnauthorizedException("You can only view your own applications."); + } + + // Get comments + var comments = commentService.getCommentsByVisaId(id); + + model.addAttribute("visa", visa); model.addAttribute("comments", comments); model.addAttribute("currentUser", user); diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java index c53ea00..8739433 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/entity/Visa.java @@ -87,10 +87,10 @@ public String toString() { ", visaType=" + visaType + ", visaStatus=" + visaStatus + ", nationality='" + nationality + '\'' + - ", passportNumber='" + passportNumber + '\'' + + ", passportNumber='***" + (passportNumber == null || passportNumber.length() < 2 ? "" : passportNumber.substring(passportNumber.length() - 2)) + '\'' + ", travelDate=" + travelDate + - ", applicant=" + applicant + - ", handler=" + handler + + ", applicantId=" + (applicant != null ? applicant.getId() : null) + + ", handlerId=" + (handler != null ? handler.getId() : null) + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + ", statusInformation='" + statusInformation + '\'' + diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java b/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java index 2ee1542..3f01066 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/repository/VisaRepository.java @@ -24,7 +24,7 @@ public interface VisaRepository extends JpaRepository { List findByHandler_IdAndVisaStatus(Long handlerId, VisaStatus visaStatus, Sort sort); - List findByApplicantId(Long applicantId, Sort sort); + List findByApplicant_Id(Long applicantId, Sort sort); diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index d014c0b..5dba717 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -63,7 +63,7 @@ public VisaDTO findVisaDtoById(Long id) { } public List findVisasByApplicant(Long applicantId) { - return visaRepository.findByApplicantId(applicantId, Sort.by(Sort.Direction.DESC, "updatedAt")) + return visaRepository.findByApplicant_Id(applicantId, Sort.by(Sort.Direction.DESC, "updatedAt")) .stream() .map(visaMapper::toDTO) .toList(); @@ -268,6 +268,10 @@ public VisaDTO rejectVisa(Long visaId, Long adminId, String reason) { @Transactional public VisaDTO requestMoreInformation(Long visaId, Long adminId, String infoText) { + if (infoText == null || infoText.isBlank()) { + throw new IllegalArgumentException("Information request text cannot be null or blank"); + } + User admin = validateHandler(adminId); Visa visa = findVisaById(visaId); @@ -305,7 +309,7 @@ public VisaDTO assignHandler(Long visaId, Long adminId) { adminId, visaId, AuditEventType.ASSIGNED, - "Admin" + admin.getFullName() + " has been assigned to case and status is now ASSIGNED." + "Admin " + admin.getFullName() + " has been assigned to case and status is now ASSIGNED." ); return visaMapper.toDTO(savedVisa); } diff --git a/src/main/resources/templates/visa/dashboard.html b/src/main/resources/templates/visa/dashboard.html index 54cd024..d713d66 100644 --- a/src/main/resources/templates/visa/dashboard.html +++ b/src/main/resources/templates/visa/dashboard.html @@ -158,6 +158,7 @@

Visa Dashboard

th:text="${visa.visaStatus}"> SUBMITTED + 2026-05-20 From ea89dabc307de57d514d5f3a9a7bf414ae9f4737 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 15:33:16 +0200 Subject: [PATCH 16/21] fix: more CodeRabbit feedback --- .../VisaCaseManagementSystemApplication.java | 40 +++++++++++++++++++ .../config/SecurityConfig.java | 21 ++++++++++ .../visa/controller/VisaViewController.java | 2 +- .../visa/service/VisaService.java | 9 ++++- .../visa/VisaServiceTest.java | 8 ++-- 5 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java diff --git a/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java b/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java index d16d4c6..e434d5b 100644 --- a/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java +++ b/src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java @@ -1,7 +1,12 @@ package org.example.visacasemanagementsystem; +import org.example.visacasemanagementsystem.user.UserAuthorization; +import org.example.visacasemanagementsystem.user.entity.User; +import org.example.visacasemanagementsystem.user.repository.UserRepository; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @@ -12,4 +17,39 @@ public static void main(String[] args) { SpringApplication.run(VisaCaseManagementSystemApplication.class, args); } + @Bean + public CommandLineRunner initData(UserRepository userRepository) { + return args -> { + // Skapa en vanlig användare (Applicant) + if (userRepository.findByEmail("user@test.com").isEmpty()) { + User user = new User(); + user.setFullName("USER"); + user.setEmail("user@test.com"); + user.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det + user.setUserAuthorization(UserAuthorization.USER); + userRepository.save(user); + System.out.println("Testanvändare skapad med ID: " + user.getId()); + + User admin = new User(); + admin.setFullName("ADMIN"); + admin.setEmail("user@test.com2"); + admin.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det + admin.setUserAuthorization(UserAuthorization.ADMIN); + userRepository.save(admin); + System.out.println("Test-admin skapad med ID: " + admin.getId()); + } + + // Skapa en admin (Handler) + if (userRepository.findByEmail("admin@test.com").isEmpty()) { + User sysadmin = new User(); + sysadmin.setFullName("SYSTEM ADMIN"); + sysadmin.setEmail("admin@test.com"); + sysadmin.setPassword("password"); + sysadmin.setUserAuthorization(UserAuthorization.SYSADMIN); + userRepository.save(sysadmin); + System.out.println("Test-sysadmin skapad med ID: " + sysadmin.getId()); + } + }; + } + } diff --git a/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java b/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java new file mode 100644 index 0000000..0e18789 --- /dev/null +++ b/src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java @@ -0,0 +1,21 @@ +package org.example.visacasemanagementsystem.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() + ) + .csrf(csrf -> csrf.disable()) + .headers(headers -> headers.frameOptions(frame -> frame.disable())); + return http.build(); + } +} diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index 2e555e1..bdf8741 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -144,7 +144,7 @@ public String processUpdate( } try { - visaService.updateVisa(updateVisaDTO, currentUserId); + visaService.updateVisa(id, updateVisaDTO, currentUserId); } catch (IllegalArgumentException e) { bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage()); prepareApplyModel(currentUserId, model); diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index 5dba717..724f767 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -1,4 +1,5 @@ package org.example.visacasemanagementsystem.visa.service; +import ch.qos.logback.core.joran.conditional.IfAction; import jakarta.persistence.EntityNotFoundException; import org.example.visacasemanagementsystem.audit.AuditEventType; import org.example.visacasemanagementsystem.audit.service.AuditService; @@ -186,8 +187,12 @@ public VisaDTO applyForVisa(CreateVisaDTO dto, Long userId) { } @Transactional - public VisaDTO updateVisa(UpdateVisaDTO dto, Long userId) { - Visa visa = visaRepository.findById(dto.id()) + public VisaDTO updateVisa(Long visaId, UpdateVisaDTO dto, Long userId) { + if (!visaId.equals(dto.id())) { + throw new IllegalArgumentException("Mismatched visa id."); + } + + Visa visa = visaRepository.findById(visaId) .orElseThrow(() -> new EntityNotFoundException("Visa not found")); if (!visa.getApplicant().getId().equals(userId)) { diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java index e576767..68acd79 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java @@ -134,7 +134,7 @@ void updateVisa_shouldUpdateVisaAndResetStatus_WhenUserIsAuthorized() { when(visaRepository.save(any(Visa.class))).thenAnswer(i -> i.getArgument(0)); // Act - visaService.updateVisa(dto, userId); + visaService.updateVisa(visaId, dto, userId ); // Assert assertThat(visa.getVisaType()).isEqualTo(VisaType.TOURIST); @@ -149,6 +149,7 @@ void updateVisa_shouldUpdateVisaAndResetStatus_WhenUserIsAuthorized() { @Test void updateVisa_shouldThrowUnauthorizedException_WhenUserIsNotTheApplicant() { // Arrange + Long visaId = 100L; Long actualApplicantId = 3L; Long unauthorizedUserId = 99L; @@ -165,7 +166,7 @@ void updateVisa_shouldThrowUnauthorizedException_WhenUserIsNotTheApplicant() { when(visaRepository.findById(anyLong())).thenReturn(Optional.of(visa)); // Act & Assert - assertThatThrownBy(() -> visaService.updateVisa(dto, unauthorizedUserId)) + assertThatThrownBy(() -> visaService.updateVisa(visaId, dto,unauthorizedUserId)) .isInstanceOf(UnauthorizedException.class) .hasMessage("You are not authorized to update this application."); @@ -174,6 +175,7 @@ void updateVisa_shouldThrowUnauthorizedException_WhenUserIsNotTheApplicant() { @Test void updateVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInPast() { // Arrange + Long visaId = 100L; Long userId = 1L; LocalDate pastDate = LocalDate.now().minusDays(1); @@ -189,7 +191,7 @@ void updateVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInPast() { when(visaRepository.findById(anyLong())).thenReturn(Optional.of(visa)); // Act & Assert - assertThatThrownBy(() -> visaService.updateVisa(dto, userId)) + assertThatThrownBy(() -> visaService.updateVisa(visaId, dto, userId)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Travel date cannot be in the past."); From 0f4002a1becec73bdfed80e170c4f794bfb82a2f Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 15:42:02 +0200 Subject: [PATCH 17/21] fix: removed unused import --- .../visacasemanagementsystem/visa/service/VisaService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index 724f767..b3db18d 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -1,5 +1,4 @@ package org.example.visacasemanagementsystem.visa.service; -import ch.qos.logback.core.joran.conditional.IfAction; import jakarta.persistence.EntityNotFoundException; import org.example.visacasemanagementsystem.audit.AuditEventType; import org.example.visacasemanagementsystem.audit.service.AuditService; From b84820b373bec24db9f33a621db204f3727b5e32 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 18:58:15 +0200 Subject: [PATCH 18/21] fix: more suggested fixed from CodeRabbit --- .../visa/controller/VisaViewController.java | 3 +++ .../visacasemanagementsystem/visa/service/VisaService.java | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index bdf8741..cbf36f1 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -149,6 +149,9 @@ public String processUpdate( bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage()); prepareApplyModel(currentUserId, model); model.addAttribute("isEdit", true); + VisaDTO visa = visaService.findVisaDtoById(id); + model.addAttribute("statusInformation", visa.statusInformation()); + return "visa/edit-form"; } return "redirect:/visas/" + id + "?currentUserId=" + currentUserId; diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index 97fafb7..d2d66a9 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -198,6 +198,10 @@ public VisaDTO updateVisa(Long visaId, UpdateVisaDTO dto, Long userId) { throw new UnauthorizedException("You are not authorized to update this application."); } + if (visa.getVisaStatus() != VisaStatus.INCOMPLETE && visa.getVisaStatus() != VisaStatus.SUBMITTED ) { + throw new IllegalArgumentException("This application can no longer be edited."); + } + validateTravelDate(dto.travelDate()); visa.setVisaType(dto.visaType()); From 03701f6a0dd7b3f19f84498919cf8f434b3b6856 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 19:15:53 +0200 Subject: [PATCH 19/21] fix: update tests in visaServiceTest to align with recent change in VisaService --- .../visa/VisaServiceTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java index 68acd79..8113ef6 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java @@ -172,6 +172,31 @@ void updateVisa_shouldThrowUnauthorizedException_WhenUserIsNotTheApplicant() { } + @Test + void updateVisa_shouldThrowIllegalArgumentException_WhenStatusIsNotEditable() { + Long visaId = 100L; + Long userId = 1L; + UpdateVisaDTO dto = new UpdateVisaDTO( + visaId, VisaType.STUDY, + VisaStatus.SUBMITTED, "SE", "123", + LocalDate.now().plusDays(10), null); + + User user = new User(); + user.setId(userId); + + Visa visa = new Visa(); + visa.setApplicant(user); + visa.setVisaStatus(VisaStatus.GRANTED); + + when(visaRepository.findById(anyLong())).thenReturn(Optional.of(visa)); + + // Act & Assert + assertThatThrownBy(() -> visaService.updateVisa(visaId, dto,userId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("This application can no longer be edited."); + } + + @Test void updateVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInPast() { // Arrange @@ -187,6 +212,7 @@ void updateVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInPast() { Visa visa = new Visa(); visa.setApplicant(actualUser); + visa.setVisaStatus(VisaStatus.SUBMITTED); when(visaRepository.findById(anyLong())).thenReturn(Optional.of(visa)); From 4ffd190b95c5a7fea3e20537272d8a85cc5aacbe Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 20:16:05 +0200 Subject: [PATCH 20/21] fix: update visa validation and apply/edit-form to handle global errors --- .../visa/controller/VisaViewController.java | 7 ++++++- .../visacasemanagementsystem/visa/service/VisaService.java | 6 +++++- src/main/resources/templates/visa/apply-form.html | 5 +++++ src/main/resources/templates/visa/edit-form.html | 5 +++++ .../visacasemanagementsystem/visa/VisaServiceTest.java | 1 - 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java index cbf36f1..c55e419 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java @@ -146,7 +146,12 @@ public String processUpdate( try { visaService.updateVisa(id, updateVisaDTO, currentUserId); } catch (IllegalArgumentException e) { - bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage()); + if (e.getMessage().contains("date")) { + bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage()); + } else { + bindingResult.reject("globalError",e.getMessage()); + } + prepareApplyModel(currentUserId, model); model.addAttribute("isEdit", true); VisaDTO visa = visaService.findVisaDtoById(id); diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index d2d66a9..fb0355a 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -340,7 +340,11 @@ public User validateHandler(Long handlerId) throws UnauthorizedException { } public void validateTravelDate(LocalDate travelDate) { - if (travelDate == null || travelDate.isBefore(LocalDate.now())) { + if (travelDate == null) { + throw new IllegalArgumentException("Travel date cannot be in the past."); + } + + if (travelDate.isBefore(LocalDate.now())) { throw new IllegalArgumentException("Travel date cannot be in the past."); } } diff --git a/src/main/resources/templates/visa/apply-form.html b/src/main/resources/templates/visa/apply-form.html index 14396e1..a76b04c 100644 --- a/src/main/resources/templates/visa/apply-form.html +++ b/src/main/resources/templates/visa/apply-form.html @@ -117,6 +117,11 @@

New Application

+
+

+
+
diff --git a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java index 8113ef6..92905f4 100644 --- a/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java +++ b/src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceTest.java @@ -196,7 +196,6 @@ void updateVisa_shouldThrowIllegalArgumentException_WhenStatusIsNotEditable() { .hasMessage("This application can no longer be edited."); } - @Test void updateVisa_shouldThrowIllegalArgumentException_WhenTravelDateIsInPast() { // Arrange From 5f4d78959bdb89554916b53cbb6ceb78359c4f35 Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 14 Apr 2026 20:29:08 +0200 Subject: [PATCH 21/21] fix: last suggested fix from CodeRabbit --- .../visacasemanagementsystem/visa/service/VisaService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java index fb0355a..708dbe3 100644 --- a/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java +++ b/src/main/java/org/example/visacasemanagementsystem/visa/service/VisaService.java @@ -341,7 +341,7 @@ public User validateHandler(Long handlerId) throws UnauthorizedException { public void validateTravelDate(LocalDate travelDate) { if (travelDate == null) { - throw new IllegalArgumentException("Travel date cannot be in the past."); + throw new IllegalArgumentException("Travel date is required."); } if (travelDate.isBefore(LocalDate.now())) {