fullstackhero/dotnet-starter-kit · error · NotFoundException
Active plan with key
Error message
Active plan with key '{query.PlanKey}' not found. What it means
GetPlanTermQueryHandler normalizes the requested plan key to lowercase and looks up an ACTIVE plan (Key == key && IsActive) in BillingDbContext. If no active plan has that key it throws NotFoundException("Active plan with key '{key}' not found.") — this covers both a nonexistent key and a plan that exists but is deactivated.
Solutions
- List active plans (GET /plans or the Plans table: SELECT Key FROM Plans WHERE IsActive) and use an exact key from that list.
- Send the key in lowercase canonical slug form; the handler lowercases input but a key stored with different casing will never match.
- Seed the plan in the target environment via the DbMigrator (dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed).
- Reactivate the plan (IsActive = true) if it was disabled intentionally but terms are still being requested.
Example fix
// before
await apiFetch(`/plans/${'Pro'}/term`);
// after: canonical key from the plans list
const plans = await apiFetch('/plans');
const key = plans.find(p => p.name === 'Pro').key; // 'pro'
await apiFetch(`/plans/${key}/term`); Defensive patterns
Strategy: validation
Validate before calling
const plans = await apiFetch('/plans');
const key = String(planKey).trim().toLowerCase();
if (!plans.some(p => p.key === key && p.isActive)) throw new Error(`Plan '${key}' is not an active plan`); Type guard
function isActivePlan(p) { return typeof p?.key === 'string' && p.isActive === true; } Try / catch
try { return await apiFetch(`/plans/${key}/term`); }
catch (e) { if (isNotFound(e)) { show('Plan unavailable'); return null; } throw e; } Prevention
- Always take plan keys from the plans list response, never from marketing copy.
- Run DbMigrator with --seed in every new environment so plan rows exist.
- Keep keys canonical lowercase slugs and validate deactivations against active signup links.
When it happens
Trigger: GET /plans/{planKey}/term with a key that is not a canonical lowercase slug (e.g. 'Pro', 'pro-annual ' with whitespace), a retired/renamed plan key, or a plan that has IsActive == false.
Common situations: Frontend hardcoding a plan key that was renamed during a pricing overhaul; environment drift (plan seeded in staging but not prod — run DbMigrator with --seed); trailing spaces or camel-case keys from marketing copy; requesting annual term for a plan that only defines monthly.
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/300cb478ac8b1343.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Plans/GetPlanTerm/GetPlanTermQueryHandler.cs:21
using FSH.Modules.Billing.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Plans.GetPlanTerm;
public sealed class GetPlanTermQueryHandler(BillingDbContext dbContext)
: IQueryHandler<GetPlanTermQuery, PlanTermResponse>
{
public async ValueTask<PlanTermResponse> Handle(GetPlanTermQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
#pragma warning disable CA1308 // Plan keys are canonical lowercase slugs
var key = query.PlanKey.ToLowerInvariant();
#pragma warning restore CA1308
var plan = await dbContext.Plans.AsNoTracking()
.FirstOrDefaultAsync(p => p.Key == key && p.IsActive, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException($"Active plan with key '{query.PlanKey}' not found.");
return new PlanTermResponse(
plan.Id,
plan.Key,
plan.Name,
plan.Interval,
plan.TermMonths,
plan.TermPrice.Amount,
plan.Currency);
}
}
View on GitHub (pinned to 3f2959e683)