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

  1. Check the request's current Status and TenantId first; only call CreateTopupInvoiceAsync for Pending requests of the matching tenant.
  2. Make the caller idempotent: on this 404, check whether an invoice for the top-up already exists and treat that as success.
  3. Verify the tenantId parameter matches the request's TenantId exactly.
  4. 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

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


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)