fullstackhero/dotnet-starter-kit · error · NotFoundException
Plan not found for tenant .
Error message
Plan {planId} not found for tenant {tenantId}. What it means
CreateSubscriptionInvoiceAsync loads the plan by planId before computing term price. If no Plan row with that Id exists, NotFoundException("Plan {planId} not found for tenant {tenantId}.") is thrown and no subscription invoice is created.
Solutions
- Validate the planId against the Plans table before creating the subscription invoice.
- Re-create or restore the missing plan row, or switch to an existing plan's Id.
- Avoid hard-coding plan Ids across environments; resolve plans by stable code/slug instead of GUID.
- Ensure seed/migrations that insert plans ran in the target environment before provisioning tenants.
Example fix
// before
await billing.CreateSubscriptionInvoiceAsync(tenantId, Guid.Parse(config["PlanId"]), ...);
// after
var plan = await db.Plans.FirstOrDefaultAsync(p => p.Code == config["PlanCode"])
?? throw new NotFoundException($"Plan {config["PlanCode"]} not found.");
await billing.CreateSubscriptionInvoiceAsync(tenantId, plan.Id, ...); Defensive patterns
Strategy: validation
Validate before calling
var plan = await db.Plans.FirstOrDefaultAsync(p => p.Code == planCode)
?? throw new NotFoundException($"Plan {planCode} not found.");
// then use plan.Id Type guard
bool planExists(Guid id) => db.Plans.Any(p => p.Id == id); // await
Try / catch
try { await billing.CreateSubscriptionInvoiceAsync(tenantId, planId, start, end, ct); }
catch (NotFoundException ex) { log.Error("Missing plan {PlanId}", planId); } Prevention
- Resolve plans by stable code/slug, not hard-coded GUIDs.
- Run plan seed data before tenant provisioning in every environment.
- Validate planId against the DB before creating subscription invoices.
When it happens
Trigger: Creating a subscription invoice with a planId that doesn't exist in the Plans table — deleted plan, wrong GUID, Id from another environment, or plan seeded after the invoice job ran.
Common situations: Tenant provisioning passing a hard-coded/stale plan Id; plans re-seeded with new GUIDs while tenants reference old ones; environment clones diverging; admin tooling sending unsaved plan Ids.
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/b071d95dddc2c153.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Services/BillingService.cs:311
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
return await _db.Invoices
.FirstOrDefaultAsync(i => i.Id == invoiceId && (isRoot || i.TenantId == callerTenantId), cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Invoice {invoiceId} not found.");
}
public async Task<Invoice?> CreateSubscriptionInvoiceAsync(
string tenantId,
Guid planId,
DateTime periodStartUtc,
DateTime periodEndUtc,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
var plan = await _db.Plans.FirstOrDefaultAsync(p => p.Id == planId, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException($"Plan {planId} not found for tenant {tenantId}.");
var termPrice = plan.TermPrice;
if (termPrice.Amount <= 0m)
{
// Free / trial plan — validity is still set, but there is nothing to bill.
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("[Billing] plan {PlanKey} term price is zero for tenant {TenantId}, no subscription invoice", plan.Key, tenantId);
}
return null;
}
var periodStart = DateTime.SpecifyKind(periodStartUtc, DateTimeKind.Utc);
var periodEnd = DateTime.SpecifyKind(periodEndUtc, DateTimeKind.Utc);
var invoiceNumber = BuildSubscriptionInvoiceNumber(tenantId, periodStart);
// Idempotency: redelivery of the subscribe/renew event must not double-invoice the term.
var existing = await _db.InvoicesView on GitHub (pinned to 3f2959e683)