diff --git a/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java b/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java index 1a2d8fc..8b2ed19 100644 --- a/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java +++ b/src/main/java/org/example/alfs/dto/comment/CommentCreateDTO.java @@ -12,4 +12,7 @@ public class CommentCreateDTO { @NotBlank private String message; -} + + private boolean internalNote = false; + +} \ No newline at end of file diff --git a/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java b/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java new file mode 100644 index 0000000..f240eef --- /dev/null +++ b/src/main/java/org/example/alfs/mapper/TicketCommentMapper.java @@ -0,0 +1,26 @@ +package org.example.alfs.mapper; + +import org.example.alfs.dto.comment.CommentViewDTO; +import org.example.alfs.entities.TicketComment; +import org.springframework.stereotype.Component; + +@Component +public class TicketCommentMapper { + public CommentViewDTO entityToViewDTO(TicketComment comment) { + CommentViewDTO dto = new CommentViewDTO(); + + dto.setId(comment.getId()); + dto.setMessage(comment.getMessage()); + dto.setCreatedAt(comment.getCreatedAt()); + + if (comment.getAuthor() != null) { + dto.setAuthor(comment.getAuthor().getUsername()); + dto.setRole(comment.getAuthor().getRole().name()); + } else { + dto.setAuthor("Anonymous"); + dto.setRole(null); + } + + return dto; + } +} diff --git a/src/main/java/org/example/alfs/services/TicketCommentService.java b/src/main/java/org/example/alfs/services/TicketCommentService.java new file mode 100644 index 0000000..209799e --- /dev/null +++ b/src/main/java/org/example/alfs/services/TicketCommentService.java @@ -0,0 +1,75 @@ +package org.example.alfs.services; + +import org.example.alfs.dto.comment.CommentCreateDTO; +import org.example.alfs.dto.comment.CommentViewDTO; +import org.example.alfs.entities.Ticket; +import org.example.alfs.entities.TicketComment; +import org.example.alfs.entities.User; +import org.example.alfs.enums.Role; +import org.example.alfs.mapper.TicketCommentMapper; +import org.example.alfs.repositories.TicketCommentRepository; +import org.example.alfs.repositories.TicketRepository; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Service +public class TicketCommentService { + + private final TicketRepository ticketRepository; + private final TicketCommentRepository ticketCommentRepository; + private final TicketCommentMapper ticketCommentMapper; + + public TicketCommentService(TicketRepository ticketRepository, + TicketCommentRepository ticketCommentRepository, + TicketCommentMapper ticketCommentMapper) { + this.ticketRepository = ticketRepository; + this.ticketCommentRepository = ticketCommentRepository; + this.ticketCommentMapper = ticketCommentMapper; + } + + @Transactional + public CommentViewDTO addComment(Long ticketId, CommentCreateDTO dto, User author) { + Ticket ticket = ticketRepository.findById(ticketId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + + TicketComment comment = new TicketComment(); + comment.setTicket(ticket); + comment.setAuthor(author); + comment.setMessage(dto.getMessage()); + + boolean internalNote = dto.isInternalNote(); + if (internalNote && (author == null || (author.getRole() != Role.ADMIN && author.getRole() != Role.INVESTIGATOR))) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Only investigators/admins can create internal notes"); + } + comment.setInternalNote(internalNote); + + TicketComment savedComment = ticketCommentRepository.save(comment); + + return ticketCommentMapper.entityToViewDTO(savedComment); + } + + @Transactional(readOnly = true) + public List getComments(Long ticketId, User actor) { + List all = ticketCommentRepository.findByTicketIdOrderByCreatedAtAsc(ticketId); + + if (all.isEmpty()) { + ticketRepository.findById(ticketId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + } + + if (actor == null || actor.getRole() == Role.REPORTER) { + return all.stream() + .filter(comment -> !comment.isInternalNote()) + .map(ticketCommentMapper::entityToViewDTO) + .toList(); + } + + return all.stream() + .map(ticketCommentMapper::entityToViewDTO) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/alfs/services/TicketService.java b/src/main/java/org/example/alfs/services/TicketService.java index 4bfb1eb..029736e 100644 --- a/src/main/java/org/example/alfs/services/TicketService.java +++ b/src/main/java/org/example/alfs/services/TicketService.java @@ -3,56 +3,60 @@ import org.example.alfs.dto.ticket.TicketCreateDTO; import org.example.alfs.dto.ticket.TicketViewDTO; import org.example.alfs.entities.Ticket; +import org.example.alfs.entities.User; +import org.example.alfs.enums.Role; import org.example.alfs.enums.TicketStatus; import org.example.alfs.mapper.TicketMapper; import org.example.alfs.repositories.TicketRepository; +import org.example.alfs.repositories.UserRepository; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.Set; @Service public class TicketService { - private final TicketRepository ticketRepository; private final TicketMapper ticketMapper; + private final UserRepository userRepository; - public TicketService(TicketRepository ticketRepository, TicketMapper ticketMapper) { + public TicketService(TicketRepository ticketRepository, TicketMapper ticketMapper, UserRepository userRepository) { this.ticketRepository = ticketRepository; this.ticketMapper = ticketMapper; + this.userRepository = userRepository; } //createNewTicket public TicketViewDTO createNewTicket(TicketCreateDTO ticketCreateDTO) { - - Ticket ticket = new Ticket(); + Ticket ticket = new Ticket(); ticket.setTitle(ticketCreateDTO.getTitle()); ticket.setDescription(ticketCreateDTO.getDescription()); - Ticket save = ticketRepository.save(ticket); + Ticket savedTicket = ticketRepository.save(ticket); - return ticketMapper.entityToViewDTO(save); + return ticketMapper.entityToViewDTO(savedTicket); } // View by token public TicketViewDTO getTicketByToken(String token) { - Ticket ticket = ticketRepository.findByReporterToken(token). orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + return ticketMapper.entityToViewDTO(ticket); } //findById public TicketViewDTO getTicketById(Long id) { - Ticket ticket = ticketRepository.findById(id). orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); - return ticketMapper.entityToViewDTO(ticket); + return ticketMapper.entityToViewDTO(ticket); } //findByReporterId @@ -64,32 +68,149 @@ public List getTicketsByReporterId(Long reporterId) { } //findByInvestigatorId - public List getTicketsByInvestigatorId(Long investigatorId){ - return ticketRepository.findByInvestigatorId(investigatorId) - .stream() - .map(ticketMapper::entityToViewDTO) - .toList(); - } + public List getTicketsByInvestigatorId(Long investigatorId) { + return ticketRepository.findByInvestigatorId(investigatorId) + .stream() + .map(ticketMapper::entityToViewDTO) + .toList(); + } //findByStatus - public List getTicketsByStatus(TicketStatus status) { - return ticketRepository.findByStatus(status) - .stream() - .map(ticketMapper::entityToViewDTO) - .toList(); - } + public List getTicketsByStatus(TicketStatus status) { + return ticketRepository.findByStatus(status) + .stream() + .map(ticketMapper::entityToViewDTO) + .toList(); + } //findByStatusAndInvestigatorId - public List getTicketsByStatusAndInvestigator( - TicketStatus status, - Long investigatorId) { - - return ticketRepository - .findByStatusAndInvestigatorId(status, investigatorId) - .stream() - .map(ticketMapper::entityToViewDTO) - .toList(); - } + public List getTicketsByStatusAndInvestigator( + TicketStatus status, + Long investigatorId) { + return ticketRepository + .findByStatusAndInvestigatorId(status, investigatorId) + .stream() + .map(ticketMapper::entityToViewDTO) + .toList(); + } //findAll (pageable) + + @Transactional + public TicketViewDTO updateTicketStatus(Long id, TicketStatus newStatus) { + // TODO: Check role? Is user is Admin or Investigator? +// Typ/Placeholder: +// if (user.getRole() != Role.ADMIN && user.getRole() != Role.INVESTIGATOR) { +// throw new AccessDeniedException("Only admins or investigators can update ticket status"); +// } +// */ + + Ticket ticket = ticketRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + + TicketStatus oldStatus = ticket.getStatus(); + + // No-op check + if (oldStatus == newStatus) { + return ticketMapper.entityToViewDTO(ticket); + } + + Set allowedTransitions = ALLOWED_TRANSITIONS.getOrDefault(ticket.getStatus(), Set.of()); + + if (!allowedTransitions.contains(newStatus)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid ticket status transition: Cannot transition from " + ticket.getStatus() + " to " + newStatus); + } + + if (newStatus == TicketStatus.IN_PROGRESS && ticket.getInvestigator() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot move ticket to IN_PROGRESS without an assigned investigator"); + } + + ticket.setStatus(newStatus); + Ticket savedTicket = ticketRepository.save(ticket); + + // TODO: Audit log(-service?), auditLogService.log() +// Typ/Placeholder: +// auditLogService.log(ticket.getId(), user, "STATUS_CHANGED", +// "Status changed from " + oldStatus + " to " + newStatus); + + return ticketMapper.entityToViewDTO(savedTicket); + } + + // Bestäm vilka övergångar/transitions som är tillåtna + private static final Map> ALLOWED_TRANSITIONS = Map.of( + TicketStatus.OPEN, Set.of(TicketStatus.IN_PROGRESS), + TicketStatus.IN_PROGRESS, Set.of(TicketStatus.RESOLVED), + TicketStatus.RESOLVED, Set.of(TicketStatus.CLOSED, TicketStatus.IN_PROGRESS), + TicketStatus.CLOSED, Set.of() + ); + + @Transactional + public TicketViewDTO assignInvestigator(Long id, Long investigatorId) { + // TODO Check if user is admin? +// Typ/Placeholder: +// if (user.getRole() != Role.ADMIN) { +// throw new AccessDeniedException("Only admins can assign handlers"); +// } +// */ + + Ticket ticket = ticketRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + + if (ticket.getInvestigator() != null) { + throw new ResponseStatusException(HttpStatus.CONFLICT, ("Ticket already has an investigator assigned")); + } + + if (ticket.getStatus() != TicketStatus.OPEN) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("Can only assign investigator to an OPEN ticket, current status: " + ticket.getStatus())); + } + + User investigator = userRepository.findById(investigatorId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Investigator not found")); + + if (investigator.getRole() != Role.INVESTIGATOR) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("User is not an investigator")); + } + + ticket.setInvestigator(investigator); + ticket.setStatus(TicketStatus.IN_PROGRESS); + Ticket savedTicket = ticketRepository.save(ticket); + // TODO: auditLogService.log() +// Typ/Placeholder: +// auditLogService.log(ticket.getId(), user, "ASSIGNED", +// "Ticket assigned to " + investigatorId); + + return ticketMapper.entityToViewDTO(savedTicket); + } + + @Transactional + public TicketViewDTO unassignInvestigator(Long id) { + // TODO: Check if user is admin? +// Typ/Placeholder: +// if (actor.getRole() != Role.ADMIN) { +// throw new AccessDeniedException("Only admins can unassign handlers"); +// } +// */ + + Ticket ticket = ticketRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found")); + + if (ticket.getInvestigator() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("Ticket does not have an investigator assigned")); + } + + if (ticket.getStatus() != TicketStatus.IN_PROGRESS) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ("Can only unassign investigator from an IN_PROGRESS ticket, current status: " + ticket.getStatus())); + } + + ticket.setInvestigator(null); + ticket.setStatus(TicketStatus.OPEN); + Ticket savedTicket = ticketRepository.save(ticket); + // TODO: auditLogService.log() +// Typ/Placeholder: +// auditLogService.log(ticket.getId(), user, "UNASSIGNED", +// "Ticket unassigned from " + investigatorId); + + return ticketMapper.entityToViewDTO(savedTicket); + } + }