{"record":{"id":"e9b25c3526831fcd","repo":"fullstackhero/dotnet-starter-kit","slug":"top-up-request-must-be-expected-was-status","errorCode":null,"errorMessage":"Top-up request must be {expected} (was {Status}).","messagePattern":"Top-up request must be (.+?) \\(was (.+?)\\)\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Domain/TopupRequest.cs","lineNumber":64,"sourceCode":"    public void MarkCompleted()\n    {\n        Require(TopupRequestStatus.Invoiced);\n        Status = TopupRequestStatus.Completed;\n        CompletedAtUtc = DateTime.UtcNow;\n    }\n\n    public void Reject(string? reason)\n    {\n        Require(TopupRequestStatus.Pending);\n        DecisionNote = reason;\n        Status = TopupRequestStatus.Rejected;\n        DecidedAtUtc = DateTime.UtcNow;\n    }\n\n    private void Require(TopupRequestStatus expected)\n    {\n        if (Status != expected)\n            throw new InvalidOperationException($\"Top-up request must be {expected} (was {Status}).\");\n    }\n}\n","sourceCodeStart":46,"sourceCodeEnd":67,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Domain/TopupRequest.cs#L46-L67","documentation":"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.","triggerScenarios":"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.","commonSituations":"Approval UI double-clicks; retrying a failed job after the first attempt already advanced the status; out-of-order processing of approve/reject events.","solutions":["Check the request's current Status before invoking the transition method.","Make the calling handler idempotent: swallow the transition when already in the target state.","Reject competing transitions early (e.g. cancel the reject path once invoiced).","Persist status changes in a single transaction to avoid concurrent transitions."],"exampleFix":"// before\nrequest.MarkCompleted(); // throws if not Invoiced\n\n// after\nif (request.Status == TopupRequestStatus.Invoiced)\n{\n    request.MarkCompleted();\n}","handlingStrategy":"validation","validationCode":"if (request.Status != TopupRequestStatus.Pending) return; // already processed","typeGuard":null,"tryCatchPattern":"try { topup.MarkInvoiced(invoiceRef); } catch (InvalidOperationException ex) { logger.LogInformation(ex, \"Top-up already transitioned\"); }","preventionTips":["Guard approval UIs against double-clicks","Check current Status before each transition","Process approve/reject events with idempotency keys"],"tags":["domain","state-machine","billing","topup"],"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"}