fullstackhero/dotnet-starter-kit · error · NotFoundException
Top-up request not found or not pending.
Error message
Top-up request {topupRequestId} not found or not pending. What it means
CreateTopupInvoiceAsync loads a top-up request matching Id AND tenantId AND Status == Pending in one query. If no such row exists (wrong tenant, wrong Id, or non-pending status), NotFoundException("Top-up request {id} not found or not pending.") is thrown before an invoice is created.
Solutions
- Check the request's current Status and TenantId first; only call CreateTopupInvoiceAsync for Pending requests of the matching tenant.
- Make the caller idempotent: on this 404, check whether an invoice for the top-up already exists and treat that as success.
- Verify the tenantId parameter matches the request's TenantId exactly.
- Resolve concurrency by transitioning status in a single conditional update (WHERE Status == Pending) before invoicing.
Example fix
// before
await billing.CreateTopupInvoiceAsync(tenantId, requestId, ct); // throws if not pending
// after
var req = await db.TopupRequests.FirstOrDefaultAsync(r => r.Id == requestId);
if (req is { Status: TopupRequestStatus.Pending, TenantId: var t } && t == tenantId)
await billing.CreateTopupInvoiceAsync(tenantId, requestId, ct); Defensive patterns
Strategy: validation
Validate before calling
var ok = await db.TopupRequests.AnyAsync(r => r.Id == requestId && r.TenantId == tenantId && r.Status == TopupRequestStatus.Pending); if (!ok) return; // already invoiced, wrong tenant, or bad id
Type guard
bool isInvoicable(TopupRequest r, string tenantId) => r is { Status: TopupRequestStatus.Pending } && r.TenantId == tenantId; Try / catch
try { await billing.CreateTopupInvoiceAsync(tenantId, requestId, ct); }
catch (NotFoundException) { /* check if invoice already exists → treat as success */ } Prevention
- Only invoice Pending requests of the exact tenant.
- Design payment webhooks idempotently: check for an existing top-up invoice first.
- Transition status with a conditional update to avoid races.
When it happens
Trigger: Marking a top-up as paid / creating its invoice for a request that was already invoiced (status moved past Pending), belongs to a different tenant, or the Id is wrong.
Common situations: Payment webhook retried after the invoice already exists; calling with root-tenant request Id while scoped to a tenant; approving the request concurrently so status is no longer Pending; copy-pasted Id from another environment.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- Cannot mark invoice as paid from status
- Paid invoices cannot be voided.
- Operation requires invoice status
- Top-up request must be
- Invoice not found.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/fe6fe5718892472d.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Services/BillingService.cs:191
.FirstOrDefaultAsync(w => w.TenantId == tenantId, cancellationToken)
.ConfigureAwait(false);
if (wallet is null)
{
wallet = Wallet.Create(tenantId, currency);
_db.Wallets.Add(wallet);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
return wallet;
}
public async Task<Invoice> CreateTopupInvoiceAsync(string tenantId, Guid topupRequestId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
var request = await _db.TopupRequests
.FirstOrDefaultAsync(r => r.Id == topupRequestId && r.TenantId == tenantId && r.Status == TopupRequestStatus.Pending, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Top-up request {topupRequestId} not found or not pending.");
var now = _timeProvider.GetUtcNow().UtcDateTime;
var invoiceNumber = BuildTopupInvoiceNumber(tenantId, now, topupRequestId);
var invoice = Invoice.CreateTopupDraft(
tenantId,
invoiceNumber,
now.Year,
now.Month,
request.Amount.Currency,
request.Amount.Amount,
$"WhatsApp wallet top-up ({request.Amount.Amount:0.##} {request.Amount.Currency})");
invoice.Issue();
_db.Invoices.Add(invoice);
request.MarkInvoiced(invoice.Id, request.Note);
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);View on GitHub (pinned to 3f2959e683)