fullstackhero/dotnet-starter-kit · error · NotFoundException
Plan not found for tenant .
Error message
Plan {subscription.PlanId} not found for tenant {tenantId}. What it means
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.
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).
Example fix
// before await db.Plans.Where(p => p.Id == id).ExecuteDeleteAsync(); // after var plan = await db.Plans.FindAsync(id); plan.Deactivate(); // keep row, mark inactive await db.SaveChangesAsync();
Defensive patterns
Strategy: validation
Validate before calling
var planExists = await db.Plans.AnyAsync(p => p.Id == subscription.PlanId);
if (!planExists) throw new NotFoundException($"Plan {subscription.PlanId} missing; fix orphaned subscription."); Try / catch
try { await billing.GenerateInvoiceForPeriodAsync(tenantId, y, m); }
catch (NotFoundException ex) { log.Error("orphaned subscription for {Tenant}", tenantId); /* skip tenant, alert */ } Prevention
- Deactivate plans instead of deleting them.
- Run periodic referential-integrity checks for orphaned subscriptions.
- Keep plan seed data consistent across environments.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/532735b47604c1d5.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Services/BillingService.cs:78
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("[Billing] usage invoice already exists for tenant {TenantId} period {Year}-{Month:00}, skipping",
tenantId, periodYear, periodMonth);
}
return existing;
}
var subscription = await _db.Subscriptions
.FirstOrDefaultAsync(s => s.TenantId == tenantId && s.Status == SubscriptionStatus.Active, cancellationToken)
.ConfigureAwait(false);
if (subscription is null)
{
_logger.LogWarning("[Billing] no active subscription for tenant {TenantId}, skipping invoice", tenantId);
return null;
}
var plan = await _db.Plans.FirstOrDefaultAsync(p => p.Id == subscription.PlanId, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException($"Plan {subscription.PlanId} not found for tenant {tenantId}.");
var snapshots = await _usageReporter.CaptureForPeriodAsync(tenantId, periodYear, periodMonth, cancellationToken).ConfigureAwait(false);
// Usage invoices bill metered overage only. The plan's base fee is billed by the
// subscription invoice on tenant create/renew (see CreateSubscriptionInvoiceAsync), so it is
// intentionally NOT added here — otherwise monthly plans would be double-billed.
var invoiceNumber = BuildUsageInvoiceNumber(tenantId, periodYear, periodMonth);
var invoice = Invoice.CreateDraft(tenantId, invoiceNumber, periodYear, periodMonth, plan.Currency,
InvoicePurpose.Usage, periodStartUtc: null, periodEndUtc: null);
foreach (var snap in snapshots)
{
if (snap.Overage <= 0)
{
continue;
}
var rate = plan.GetOverageRate(snap.Resource);
if (rate <= 0)View on GitHub (pinned to 3f2959e683)