From 98dc502c826b41ffade8ee4d3688541ee4356c42 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Wed, 15 Apr 2026 14:50:30 +0200 Subject: [PATCH 1/2] allow agents and admin to update ticket priority --- .../application/TicketOrchestrator.java | 42 +++++++++++++++++++ .../dto/UpdateTicketPriorityRequest.java | 15 +++++++ .../casemanager/ticket/http/TicketApi.java | 11 +++++ .../ticket/http/TicketController.java | 13 ++++++ .../user/domain/rbac/RoleName.java | 1 - 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketPriorityRequest.java diff --git a/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java b/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java index 2699a76..6ce77cf 100644 --- a/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java +++ b/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java @@ -2,12 +2,14 @@ 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.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; @@ -175,6 +177,46 @@ 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 addComment(UUID ticketId, TicketCommentRequest request, UUID actorId) { User actor = userLookupService.requireActor(actorId); diff --git a/src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketPriorityRequest.java b/src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketPriorityRequest.java new file mode 100644 index 0000000..a528db5 --- /dev/null +++ b/src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketPriorityRequest.java @@ -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 +) { +} diff --git a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java index 82cafd7..835802c 100644 --- a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java +++ b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java @@ -7,6 +7,7 @@ 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; @@ -127,6 +128,16 @@ ResponseEntity> 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> changeTicketPriority( + @Parameter(hidden = true) @CurrentUser User actor, + @PathVariable UUID ticketId, + @Valid @RequestBody UpdateTicketPriorityRequest request + ); + @Operation(summary = "Add a comment (public or internal work note; visibility is enforced server-side)") @ApiResponse(responseCode = "202", description = "Accepted", useReturnTypeSchema = true) @NotFoundApiResponse diff --git a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java index fec3bc0..2cf2e3e 100644 --- a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java +++ b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java @@ -1,6 +1,7 @@ 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; @@ -127,6 +128,18 @@ public ResponseEntity> changeTicketStatus( return ResponseEntity.ok(new ApiResponseWrapper<>(response)); } + @PatchMapping("{ticketId}/priority") + @Override + public ResponseEntity> 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)); + } + @PostMapping("{ticketId}/comment") @Override public ResponseEntity> comment( diff --git a/src/main/java/org/fmazmz/casemanager/user/domain/rbac/RoleName.java b/src/main/java/org/fmazmz/casemanager/user/domain/rbac/RoleName.java index 2b99f44..c5d14d8 100644 --- a/src/main/java/org/fmazmz/casemanager/user/domain/rbac/RoleName.java +++ b/src/main/java/org/fmazmz/casemanager/user/domain/rbac/RoleName.java @@ -23,7 +23,6 @@ public enum RoleName { REPORTER(EnumSet.of( TicketAction.CREATE, TicketAction.READ, - TicketAction.UPDATE, TicketAction.COMMENT_PUBLIC, TicketAction.UPLOAD_ATTACHMENT, TicketAction.REOPEN From f69f6b8e809ab5e5c0dbeee293c0486890100ed2 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Wed, 15 Apr 2026 14:56:10 +0200 Subject: [PATCH 2/2] allow agents to update title and description --- .../application/TicketOrchestrator.java | 53 +++++++++++++++++++ .../ticket/dto/UpdateTicketRequest.java | 11 ++++ .../casemanager/ticket/http/TicketApi.java | 11 ++++ .../ticket/http/TicketController.java | 13 +++++ 4 files changed, 88 insertions(+) create mode 100644 src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketRequest.java diff --git a/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java b/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java index 6ce77cf..f80756b 100644 --- a/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java +++ b/src/main/java/org/fmazmz/casemanager/ticket/application/TicketOrchestrator.java @@ -5,6 +5,7 @@ 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; @@ -28,6 +29,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.util.UUID; @@ -217,6 +219,57 @@ public TicketResponse changePriority(UUID ticketId, UpdateTicketPriorityRequest 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); diff --git a/src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketRequest.java b/src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketRequest.java new file mode 100644 index 0000000..8ee16b0 --- /dev/null +++ b/src/main/java/org/fmazmz/casemanager/ticket/dto/UpdateTicketRequest.java @@ -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 +) { +} diff --git a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java index 835802c..acf2ed2 100644 --- a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java +++ b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketApi.java @@ -12,6 +12,7 @@ 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; @@ -138,6 +139,16 @@ ResponseEntity> changeTicketPriority( @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> 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 diff --git a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java index 2cf2e3e..d9e94f3 100644 --- a/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java +++ b/src/main/java/org/fmazmz/casemanager/ticket/http/TicketController.java @@ -6,6 +6,7 @@ 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; @@ -140,6 +141,18 @@ public ResponseEntity> changeTicketPriority( return ResponseEntity.ok(new ApiResponseWrapper<>(response)); } + @PatchMapping("{ticketId}") + @Override + public ResponseEntity> 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> comment(