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

  1. Restore or re-create the missing Plan row with the referenced Id (or point the subscription at an existing plan).
  2. Never hard-delete plans — deactivate them so subscriptions remain valid.
  3. Add a data-integrity check/query for subscriptions whose PlanId has no matching Plan and fix orphans.
  4. 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

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)