{"record":{"id":"944db1db83564ecb","repo":"fullstackhero/dotnet-starter-kit","slug":"invoice-query-invoiceid-not-found","errorCode":null,"errorMessage":"Invoice {query.InvoiceId} not found.","messagePattern":"Invoice (.+?) not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoiceById/GetInvoiceByIdQueryHandler.cs","lineNumber":33,"sourceCode":"    : IQueryHandler<GetInvoiceByIdQuery, InvoiceDto>\n{\n    public async ValueTask<InvoiceDto> Handle(GetInvoiceByIdQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n\n        // BillingDbContext isn't tenant-filtered (raw DbContext for cross-tenant admin visibility): root\n        // reads any invoice by id; a tenant caller is pinned to its own so it can't read another's. Mirrors GetSubscriptionQueryHandler.\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n\n        var invoice = await dbContext.Invoices.AsNoTracking()\n            .Include(i => i.LineItems)\n            .FirstOrDefaultAsync(\n                i => i.Id == query.InvoiceId && (isRoot || i.TenantId == callerTenantId),\n                cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Invoice {query.InvoiceId} not found.\");\n\n        return invoice.ToDto();\n    }\n}\n","sourceCodeStart":15,"sourceCodeEnd":38,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GetInvoiceById/GetInvoiceByIdQueryHandler.cs#L15-L38","documentation":"GetInvoiceByIdQueryHandler looks up the invoice by id and, when the caller is not root, adds a TenantId == callerTenantId filter to the otherwise unfiltered BillingDbContext query. If no invoice matches (wrong id, or the invoice belongs to another tenant), it throws NotFoundException(\"Invoice {id} not found.\"). Notably the same 404 is returned for cross-tenant ids so existence is never leaked.","triggerScenarios":"GET /invoices/{id} with an invoice id that does not exist; a tenant caller passing another tenant's invoice id (filtered out by TenantId predicate); a deleted or soft-removed invoice id; root passing a typo'd GUID.","commonSituations":"Frontend caching an invoice id from a different environment (staging vs prod database); copying an invoice id from a support ticket issued for another tenant; referencing an invoice before the assign/creation transaction committed; case/format-mangled id from a URL.","solutions":["Verify the invoice id exists and matches the intended tenant by querying the invoices table (SELECT * FROM Invoices WHERE Id = '<id>').","If you are a tenant caller, confirm the invoice actually belongs to your tenant; cross-tenant reads require the root identity.","Re-check the id source: re-list invoices via GET /invoices and use an id from that response rather than a hand-copied value.","If root needs cross-tenant visibility, confirm the caller token is root (MultitenancyConstants.Root.Id) — non-root callers are pinned to their own TenantId."],"exampleFix":"// before: stale/hand-copied id\nawait apiFetch(`/invoices/${invoiceIdFromTicket}`);\n\n// after: resolve a fresh id from the tenant-scoped list\nconst { items } = await apiFetch('/invoices');\nconst target = items.find(i => i.invoiceNumber === 'INV-0042');\nawait apiFetch(`/invoices/${target.id}`);","handlingStrategy":"try-catch","validationCode":"const res = await apiFetch('/invoices');\nconst exists = res.items.some(i => i.id === invoiceId);\nif (!exists) throw new Error(`Invoice ${invoiceId} not visible to this tenant`);","typeGuard":"function isNotFound(e) { return e?.status === 404 || /not found/i.test(e?.message ?? ''); }","tryCatchPattern":"try { return await apiFetch(`/invoices/${id}`); }\ncatch (e) { if (isNotFound(e)) { return null; } throw e; }","preventionTips":["Derive invoice ids from the tenant-scoped list response, never from hand-copied values or tickets.","Remember cross-tenant ids return 404 by design — use a root token for cross-tenant lookups.","Tag ids with their environment when copying between staging and prod to avoid mismatches.","Handle 404 in the UI with a 'not found or not accessible' message rather than retrying."],"tags":["billing","not-found","multitenancy"],"backgroundTag":"resource-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"}