Skip to content

feat: add manual admin assignment for PENDING tickets - #65

Merged
ikennot merged 2 commits into
developmentfrom
feat/admin-assign
Apr 12, 2026
Merged

feat: add manual admin assignment for PENDING tickets#65
ikennot merged 2 commits into
developmentfrom
feat/admin-assign

Conversation

@ikennot

@ikennot ikennot commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Type of Change

  • feat — new feature

Labels

p2-medium feat backend

What Changed

  • Added assigned_admin_id (nullable UUID FK) column to the ticket table via Flyway migration V13
  • Added assignedAdminId field to Ticket entity
  • Added AssignTicketRequest DTO with optional adminId field
  • Added assignedAdminId and assignedAdminName fields to TicketResponse
  • Added assignAdmin() service method in TicketService with validation
  • Added PATCH /api/tickets/{id}/assign endpoint (ADMIN only) in TicketController

Why

Admins had no way to indicate ownership of a PENDING ticket. All admins received broadcast notifications but no one was formally assigned. This adds a lightweight assignment mechanism — purely informational, no access restrictions — so admins can coordinate who handles which ticket.

How to Test

  1. Create a ticket as a STUDENT → verify assignedAdminId is null in response
  2. Call PATCH /api/tickets/{id}/assign as ADMIN with no body ({}) → ticket is self-assigned to the authenticated admin; response shows assignedAdminId and assignedAdminName
  3. Call PATCH /api/tickets/{id}/assign as ADMIN with { "adminId": "<uuid>" } → ticket is assigned to the specified admin; that admin receives a notification
  4. Call the endpoint on a non-PENDING ticket → expect 409 INVALID_ASSIGNMENT
  5. Pass an adminId belonging to a STUDENT profile → expect 400 INVALID_ADMIN
  6. Pass a non-existent adminId → expect 404 ADMIN_NOT_FOUND
  7. Verify all admins can still view and update the ticket regardless of who is assigned

Related Issues

Screenshots

N/A

Checklist

  • Code follows the Google Java Style Guide
  • No business logic in controllers
  • Ownership + role checks are in the service layer
  • All responses use ApiResponse<T> wrapper
  • No secrets or credentials committed
  • application.yaml is NOT staged
  • CI passes

Summary by CodeRabbit

  • New Features

    • New admin ticket assignment functionality allows administrators to assign support tickets to specific team members for better ticket distribution and management.
    • Ticket response now includes the assigned admin's name and identifier for improved visibility.
  • Improvements

    • Administrators now have access to notification synchronization capabilities, previously available only to students.

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19409cfd-b3f0-41be-856b-d42a89bdc5a9

📥 Commits

Reviewing files that changed from the base of the PR and between 7e8faa0 and ee82678.

📒 Files selected for processing (1)
  • src/main/java/com/ucms_backend/controller/SyncController.java

📝 Walkthrough

Walkthrough

This PR adds the capability to assign tickets to admin users. It introduces a new PATCH endpoint for ticket assignment, adds database persistence to track assigned admins, includes request/response DTOs, implements service-layer assignment logic with validation, and extends authorization for the sync notifications endpoint to admin users.

Changes

Cohort / File(s) Summary
Ticket Assignment Endpoint
src/main/java/com/ucms_backend/controller/TicketController.java
Added new assignAdmin PATCH endpoint restricted to ADMIN role. Accepts AssignTicketRequest body and returns ticket response with assigned admin details.
Request and Response DTOs
src/main/java/com/ucms_backend/dto/AssignTicketRequest.java, src/main/java/com/ucms_backend/dto/TicketResponse.java
Created AssignTicketRequest DTO with adminId field. Extended TicketResponse with assignedAdminId and assignedAdminName fields; added factory method overload to populate assigned admin information.
Entity Model
src/main/java/com/ucms_backend/model/entity/Ticket.java
Added nullable assignedAdminId UUID field with column mapping to assigned_admin_id.
Service Layer
src/main/java/com/ucms_backend/service/TicketService.java
Implemented assignAdmin method with status validation (PENDING only), optional fallback to current user, admin role verification, and notification creation. Added resolveAdminName helper and updated response mapping to include assigned admin names.
Database Schema
src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql
Migration adds assigned_admin_id UUID column to ticket table with foreign key constraint referencing profile(auth_user_id).
Authorization Update
src/main/java/com/ucms_backend/controller/SyncController.java
Expanded syncNotifications endpoint access from STUDENT role only to hasAnyRole('STUDENT','ADMIN').

Sequence Diagram

sequenceDiagram
    participant Client
    participant TicketController
    participant TicketService
    participant Database
    participant ProfileService
    participant NotificationService

    Client->>TicketController: PATCH /api/tickets/{id}/assign<br/>AssignTicketRequest(adminId)
    TicketController->>TicketService: assignAdmin(ticketId, request)
    TicketService->>Database: Load Ticket by ID
    Database-->>TicketService: Ticket entity
    TicketService->>TicketService: Validate ticket status == PENDING
    TicketService->>TicketService: Determine target adminId<br/>(from request or current user)
    TicketService->>ProfileService: Load admin Profile
    ProfileService-->>TicketService: Profile with role
    TicketService->>TicketService: Validate role == ADMIN
    TicketService->>Database: Persist ticket with assignedAdminId
    Database-->>TicketService: Updated Ticket
    TicketService->>NotificationService: Create notification for admin
    TicketService->>TicketService: Resolve admin name & build response
    TicketService-->>TicketController: TicketResponse with assigned admin info
    TicketController-->>Client: ApiResponse<TicketResponse><br/>"Admin assigned"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A ticket finds its guardian swift,
When admins claim their noble gift,
The PATCH endpoint does assign,
With validation checks divine,
And notifications dance in glee! 📬✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding manual admin assignment capability for PENDING tickets, which aligns with the core changes across controller, service, entity, and DTO layers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-assign

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql (1)

1-2: Add an index for assigned_admin_id to avoid future FK lookup slowdowns.

Line [2] adds a foreign key but no supporting index. This can hurt assignment-related reads and FK maintenance on larger tables.

Suggested migration addition
 ALTER TABLE ticket
     ADD COLUMN assigned_admin_id UUID REFERENCES profile(auth_user_id);
+
+CREATE INDEX idx_ticket_assigned_admin_id
+    ON ticket(assigned_admin_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql` around
lines 1 - 2, The migration adds ticket.assigned_admin_id but omits an index;
update V13__add_ticket_assigned_admin.sql to create a supporting index on the
assigned_admin_id column (e.g., a non-unique index named like
idx_ticket_assigned_admin_id) so FK lookups and assignment queries are fast;
ensure the index creation statement is added after the ADD COLUMN/REFERENCES
statement and, if running in production with minimal locking, consider using
CREATE INDEX CONCURRENTLY for the index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/ucms_backend/service/TicketService.java`:
- Around line 351-380: The assignAdmin method updates and saves the Ticket but
never publishes a realtime event; after saving the ticket (Ticket saved =
ticketRepository.save(ticket)) invoke publishTicketEvent(saved) (or the existing
publishTicketEvent method used by other mutations) so the SSE/WS subscribers
receive the assignment update, and keep the existing
notificationService.createNotification and return toDetailedResponse(saved) as
before.
- Around line 75-99: The code currently calls resolveAdminName(UUID) inside
toResponse/toDetailedResponse for each ticket, causing N+1 queries; instead,
collect all assignedAdminId values for a ticket list, query
profileRepository.findAllById(...) once to build a Map<UUID,String> of
adminId->name, and change the mappers to accept that map (e.g., add overloads
toResponse(Ticket, Set<Long>, Map<UUID,String>) and toDetailedResponse(Ticket,
Map<UUID,String>) or modify resolveAdminName to accept the map) so the mapping
logic uses map.get(adminId) rather than profileRepository.findById per ticket;
ensure callers that build lists compute the map and pass it into the mapper
methods.

---

Nitpick comments:
In `@src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql`:
- Around line 1-2: The migration adds ticket.assigned_admin_id but omits an
index; update V13__add_ticket_assigned_admin.sql to create a supporting index on
the assigned_admin_id column (e.g., a non-unique index named like
idx_ticket_assigned_admin_id) so FK lookups and assignment queries are fast;
ensure the index creation statement is added after the ADD COLUMN/REFERENCES
statement and, if running in production with minimal locking, consider using
CREATE INDEX CONCURRENTLY for the index.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39509c4d-41f7-4c48-bb8d-5fd4bd367ea4

📥 Commits

Reviewing files that changed from the base of the PR and between 4520d25 and 7e8faa0.

📒 Files selected for processing (6)
  • src/main/java/com/ucms_backend/controller/TicketController.java
  • src/main/java/com/ucms_backend/dto/AssignTicketRequest.java
  • src/main/java/com/ucms_backend/dto/TicketResponse.java
  • src/main/java/com/ucms_backend/model/entity/Ticket.java
  • src/main/java/com/ucms_backend/service/TicketService.java
  • src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql

Comment on lines +75 to 99
private String resolveAdminName(UUID adminId) {
if (adminId == null) return null;
return profileRepository.findById(adminId)
.map(Profile::getName)
.orElse(null);
}

private TicketResponse toResponse(Ticket ticket) {
boolean hasAdminResponse = ticketResponseRepository.existsByTicketId(ticket.getId());
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse);
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse,
resolveAdminName(ticket.getAssignedAdminId()));
}

private TicketResponse toResponse(Ticket ticket, Set<Long> ticketIdsWithResponses) {
boolean hasAdminResponse = ticketIdsWithResponses.contains(ticket.getId());
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse);
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse,
resolveAdminName(ticket.getAssignedAdminId()));
}

private TicketResponse toDetailedResponse(Ticket ticket) {
Profile studentProfile = profileRepository.findById(ticket.getUserId()).orElse(null);
boolean hasAdminResponse = ticketResponseRepository.existsByTicketId(ticket.getId());
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), studentProfile, hasAdminResponse);
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), studentProfile, hasAdminResponse,
resolveAdminName(ticket.getAssignedAdminId()));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid N+1 profile queries when populating assignedAdminName.

Lines [75-79] are called from list mapping paths (Lines [84-85], [90-91], [97-98]), causing one profileRepository.findById(...) per ticket. This will degrade admin ticket-list performance.

Refactor direction (batch-resolve admin names)
+// Build once per list request
+private Map<UUID, String> resolveAdminNames(List<Ticket> tickets) {
+    Set<UUID> ids = tickets.stream()
+            .map(Ticket::getAssignedAdminId)
+            .filter(java.util.Objects::nonNull)
+            .collect(java.util.stream.Collectors.toSet());
+    if (ids.isEmpty()) return Map.of();
+    return profileRepository.findAllById(ids).stream()
+            .collect(java.util.stream.Collectors.toMap(Profile::getAuthUserId, Profile::getName));
+}

Then pass the map into list mappers instead of calling findById per ticket.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ucms_backend/service/TicketService.java` around lines 75 -
99, The code currently calls resolveAdminName(UUID) inside
toResponse/toDetailedResponse for each ticket, causing N+1 queries; instead,
collect all assignedAdminId values for a ticket list, query
profileRepository.findAllById(...) once to build a Map<UUID,String> of
adminId->name, and change the mappers to accept that map (e.g., add overloads
toResponse(Ticket, Set<Long>, Map<UUID,String>) and toDetailedResponse(Ticket,
Map<UUID,String>) or modify resolveAdminName to accept the map) so the mapping
logic uses map.get(adminId) rather than profileRepository.findById per ticket;
ensure callers that build lists compute the map and pass it into the mapper
methods.

Comment on lines +351 to +380
public TicketResponse assignAdmin(Long ticketId, AssignTicketRequest request) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new AppException(404, "TICKET_NOT_FOUND", "Ticket not found"));

if (ticket.getStatus() != TicketStatus.PENDING) {
throw new AppException(409, "INVALID_ASSIGNMENT", "Can only assign admin to PENDING tickets");
}

UUID targetAdminId = (request.getAdminId() != null)
? request.getAdminId()
: SecurityUtils.getCurrentUserId();

Profile admin = profileRepository.findById(targetAdminId)
.orElseThrow(() -> new AppException(404, "ADMIN_NOT_FOUND", "Admin not found"));

if (!"ADMIN".equals(admin.getRole())) {
throw new AppException(400, "INVALID_ADMIN", "Target user is not an admin");
}

ticket.setAssignedAdminId(targetAdminId);
Ticket saved = ticketRepository.save(ticket);

notificationService.createNotification(
targetAdminId,
saved.getId(),
"You have been assigned ticket #" + saved.getTicketNumber()
);

return toDetailedResponse(saved);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Publish a realtime ticket event after admin assignment.

assignAdmin(...) mutates ticket state (Line [370]) but does not call publishTicketEvent(...), unlike other ticket mutations. Clients relying on SSE may not see assignment updates until refresh.

Suggested fix
         ticket.setAssignedAdminId(targetAdminId);
         Ticket saved = ticketRepository.save(ticket);
+        publishTicketEvent(saved, "TICKET_ASSIGNED", "ADMIN");
 
         notificationService.createNotification(
                 targetAdminId,
                 saved.getId(),
                 "You have been assigned ticket #" + saved.getTicketNumber()
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ucms_backend/service/TicketService.java` around lines 351 -
380, The assignAdmin method updates and saves the Ticket but never publishes a
realtime event; after saving the ticket (Ticket saved =
ticketRepository.save(ticket)) invoke publishTicketEvent(saved) (or the existing
publishTicketEvent method used by other mutations) so the SSE/WS subscribers
receive the assignment update, and keep the existing
notificationService.createNotification and return toDetailedResponse(saved) as
before.

@ikennot
ikennot merged commit 60c5f18 into development Apr 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant