{"record":{"id":"532735b47604c1d5","repo":"fullstackhero/dotnet-starter-kit","slug":"plan-subscription-planid-not-found-for-tenant-tenantid","errorCode":null,"errorMessage":"Plan {subscription.PlanId} not found for tenant {tenantId}.","messagePattern":"Plan (.+?) not found for tenant (.+?)\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Services/BillingService.cs","lineNumber":78,"sourceCode":"            if (_logger.IsEnabled(LogLevel.Information))\n            {\n                _logger.LogInformation(\"[Billing] usage invoice already exists for tenant {TenantId} period {Year}-{Month:00}, skipping\",\n                    tenantId, periodYear, periodMonth);\n            }\n            return existing;\n        }\n\n        var subscription = await _db.Subscriptions\n            .FirstOrDefaultAsync(s => s.TenantId == tenantId && s.Status == SubscriptionStatus.Active, cancellationToken)\n            .ConfigureAwait(false);\n        if (subscription is null)\n        {\n            _logger.LogWarning(\"[Billing] no active subscription for tenant {TenantId}, skipping invoice\", tenantId);\n            return null;\n        }\n\n        var plan = await _db.Plans.FirstOrDefaultAsync(p => p.Id == subscription.PlanId, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Plan {subscription.PlanId} not found for tenant {tenantId}.\");\n\n        var snapshots = await _usageReporter.CaptureForPeriodAsync(tenantId, periodYear, periodMonth, cancellationToken).ConfigureAwait(false);\n\n        // Usage invoices bill metered overage only. The plan's base fee is billed by the\n        // subscription invoice on tenant create/renew (see CreateSubscriptionInvoiceAsync), so it is\n        // intentionally NOT added here — otherwise monthly plans would be double-billed.\n        var invoiceNumber = BuildUsageInvoiceNumber(tenantId, periodYear, periodMonth);\n        var invoice = Invoice.CreateDraft(tenantId, invoiceNumber, periodYear, periodMonth, plan.Currency,\n            InvoicePurpose.Usage, periodStartUtc: null, periodEndUtc: null);\n\n        foreach (var snap in snapshots)\n        {\n            if (snap.Overage <= 0)\n            {\n                continue;\n            }\n            var rate = plan.GetOverageRate(snap.Resource);\n            if (rate <= 0)","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Services/BillingService.cs#L60-L96","documentation":"During monthly invoice generation (GenerateInvoiceForPeriodAsync), the service loads the plan referenced by the active subscription's PlanId. If the plan row is missing while a subscription still points to it, NotFoundException(\"Plan {planId} not found for tenant {tenantId}.\") is thrown, aborting invoice generation for that tenant.","triggerScenarios":"A subscription references a PlanId that no longer exists in the Plans table — plan hard-deleted instead of deactivated, database restored partially, seed data removed, or subscriptions copied across environments.","commonSituations":"Admin deleted a plan directly in SQL while tenants still subscribed; migrations/seed re-run dropping plan rows; environment clone where subscription rows carried over but plans did not.","solutions":["Restore or re-create the missing Plan row with the referenced Id (or point the subscription at an existing plan).","Never hard-delete plans — deactivate them so subscriptions remain valid.","Add a data-integrity check/query for subscriptions whose PlanId has no matching Plan and fix orphans.","Verify the same database/environment is being used for subscriptions and plans (no cross-environment drift)."],"exampleFix":"// before\nawait db.Plans.Where(p => p.Id == id).ExecuteDeleteAsync();\n// after\nvar plan = await db.Plans.FindAsync(id);\nplan.Deactivate(); // keep row, mark inactive\nawait db.SaveChangesAsync();","handlingStrategy":"validation","validationCode":"var planExists = await db.Plans.AnyAsync(p => p.Id == subscription.PlanId);\nif (!planExists) throw new NotFoundException($\"Plan {subscription.PlanId} missing; fix orphaned subscription.\");","typeGuard":null,"tryCatchPattern":"try { await billing.GenerateInvoiceForPeriodAsync(tenantId, y, m); }\ncatch (NotFoundException ex) { log.Error(\"orphaned subscription for {Tenant}\", tenantId); /* skip tenant, alert */ }","preventionTips":["Deactivate plans instead of deleting them.","Run periodic referential-integrity checks for orphaned subscriptions.","Keep plan seed data consistent across environments."],"tags":["billing","referential-integrity","not-found"],"backgroundTag":"entity-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"}