fullstackhero/dotnet-starter-kit · error · InvalidOperationException

Top-up request must be

Error message

Top-up request must be {expected} (was {Status}).

What it means

TopupRequest.Require is the private state guard called by MarkInvoiced, MarkCompleted, and Reject: the wallet top-up request must currently be in the exact expected status (e.g. Pending) for the transition to be legal, otherwise InvalidOperationException is thrown.

Solutions

  1. Check the request's current Status before invoking the transition method.
  2. Make the calling handler idempotent: swallow the transition when already in the target state.
  3. Reject competing transitions early (e.g. cancel the reject path once invoiced).
  4. Persist status changes in a single transaction to avoid concurrent transitions.

Example fix

// before
request.MarkCompleted(); // throws if not Invoiced

// after
if (request.Status == TopupRequestStatus.Invoiced)
{
    request.MarkCompleted();
}
Defensive patterns

Strategy: validation

Validate before calling

if (request.Status != TopupRequestStatus.Pending) return; // already processed

Try / catch

try { topup.MarkInvoiced(invoiceRef); } catch (InvalidOperationException ex) { logger.LogInformation(ex, "Top-up already transitioned"); }

Prevention

When it happens

Trigger: Calling MarkInvoiced on a request already invoiced/completed; MarkCompleted on a pending-but-uninvoiced request; Reject on a completed request; duplicate webhook/event handling driving the same transition twice.

Common situations: Approval UI double-clicks; retrying a failed job after the first attempt already advanced the status; out-of-order processing of approve/reject events.

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

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Domain/TopupRequest.cs:64

    public void MarkCompleted()
    {
        Require(TopupRequestStatus.Invoiced);
        Status = TopupRequestStatus.Completed;
        CompletedAtUtc = DateTime.UtcNow;
    }

    public void Reject(string? reason)
    {
        Require(TopupRequestStatus.Pending);
        DecisionNote = reason;
        Status = TopupRequestStatus.Rejected;
        DecidedAtUtc = DateTime.UtcNow;
    }

    private void Require(TopupRequestStatus expected)
    {
        if (Status != expected)
            throw new InvalidOperationException($"Top-up request must be {expected} (was {Status}).");
    }
}

View on GitHub (pinned to 3f2959e683)