{"record":{"id":"fe6fe5718892472d","repo":"fullstackhero/dotnet-starter-kit","slug":"top-up-request-topuprequestid-not-found-or-not-pending","errorCode":null,"errorMessage":"Top-up request {topupRequestId} not found or not pending.","messagePattern":"Top-up request (.+?) not found or not pending\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Services/BillingService.cs","lineNumber":191,"sourceCode":"            .FirstOrDefaultAsync(w => w.TenantId == tenantId, cancellationToken)\n            .ConfigureAwait(false);\n        if (wallet is null)\n        {\n            wallet = Wallet.Create(tenantId, currency);\n            _db.Wallets.Add(wallet);\n            await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        }\n        return wallet;\n    }\n\n    public async Task<Invoice> CreateTopupInvoiceAsync(string tenantId, Guid topupRequestId, CancellationToken cancellationToken = default)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);\n\n        var request = await _db.TopupRequests\n            .FirstOrDefaultAsync(r => r.Id == topupRequestId && r.TenantId == tenantId && r.Status == TopupRequestStatus.Pending, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Top-up request {topupRequestId} not found or not pending.\");\n\n        var now = _timeProvider.GetUtcNow().UtcDateTime;\n        var invoiceNumber = BuildTopupInvoiceNumber(tenantId, now, topupRequestId);\n\n        var invoice = Invoice.CreateTopupDraft(\n            tenantId,\n            invoiceNumber,\n            now.Year,\n            now.Month,\n            request.Amount.Currency,\n            request.Amount.Amount,\n            $\"WhatsApp wallet top-up ({request.Amount.Amount:0.##} {request.Amount.Currency})\");\n\n        invoice.Issue();\n        _db.Invoices.Add(invoice);\n        request.MarkInvoiced(invoice.Id, request.Note);\n\n        await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Services/BillingService.cs#L173-L209","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait billing.CreateTopupInvoiceAsync(tenantId, requestId, ct); // throws if not pending\n// after\nvar req = await db.TopupRequests.FirstOrDefaultAsync(r => r.Id == requestId);\nif (req is { Status: TopupRequestStatus.Pending, TenantId: var t } && t == tenantId)\n    await billing.CreateTopupInvoiceAsync(tenantId, requestId, ct);","handlingStrategy":"validation","validationCode":"var ok = await db.TopupRequests.AnyAsync(r => r.Id == requestId && r.TenantId == tenantId && r.Status == TopupRequestStatus.Pending);\nif (!ok) return; // already invoiced, wrong tenant, or bad id","typeGuard":"bool isInvoicable(TopupRequest r, string tenantId) => r is { Status: TopupRequestStatus.Pending } && r.TenantId == tenantId;","tryCatchPattern":"try { await billing.CreateTopupInvoiceAsync(tenantId, requestId, ct); }\ncatch (NotFoundException) { /* check if invoice already exists → treat as success */ }","preventionTips":["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."],"tags":["billing","not-found","state-machine"],"backgroundTag":"record-not-found","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"}