fullstackhero/dotnet-starter-kit · error · CustomException

A closed ticket cannot be resolved — reopen it first.

Error message

A closed ticket cannot be resolved — reopen it first.

What it means

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.

Solutions

  1. Reopen the ticket first (Reopen method/endpoint), then call Resolve.
  2. Reload the ticket to get fresh status before resolving — the UI state may be stale.
  3. Handle the 409 in the client and prompt the user to reopen the ticket.
  4. Check Status via the ticket query endpoint before issuing Resolve.

Example fix

// before
ticket.Resolve(resolutionNote); // 409 if closed
// after
if (ticket.Status == TicketStatus.Closed)
{
    ticket.Reopen();
}
ticket.Resolve(resolutionNote);
Defensive patterns

Strategy: validation

Validate before calling

if (ticket.status === TicketStatus.Closed) {
  throw new Error('Reopen the ticket before resolving.');
}
ticket.resolve(resolutionNote);

Type guard

bool CanResolve(Ticket t) => t.Status != TicketStatus.Closed;

Try / catch

try
{
    ticket.Resolve(resolutionNote);
}
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
{
    // ticket was closed concurrently — reload and prompt to reopen
    await repo.ReloadAsync(ticket.Id, ct);
}

Prevention

When it happens

Trigger: Calling Resolve (or the resolve endpoint) on a ticket whose Status is TicketStatus.Closed.

Common situations: 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.

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/e3844263f67aaa39. Report an issue: GitHub.

Appendix: source

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

        // ticket sends it back to Open since no owner is pushing it forward.
        if (assigneeUserId is not null && Status == TicketStatus.Open)
        {
            TransitionStatus(TicketStatus.InProgress);
        }
        else if (assigneeUserId is null && Status == TicketStatus.InProgress)
        {
            TransitionStatus(TicketStatus.Open);
        }

        AddDomainEvent(DomainEvent.Create<TicketAssignedDomainEvent>(
            (id, ts) => new TicketAssignedDomainEvent(Id, previous, assigneeUserId, id, ts)));
    }

    public void Resolve(string? resolutionNote)
    {
        if (Status == TicketStatus.Closed)
        {
            throw new CustomException(
                "A closed ticket cannot be resolved — reopen it first.",
                (IEnumerable<string>?)null,
                HttpStatusCode.Conflict);
        }
        if (Status == TicketStatus.Resolved)
        {
            return;
        }

        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.

View on GitHub (pinned to 3f2959e683)