fullstackhero/dotnet-starter-kit · warning · CustomException
Top-up request cannot be rejected because it is (only…
Error message
Top-up request {command.Id} cannot be rejected because it is {request.Status} (only Pending requests can be rejected). What it means
Only top-up requests in TopupRequestStatus.Pending can be rejected. When request.Status is anything else (Approved, Rejected, etc.), the handler throws CustomException with HTTP 409 Conflict explaining the current state. This is an optimistic-concurrency/state-machine guard.
Solutions
- Refresh the top-up request status and only reject while it shows Pending.
- Make the reject action idempotent on the client: on 409, re-fetch and reflect the actual state instead of retrying blindly.
- Serialize workflows so one operator/process owns a pending request, or use a conditional update (WHERE Status == Pending).
- Surface the exception message's current status in the UI so users understand why rejection failed.
Example fix
// before
await mediator.Send(new RejectTopupRequestCommand(id, reason)); // may 409
// after
var req = await db.TopupRequests.FindAsync(id);
if (req?.Status != TopupRequestStatus.Pending) { /* refresh UI, skip reject */ }
else await mediator.Send(new RejectTopupRequestCommand(id, reason)); Defensive patterns
Strategy: try-catch
Validate before calling
var status = await db.TopupRequests.Where(r => r.Id == id).Select(r => (TopupRequestStatus?)r.Status).SingleOrDefaultAsync();
if (status != TopupRequestStatus.Pending) return Conflict($"Request is {status}, not Pending."); Type guard
bool isPending(TopupRequest r) => r.Status == TopupRequestStatus.Pending;
Try / catch
try { await mediator.Send(cmd); }
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { /* re-fetch and show current state */ } Prevention
- Disable the reject button unless the request shows Pending in fresh data.
- Make reject actions idempotent on the client; on 409, re-fetch.
- Serialize workflows so a single actor owns a pending request.
When it happens
Trigger: Rejecting an already-approved or already-rejected top-up request — e.g. two admins acting concurrently, a retry after a successful rejection, or rejecting after the invoice was generated and marked paid.
Common situations: Double-click / duplicate submission of the reject action; another operator approved the request moments earlier; a webhook or billing job already transitioned the request; stale page data.
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
- Cannot mark invoice as paid from status
- Paid invoices cannot be voided.
- Operation requires invoice status
- Top-up request must be
- Top-up request not found or not pending.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/b6d2270fbd58d35c.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/RejectTopupRequest/RejectTopupRequestCommandHandler.cs:38
ArgumentNullException.ThrowIfNull(command);
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
var request = await db.TopupRequests
.FirstOrDefaultAsync(r => r.Id == command.Id, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Top-up request {command.Id} not found.");
if (!isRoot && request.TenantId != callerTenantId)
{
throw new UnauthorizedException("You can only reject top-up requests for your own tenant.");
}
if (request.Status != TopupRequestStatus.Pending)
{
throw new CustomException(
$"Top-up request {command.Id} cannot be rejected because it is {request.Status} (only Pending requests can be rejected).",
(IEnumerable<string>?)null,
HttpStatusCode.Conflict);
}
request.Reject(command.Reason);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return request.Id;
}
}
View on GitHub (pinned to 3f2959e683)