{"record":{"id":"300cb478ac8b1343","repo":"fullstackhero/dotnet-starter-kit","slug":"active-plan-with-key-query-plankey-not-found","errorCode":null,"errorMessage":"Active plan with key '{query.PlanKey}' not found.","messagePattern":"Active plan with key '(.+?)' not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Plans/GetPlanTerm/GetPlanTermQueryHandler.cs","lineNumber":21,"sourceCode":"using FSH.Modules.Billing.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Billing.Features.v1.Plans.GetPlanTerm;\n\npublic sealed class GetPlanTermQueryHandler(BillingDbContext dbContext)\n    : IQueryHandler<GetPlanTermQuery, PlanTermResponse>\n{\n    public async ValueTask<PlanTermResponse> Handle(GetPlanTermQuery query, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(query);\n\n#pragma warning disable CA1308 // Plan keys are canonical lowercase slugs\n        var key = query.PlanKey.ToLowerInvariant();\n#pragma warning restore CA1308\n        var plan = await dbContext.Plans.AsNoTracking()\n            .FirstOrDefaultAsync(p => p.Key == key && p.IsActive, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Active plan with key '{query.PlanKey}' not found.\");\n\n        return new PlanTermResponse(\n            plan.Id,\n            plan.Key,\n            plan.Name,\n            plan.Interval,\n            plan.TermMonths,\n            plan.TermPrice.Amount,\n            plan.Currency);\n    }\n}\n","sourceCodeStart":3,"sourceCodeEnd":33,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Plans/GetPlanTerm/GetPlanTermQueryHandler.cs#L3-L33","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait apiFetch(`/plans/${'Pro'}/term`);\n\n// after: canonical key from the plans list\nconst plans = await apiFetch('/plans');\nconst key = plans.find(p => p.name === 'Pro').key; // 'pro'\nawait apiFetch(`/plans/${key}/term`);","handlingStrategy":"validation","validationCode":"const plans = await apiFetch('/plans');\nconst key = String(planKey).trim().toLowerCase();\nif (!plans.some(p => p.key === key && p.isActive)) throw new Error(`Plan '${key}' is not an active plan`);","typeGuard":"function isActivePlan(p) { return typeof p?.key === 'string' && p.isActive === true; }","tryCatchPattern":"try { return await apiFetch(`/plans/${key}/term`); }\ncatch (e) { if (isNotFound(e)) { show('Plan unavailable'); return null; } throw e; }","preventionTips":["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."],"tags":["billing","not-found","plans","seeding"],"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"}