{"record":{"id":"eecb2ec81812d83d","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-action-a-ticket-in-status-status-reopen-it-first","errorCode":null,"errorMessage":"Cannot {action} a ticket in status {Status} — reopen it first.","messagePattern":"Cannot (.+?) a ticket in status (.+?) — reopen it first\\.","errorType":"exception","errorClass":"CustomException","httpStatus":409,"severity":"warning","filePath":"src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs","lineNumber":234,"sourceCode":"\n    private void TransitionStatus(TicketStatus next)\n    {\n        if (next == Status)\n        {\n            return;\n        }\n\n        var previous = Status;\n        Status = next;\n        AddDomainEvent(DomainEvent.Create<TicketStatusChangedDomainEvent>(\n            (id, ts) => new TicketStatusChangedDomainEvent(Id, previous, next, id, ts)));\n    }\n\n    private void ThrowIfClosedOrResolved(string action)\n    {\n        if (Status is TicketStatus.Closed or TicketStatus.Resolved)\n        {\n            throw new CustomException(\n                $\"Cannot {action} a ticket in status {Status} — reopen it first.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.Conflict);\n        }\n    }\n}\n","sourceCodeStart":216,"sourceCodeEnd":241,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs#L216-L241","documentation":"The Ticket aggregate guards closed/resolved tickets: ThrowIfClosedOrResolved fires a 409 CustomException whenever a mutating action (e.g. Assign) is attempted on a ticket whose Status is Closed or Resolved. Tickets are meant to be immutable once resolved/closed until explicitly Reopen()ed. This is a domain-rule conflict, not a data error.","triggerScenarios":"Calling ticket.Assign(assigneeUserId) (via AssignTicketCommand) while Status == Closed or Resolved; any future action routed through ThrowIfClosedOrResolved with action names like 'assign' on a closed/resolved ticket.","commonSituations":"Stale UI showing a resolved ticket where an operator clicks 'Assign'; two agents working the same ticket — one closes it, the other submits an assignment that lands after; automated workflows assigning resolved tickets for follow-up.","solutions":["Check the ticket status before calling Assign (fetch it or expose Status on the DTO) and skip/warn if Closed or Resolved.","Call ReopenTicket (ticket.Reopen()) first when the workflow genuinely requires reassigning a resolved ticket, then Assign.","Catch CustomException with 409 Conflict in the endpoint layer and surface 'reopen the ticket first' to the user.","Have the UI disable the Assign action when the ticket's status is Resolved/Closed to prevent the doomed call."],"exampleFix":"// before\nawait mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user });\n// after\nvar ticket = await dbContext.Tickets.FindAsync([id]);\nif (ticket is { Status: TicketStatus.Closed or TicketStatus.Resolved })\n    await mediator.Send(new ReopenTicketCommand { TicketId = id });\nawait mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user });","handlingStrategy":"validation","validationCode":"var ticket = await dbContext.Tickets.AsNoTracking().FirstOrDefaultAsync(t => t.Id == id);\nif (ticket is { Status: TicketStatus.Closed or TicketStatus.Resolved })\n    throw new InvalidOperationException(\"Reopen the ticket before assigning.\");","typeGuard":"static bool CanAssign(Ticket t) => t is { Status: not TicketStatus.Closed and not TicketStatus.Resolved };","tryCatchPattern":"try { await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user }); }\ncatch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { NotifyUser(\"Ticket must be reopened first.\"); }","preventionTips":["Read ticket Status before any mutating call and branch in the UI.","Disable Assign/Close actions client-side when status is Resolved or Closed.","Catch 409 CustomException centrally in the API client and map it to a friendly message.","Prefer explicit Reopen then Assign in automation workflows instead of assuming assign-on-resolved works."],"tags":["tickets","domain-rules","conflict","state-machine"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}