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

  1. Verify the id exists: SELECT * FROM "Plans" WHERE "Id" = '<id>'; if absent, re-list plans and use a current id.
  2. Refresh the admin plans page so the form uses ids from the live database rather than a stale cache.
  3. Re-seed the environment if plan ids changed (DbMigrator apply --seed) and update any stored references.
  4. 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

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


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)