{"record":{"id":"e3844263f67aaa39","repo":"fullstackhero/dotnet-starter-kit","slug":"a-closed-ticket-cannot-be-resolved-reopen-it-first","errorCode":null,"errorMessage":"A closed ticket cannot be resolved — reopen it first.","messagePattern":"A closed ticket cannot be resolved — reopen it first\\.","errorType":"exception","errorClass":"CustomException","httpStatus":409,"severity":"error","filePath":"src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs","lineNumber":120,"sourceCode":"        // ticket sends it back to Open since no owner is pushing it forward.\n        if (assigneeUserId is not null && Status == TicketStatus.Open)\n        {\n            TransitionStatus(TicketStatus.InProgress);\n        }\n        else if (assigneeUserId is null && Status == TicketStatus.InProgress)\n        {\n            TransitionStatus(TicketStatus.Open);\n        }\n\n        AddDomainEvent(DomainEvent.Create<TicketAssignedDomainEvent>(\n            (id, ts) => new TicketAssignedDomainEvent(Id, previous, assigneeUserId, id, ts)));\n    }\n\n    public void Resolve(string? resolutionNote)\n    {\n        if (Status == TicketStatus.Closed)\n        {\n            throw new CustomException(\n                \"A closed ticket cannot be resolved — reopen it first.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.Conflict);\n        }\n        if (Status == TicketStatus.Resolved)\n        {\n            return;\n        }\n\n        ResolutionNote = string.IsNullOrWhiteSpace(resolutionNote) ? null : resolutionNote.Trim();\n        ResolvedAtUtc = DateTime.UtcNow;\n        UpdatedAtUtc = DateTime.UtcNow;\n        TransitionStatus(TicketStatus.Resolved);\n    }\n\n    /// <summary>\n    /// Finalizes a resolved ticket (Resolved → Closed). Idempotent when already Closed;\n    /// rejects any other source state with a 409 so the documented state machine holds.","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs#L102-L138","documentation":"Ticket.Resolve enforces the domain rule that a Closed ticket cannot be resolved; the ticket must be reopened first. It throws CustomException with HTTP 409 Conflict. This is a guard against illegal status transitions on the Ticket aggregate.","triggerScenarios":"Calling Resolve (or the resolve endpoint) on a ticket whose Status is TicketStatus.Closed.","commonSituations":"Two agents acting concurrently — one closed the ticket while another was writing a resolution; client UI state stale (showed an open ticket that was actually closed); automated scripts replaying old actions against closed tickets.","solutions":["Reopen the ticket first (Reopen method/endpoint), then call Resolve.","Reload the ticket to get fresh status before resolving — the UI state may be stale.","Handle the 409 in the client and prompt the user to reopen the ticket.","Check Status via the ticket query endpoint before issuing Resolve."],"exampleFix":"// before\nticket.Resolve(resolutionNote); // 409 if closed\n// after\nif (ticket.Status == TicketStatus.Closed)\n{\n    ticket.Reopen();\n}\nticket.Resolve(resolutionNote);","handlingStrategy":"validation","validationCode":"if (ticket.status === TicketStatus.Closed) {\n  throw new Error('Reopen the ticket before resolving.');\n}\nticket.resolve(resolutionNote);","typeGuard":"bool CanResolve(Ticket t) => t.Status != TicketStatus.Closed;","tryCatchPattern":"try\n{\n    ticket.Resolve(resolutionNote);\n}\ncatch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict)\n{\n    // ticket was closed concurrently — reload and prompt to reopen\n    await repo.ReloadAsync(ticket.Id, ct);\n}","preventionTips":["Check ticket status immediately before resolving; reload from the server if the view is stale.","Disable Resolve controls in the UI for closed tickets.","Handle 409 responses by refreshing the ticket and informing the user.","Use optimistic-concurrency-aware updates to detect concurrent close/resolve races."],"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"}