fullstackhero/dotnet-starter-kit · error · NotFoundException
Active plan with key
Error message
Active plan with key '{command.PlanKey}' not found. What it means
AssignSubscriptionCommandHandler lowercases command.PlanKey and looks up an ACTIVE plan (Key == key && IsActive); the target tenant's current active subscription is then cancelled and replaced. If no active plan matches the key it throws NotFoundException("Active plan with key '{key}' not found.") and no assignment happens.
Solutions
- Use an exact active plan key from GET /plans (SELECT Key FROM Plans WHERE IsActive) — canonical lowercase slugs.
- Seed the target environment: dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed.
- Reactivate or re-add the plan if it was disabled while subscriptions were still being assigned to it.
- Trim/normalize the key client-side before sending (keys are lowercase slugs without spaces).
Example fix
// before
assign({ tenantId: 'acme', planKey: 'Pro Annual' });
// after: canonical key from the active plans list
const plans = await apiFetch('/plans');
const key = plans.find(p => p.name === 'Pro Annual').key; // 'pro-annual'
assign({ tenantId: 'acme', planKey: key }); Defensive patterns
Strategy: validation
Validate before calling
const plans = await apiFetch('/plans');
const key = String(command.planKey).trim().toLowerCase();
if (!plans.some(p => p.key === key && p.isActive)) throw new Error(`No active plan '${key}'; check seeding`); Type guard
function isActivePlan(p) { return typeof p?.key === 'string' && p.isActive === true; } Try / catch
try { await assignSubscription({ tenantId, planKey }); }
catch (e) { if (isNotFound(e)) { show(`Plan '${planKey}' is not available`); return; } throw e; } Prevention
- Send canonical lowercase plan keys resolved from GET /plans.
- Seed plans in every environment (DbMigrator apply --seed) before enabling signups.
- Treat plan deactivation as breaking for any flow that still assigns that key.
When it happens
Trigger: POST /subscriptions/assign with a plan key that doesn't exist, is deactivated, has wrong casing/whitespace (e.g. 'Pro'), or is seeded only in another environment.
Common situations: Checkout page sending a marketing plan name instead of the canonical slug; plan renamed/deactivated during a pricing change while old signup links persist; fresh environment where plan seeding via DbMigrator was skipped.
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/cf59f208bff448b6.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Subscriptions/AssignSubscription/AssignSubscriptionCommandHandler.cs:32
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: ICommandHandler<AssignSubscriptionCommand, Guid>
{
public async ValueTask<Guid> Handle(AssignSubscriptionCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
// Only root may target an arbitrary tenant; a tenant caller is pinned to its own, so it can't
// (re)assign or cancel another tenant's subscription via a foreign tenant id in the body.
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
var targetTenantId = isRoot ? command.TenantId : callerTenantId;
#pragma warning disable CA1308 // Plan keys are canonical lowercase slugs
var key = command.PlanKey.ToLowerInvariant();
#pragma warning restore CA1308
var plan = await dbContext.Plans.FirstOrDefaultAsync(p => p.Key == key && p.IsActive, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException($"Active plan with key '{command.PlanKey}' not found.");
var now = DateTime.UtcNow;
var current = await dbContext.Subscriptions
.FirstOrDefaultAsync(s => s.TenantId == targetTenantId && s.Status == Contracts.SubscriptionStatus.Active, cancellationToken)
.ConfigureAwait(false);
current?.Cancel(now);
var subscription = Subscription.Create(targetTenantId, plan.Id, now);
dbContext.Subscriptions.Add(subscription);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return subscription.Id;
}
}
View on GitHub (pinned to 3f2959e683)