{"record":{"id":"cf59f208bff448b6","repo":"fullstackhero/dotnet-starter-kit","slug":"active-plan-with-key-command-plankey-not-found","errorCode":null,"errorMessage":"Active plan with key '{command.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/Subscriptions/AssignSubscription/AssignSubscriptionCommandHandler.cs","lineNumber":32,"sourceCode":"    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : ICommandHandler<AssignSubscriptionCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(AssignSubscriptionCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        // Only root may target an arbitrary tenant; a tenant caller is pinned to its own, so it can't\n        // (re)assign or cancel another tenant's subscription via a foreign tenant id in the body.\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n        var targetTenantId = isRoot ? command.TenantId : callerTenantId;\n\n#pragma warning disable CA1308 // Plan keys are canonical lowercase slugs\n        var key = command.PlanKey.ToLowerInvariant();\n#pragma warning restore CA1308\n        var plan = await dbContext.Plans.FirstOrDefaultAsync(p => p.Key == key && p.IsActive, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Active plan with key '{command.PlanKey}' not found.\");\n\n        var now = DateTime.UtcNow;\n        var current = await dbContext.Subscriptions\n            .FirstOrDefaultAsync(s => s.TenantId == targetTenantId && s.Status == Contracts.SubscriptionStatus.Active, cancellationToken)\n            .ConfigureAwait(false);\n        current?.Cancel(now);\n\n        var subscription = Subscription.Create(targetTenantId, plan.Id, now);\n        dbContext.Subscriptions.Add(subscription);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return subscription.Id;\n    }\n}\n","sourceCodeStart":14,"sourceCodeEnd":46,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Subscriptions/AssignSubscription/AssignSubscriptionCommandHandler.cs#L14-L46","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nassign({ tenantId: 'acme', planKey: 'Pro Annual' });\n\n// after: canonical key from the active plans list\nconst plans = await apiFetch('/plans');\nconst key = plans.find(p => p.name === 'Pro Annual').key; // 'pro-annual'\nassign({ tenantId: 'acme', planKey: key });","handlingStrategy":"validation","validationCode":"const plans = await apiFetch('/plans');\nconst key = String(command.planKey).trim().toLowerCase();\nif (!plans.some(p => p.key === key && p.isActive)) throw new Error(`No active plan '${key}'; check seeding`);","typeGuard":"function isActivePlan(p) { return typeof p?.key === 'string' && p.isActive === true; }","tryCatchPattern":"try { await assignSubscription({ tenantId, planKey }); }\ncatch (e) { if (isNotFound(e)) { show(`Plan '${planKey}' is not available`); return; } throw e; }","preventionTips":["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."],"tags":["billing","not-found","plans","subscriptions","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"}