fullstackhero/dotnet-starter-kit · error · NotFoundException
Plan not found.
Error message
Plan {command.PlanId} not found. What it means
UpdatePlanCommandHandler fetches the plan by PlanId before applying updates; if no plan row with that id exists it throws NotFoundException("Plan {id} not found."). Unlike invoice handlers this one is id-based and unscoped, so a miss means the id truly isn't in the Plans table.
Solutions
- Verify the id exists: SELECT * FROM "Plans" WHERE "Id" = '<id>'; if absent, re-list plans and use a current id.
- Refresh the admin plans page so the form uses ids from the live database rather than a stale cache.
- Re-seed the environment if plan ids changed (DbMigrator apply --seed) and update any stored references.
- Double-check you are editing the right aggregate — plan ids differ from subscription ids.
Example fix
// before: stale id after reseed
await apiFetch(`/plans/${cachedPlanId}`, { method: 'PUT', body });
// after: resolve fresh id by key
const plans = await apiFetch('/plans');
const id = plans.find(p => p.key === 'pro').id;
await apiFetch(`/plans/${id}`, { method: 'PUT', body }); Defensive patterns
Strategy: validation
Validate before calling
const plans = await apiFetch('/plans');
if (!plans.some(p => p.id === planId)) throw new Error(`Plan ${planId} does not exist; refresh the list`); Type guard
function planExists(plans, id) { return plans.some(p => p.id === id); } Try / catch
try { await apiFetch(`/plans/${planId}`, { method: 'PUT', body }); }
catch (e) { if (isNotFound(e)) { await refreshPlanList(); show('Plan was removed; reselect it'); } else throw e; } Prevention
- Refresh the plans list before edits instead of trusting long-lived cached ids.
- Re-seeding regenerates ids — update any stored references after running the DbMigrator.
- Double-check the id belongs to the Plans table, not subscriptions or tenants.
When it happens
Trigger: PUT /plans/{id} (v1) with a nonexistent plan GUID, an id from a different database/environment, an id of another aggregate (e.g. subscription id pasted into the plan form), or a plan deleted before the update landed.
Common situations: Stale admin UI holding a plan id from a previous seed run (re-seeding regenerated ids); copying a subscription or tenant id into the plan edit dialog; concurrent cleanup job removed the plan while an editor was open.
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
- Active plan with key
- Active plan with key
- Invoice not found.
- Invoice not found.
- Top-up request not found.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/b4a106ef25670293.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Plans/UpdatePlan/UpdatePlanCommandHandler.cs:17
using FSH.Framework.Core.Exceptions;
using FSH.Modules.Billing.Contracts.v1.Plans;
using FSH.Modules.Billing.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Plans.UpdatePlan;
public sealed class UpdatePlanCommandHandler(BillingDbContext dbContext)
: ICommandHandler<UpdatePlanCommand, Guid>
{
public async ValueTask<Guid> Handle(UpdatePlanCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var plan = await dbContext.Plans.FirstOrDefaultAsync(p => p.Id == command.PlanId, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException($"Plan {command.PlanId} not found.");
plan.Update(command.Name, command.MonthlyBasePrice, command.OverageRates, command.Interval, command.AnnualPrice);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return plan.Id;
}
}
View on GitHub (pinned to 3f2959e683)