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

  1. Use an exact active plan key from GET /plans (SELECT Key FROM Plans WHERE IsActive) — canonical lowercase slugs.
  2. Seed the target environment: dotnet run --project src/Host/FSH.Starter.DbMigrator -- apply --seed.
  3. Reactivate or re-add the plan if it was disabled while subscriptions were still being assigned to it.
  4. 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

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)