fullstackhero/dotnet-starter-kit · error · CustomException

Only a resolved ticket can be closed — current status is

Error message

Only a resolved ticket can be closed — current status is {Status}. Resolve it first.

What it means

Ticket.Close enforces that only a Resolved ticket can be closed; closing from any other status throws CustomException with HTTP 409 Conflict and reports the current status. Closing is the terminal step of the Open → Resolved → Closed workflow.

Solutions

  1. Call Resolve(resolutionNote) first, then Close — the workflow requires Resolved before Closed.
  2. Reload the ticket and check Status before closing.
  3. Make client close actions conditional: only enable Close when status is Resolved.
  4. Handle the 409 in the client by refreshing the ticket and prompting for the Resolve step.

Example fix

// before
ticket.Close(); // 409 unless Resolved
// after
if (ticket.Status == TicketStatus.Open)
{
    ticket.Resolve("Auto-resolved before close");
}
ticket.Close();
Defensive patterns

Strategy: validation

Validate before calling

if (ticket.status !== TicketStatus.Resolved) {
  throw new Error(`Ticket must be Resolved before closing (current: ${ticket.status}).`);
}
ticket.close();

Type guard

bool CanClose(Ticket t) => t.Status == TicketStatus.Resolved;

Try / catch

try
{
    ticket.Close();
}
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
{
    ticket.Resolve("Resolved prior to close");
    ticket.Close();
}

Prevention

When it happens

Trigger: Calling Close on a ticket whose Status is Open (or any status other than Resolved/Closed). Closed tickets return silently (idempotent); everything else throws.

Common situations: Support agent closing a ticket without first marking it resolved; client skipping the Resolve step; stale UI showing Resolved while the ticket was reopened; batch scripts that close tickets without transitioning them through Resolved.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/03a48dd46b9beee2. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs:148

        ResolutionNote = string.IsNullOrWhiteSpace(resolutionNote) ? null : resolutionNote.Trim();
        ResolvedAtUtc = DateTime.UtcNow;
        UpdatedAtUtc = DateTime.UtcNow;
        TransitionStatus(TicketStatus.Resolved);
    }

    /// <summary>
    /// Finalizes a resolved ticket (Resolved → Closed). Idempotent when already Closed;
    /// rejects any other source state with a 409 so the documented state machine holds.
    /// </summary>
    public void Close()
    {
        if (Status == TicketStatus.Closed)
        {
            return;
        }
        if (Status != TicketStatus.Resolved)
        {
            throw new CustomException(
                $"Only a resolved ticket can be closed — current status is {Status}. Resolve it first.",
                (IEnumerable<string>?)null,
                HttpStatusCode.Conflict);
        }

        ClosedAtUtc = DateTime.UtcNow;
        UpdatedAtUtc = DateTime.UtcNow;
        TransitionStatus(TicketStatus.Closed);
    }

    /// <summary>
    /// Edits the mutable details of an open/in-progress/resolved ticket. A closed ticket is
    /// frozen — it must be reopened first.
    /// </summary>
    public void UpdateDetails(string title, string? description, TicketPriority priority)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(title);
        if (Status == TicketStatus.Closed)

View on GitHub (pinned to 3f2959e683)