Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import org.fmazmz.casemanager.exception.AccessDeniedException;
import org.fmazmz.casemanager.audit.application.AuditLogWriter;
import org.fmazmz.casemanager.ticket.dto.UpdateTicketPriorityRequest;
import org.fmazmz.casemanager.ticket.dto.ChangeTicketStatusRequest;
import org.fmazmz.casemanager.ticket.dto.TicketCommentRequest;
import org.fmazmz.casemanager.ticket.dto.UpdateTicketRequest;
import org.fmazmz.casemanager.ticket.mapper.CommentMapper;
import org.fmazmz.casemanager.ticket.mapper.TicketMapper;
import org.fmazmz.casemanager.ticket.domain.Comment;
import org.fmazmz.casemanager.ticket.domain.CommentVisibility;
import org.fmazmz.casemanager.ticket.domain.Priority;
import org.fmazmz.casemanager.ticket.domain.Ticket;
import org.fmazmz.casemanager.ticket.domain.TicketAction;
import org.fmazmz.casemanager.ticket.domain.TicketStatus;
Expand All @@ -26,6 +29,7 @@

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.UUID;

Expand Down Expand Up @@ -175,6 +179,97 @@ public TicketResponse changeStatus(UUID ticketId, ChangeTicketStatusRequest requ
return TicketMapper.toDto(saved, permissionEvaluator.includeInternalComments(actor));
}

@Transactional
public TicketResponse changePriority(UUID ticketId, UpdateTicketPriorityRequest request, UUID actorId) {
User actor = userLookupService.requireActor(actorId);

if (!permissionEvaluator.hasPermission(actor, TicketAction.CHANGE_PRIORITY)) {
throw new AccessDeniedException("User is not authorized to perform action: " + TicketAction.CHANGE_PRIORITY);
}

Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new IllegalArgumentException("Ticket not found"));

Priority fromPriority = ticket.getPriority();
Priority toPriority = request.priority();

if (fromPriority == toPriority) {
return TicketMapper.toDto(ticket, permissionEvaluator.includeInternalComments(actor));
}

Comment internalComment = new Comment();
internalComment.setTicket(ticket);
internalComment.setUser(actor);
internalComment.setVisibility(CommentVisibility.INTERNAL);
internalComment.setMessage(request.internalComment().trim());
commentRepository.save(internalComment);

ticket.setPriority(toPriority);
Ticket saved = ticketRepository.saveAndFlush(ticket);

auditLogWriter.logChange(
saved,
actor,
TicketAction.CHANGE_PRIORITY,
"priority",
fromPriority != null ? fromPriority.name() : null,
toPriority.name()
);

return TicketMapper.toDto(saved, permissionEvaluator.includeInternalComments(actor));
}

@Transactional
public TicketResponse updateTicket(UUID ticketId, UpdateTicketRequest request, UUID actorId) {
User actor = userLookupService.requireActor(actorId);

if (!permissionEvaluator.hasPermission(actor, TicketAction.UPDATE)) {
throw new AccessDeniedException("User is not authorized to perform action: " + TicketAction.UPDATE);
}

if (request.title() == null && request.description() == null) {
throw new IllegalArgumentException("At least one field must be provided: title or description");
}

Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new IllegalArgumentException("Ticket not found"));

boolean changed = false;

if (request.title() != null) {
String newTitle = request.title().trim();
if (!StringUtils.hasText(newTitle)) {
throw new IllegalArgumentException("Title must not be blank");
}
if (!newTitle.equals(ticket.getTitle())) {
String oldTitle = ticket.getTitle();
ticket.setTitle(newTitle);
auditLogWriter.logChange(ticket, actor, TicketAction.UPDATE, "title", oldTitle, newTitle);
changed = true;
}
}

if (request.description() != null) {
String newDescription = request.description().trim();
if (!StringUtils.hasText(newDescription)) {
throw new IllegalArgumentException("Description must not be blank");
}
if (!newDescription.equals(ticket.getDescription())) {
String oldDescription = ticket.getDescription();
ticket.setDescription(newDescription);
auditLogWriter.logChange(ticket, actor, TicketAction.UPDATE, "description", oldDescription, newDescription);
changed = true;
}
}

if (!changed) {
return TicketMapper.toDto(ticket, permissionEvaluator.includeInternalComments(actor));
}

Ticket saved = ticketRepository.saveAndFlush(ticket);
return TicketMapper.toDto(saved, permissionEvaluator.includeInternalComments(actor));
}

@Transactional
public TicketResponse addComment(UUID ticketId, TicketCommentRequest request, UUID actorId) {
User actor = userLookupService.requireActor(actorId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.fmazmz.casemanager.ticket.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.fmazmz.casemanager.ticket.domain.Priority;

public record UpdateTicketPriorityRequest(
@NotNull
Priority priority,
@NotBlank
@Size(max = 5000, message = "Internal comment must be at most 5000 characters")
String internalComment
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.fmazmz.casemanager.ticket.dto;

import jakarta.validation.constraints.Size;

public record UpdateTicketRequest(
@Size(min = 5, max = 100, message = "Title must be between 5 and 100 characters")
String title,
@Size(min = 5, max = 5000, message = "Description must be between 5 and 5000 characters")
String description
) {
}
22 changes: 22 additions & 0 deletions src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import jakarta.validation.Valid;
import org.fmazmz.casemanager.common.api.openapi.NotFoundApiResponse;
import org.fmazmz.casemanager.common.api.openapi.StandardRestApiResponses;
import org.fmazmz.casemanager.ticket.dto.UpdateTicketPriorityRequest;
import org.fmazmz.casemanager.ticket.dto.ChangeTicketStatusRequest;
import org.fmazmz.casemanager.ticket.dto.CreateTicketRequest;
import org.fmazmz.casemanager.ticket.dto.TicketCommentRequest;
import org.fmazmz.casemanager.ticket.dto.TicketResponse;
import org.fmazmz.casemanager.ticket.dto.UpdateTicketRequest;
import org.fmazmz.casemanager.user.authentication.CurrentUser;
import org.fmazmz.casemanager.user.domain.User;
import org.fmazmz.casemanager.common.api.ApiResponseWrapper;
Expand Down Expand Up @@ -127,6 +129,26 @@ ResponseEntity<ApiResponseWrapper<TicketResponse>> changeTicketStatus(
@Valid @RequestBody ChangeTicketStatusRequest request
);

@Operation(summary = "Update ticket priority")
@ApiResponse(responseCode = "200", description = "Updated", useReturnTypeSchema = true)
@NotFoundApiResponse
@PatchMapping(path = "{ticketId}/priority", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponseWrapper<TicketResponse>> changeTicketPriority(
@Parameter(hidden = true) @CurrentUser User actor,
@PathVariable UUID ticketId,
@Valid @RequestBody UpdateTicketPriorityRequest request
);

@Operation(summary = "Update ticket title and/or description")
@ApiResponse(responseCode = "200", description = "Updated", useReturnTypeSchema = true)
@NotFoundApiResponse
@PatchMapping(path = "{ticketId}", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponseWrapper<TicketResponse>> updateTicket(
@Parameter(hidden = true) @CurrentUser User actor,
@PathVariable UUID ticketId,
@Valid @RequestBody UpdateTicketRequest request
);

@Operation(summary = "Add a comment (public or internal work note; visibility is enforced server-side)")
@ApiResponse(responseCode = "202", description = "Accepted", useReturnTypeSchema = true)
@NotFoundApiResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.fmazmz.casemanager.ticket.http;

import jakarta.validation.Valid;
import org.fmazmz.casemanager.ticket.dto.UpdateTicketPriorityRequest;
import org.fmazmz.casemanager.ticket.dto.ChangeTicketStatusRequest;
import org.fmazmz.casemanager.ticket.dto.CreateTicketRequest;
import org.fmazmz.casemanager.ticket.dto.TicketCommentRequest;
import org.fmazmz.casemanager.ticket.dto.TicketResponse;
import org.fmazmz.casemanager.ticket.dto.UpdateTicketRequest;
import org.fmazmz.casemanager.ticket.application.TicketOrchestrator;
import org.fmazmz.casemanager.ticket.application.TicketQueryFacade;
import org.fmazmz.casemanager.user.authentication.CurrentUser;
Expand Down Expand Up @@ -127,6 +129,30 @@ public ResponseEntity<ApiResponseWrapper<TicketResponse>> changeTicketStatus(
return ResponseEntity.ok(new ApiResponseWrapper<>(response));
}

@PatchMapping("{ticketId}/priority")
@Override
public ResponseEntity<ApiResponseWrapper<TicketResponse>> changeTicketPriority(
@CurrentUser User actor,
@PathVariable UUID ticketId,
@RequestBody @Valid UpdateTicketPriorityRequest request) {

TicketResponse response = ticketOrchestrator.changePriority(ticketId, request, actor.getId());

return ResponseEntity.ok(new ApiResponseWrapper<>(response));
}

@PatchMapping("{ticketId}")
@Override
public ResponseEntity<ApiResponseWrapper<TicketResponse>> updateTicket(
@CurrentUser User actor,
@PathVariable UUID ticketId,
@RequestBody @Valid UpdateTicketRequest request) {

TicketResponse response = ticketOrchestrator.updateTicket(ticketId, request, actor.getId());

return ResponseEntity.ok(new ApiResponseWrapper<>(response));
}

@PostMapping("{ticketId}/comment")
@Override
public ResponseEntity<ApiResponseWrapper<TicketResponse>> comment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public enum RoleName {
REPORTER(EnumSet.of(
TicketAction.CREATE,
TicketAction.READ,
TicketAction.UPDATE,
TicketAction.COMMENT_PUBLIC,
TicketAction.UPLOAD_ATTACHMENT,
TicketAction.REOPEN
Expand Down