{"record":{"id":"487b13d422dcd9f2","repo":"fullstackhero/dotnet-starter-kit","slug":"invoice-invoiceid-not-found","errorCode":null,"errorMessage":"Invoice {invoiceId} not found.","messagePattern":"Invoice (.+?) not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Services/BillingService.cs","lineNumber":298,"sourceCode":"    public async Task VoidInvoiceAsync(Guid invoiceId, string? reason, CancellationToken cancellationToken = default)\n    {\n        var invoice = await LoadInvoiceAsync(invoiceId, cancellationToken).ConfigureAwait(false);\n        invoice.Void(reason);\n        await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n    }\n\n    // Issue/MarkPaid/Void load here. BillingDbContext isn't tenant-filtered, so scope to caller: root\n    // mutates any invoice; a tenant caller is pinned to its own (cross-tenant id → 404, can't mutate).\n    private async Task<Invoice> LoadInvoiceAsync(Guid invoiceId, CancellationToken cancellationToken)\n    {\n        var callerTenantId = _tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n\n        return await _db.Invoices\n            .FirstOrDefaultAsync(i => i.Id == invoiceId && (isRoot || i.TenantId == callerTenantId), cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Invoice {invoiceId} not found.\");\n    }\n\n    public async Task<Invoice?> CreateSubscriptionInvoiceAsync(\n        string tenantId,\n        Guid planId,\n        DateTime periodStartUtc,\n        DateTime periodEndUtc,\n        CancellationToken cancellationToken = default)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);\n\n        var plan = await _db.Plans.FirstOrDefaultAsync(p => p.Id == planId, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Plan {planId} not found for tenant {tenantId}.\");\n\n        var termPrice = plan.TermPrice;\n        if (termPrice.Amount <= 0m)\n        {\n            // Free / trial plan — validity is still set, but there is nothing to bill.","sourceCodeStart":280,"sourceCodeEnd":316,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Services/BillingService.cs#L280-L316","documentation":"LoadInvoiceAsync queries Invoices with (isRoot || i.TenantId == callerTenantId) and Id match. If no invoice satisfies both, NotFoundException(\"Invoice {id} not found.\") is thrown — including the deliberate case where a tenant asks for another tenant's invoice (returned as 404 to avoid leaking existence).","triggerScenarios":"Issue/MarkPaid/Void called with an invoiceId that doesn't exist, was deleted, belongs to another tenant (as non-root caller), or the caller is not root and the Id came from a different environment.","commonSituations":"Webhook referencing an invoice from a different environment (staging vs prod); tenant user reusing a root-visible invoice Id; stale UI after invoice void/deletion; connection string drift.","solutions":["Verify the invoiceId exists and belongs to the caller's tenant (query as root to confirm).","As a tenant, only operate on invoice Ids returned from your own tenant's invoice list.","Check environment/connection-string consistency between the caller and the database.","Treat the 404 for cross-tenant Ids as expected security behavior, not a bug."],"exampleFix":"// before\nawait billing.VoidInvoiceAsync(otherTenantInvoiceId, ...); // 404\n// after\nvar inv = await db.Invoices.FirstOrDefaultAsync(i => i.Id == id && i.TenantId == callerTenantId);\nif (inv is null) throw new NotFoundException($\"Invoice {id} not found.\");","handlingStrategy":"try-catch","validationCode":"var exists = await db.Invoices.AnyAsync(i => i.Id == id && (isRoot || i.TenantId == callerTenantId));\nif (!exists) return NotFound($\"Invoice {id} not found.\");","typeGuard":null,"tryCatchPattern":"try { await billing.VoidInvoiceAsync(id, ct); }\ncatch (NotFoundException) { /* 404 — wrong id, deleted, or cross-tenant */ }","preventionTips":["Only use invoice Ids returned by your own tenant's list endpoints.","Confirm environment/connection-string consistency before webhooks.","Expect 404 (not 403) for cross-tenant invoice Ids; don't treat it as a leak."],"tags":["not-found","multitenancy","billing"],"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"}