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

  1. Refresh the top-up request status and only reject while it shows Pending.
  2. Make the reject action idempotent on the client: on 409, re-fetch and reflect the actual state instead of retrying blindly.
  3. Serialize workflows so one operator/process owns a pending request, or use a conditional update (WHERE Status == Pending).
  4. 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

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


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)