{"record":{"id":"b3c5deebc22c9678","repo":"hcengineering/platform","slug":"no-price-configured-for-plan-plankey","errorCode":null,"errorMessage":"No price configured for plan: ${planKey}","messagePattern":"No price configured for plan: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"services/payment/pod-payment/src/providers/stripe/provider.ts","lineNumber":311,"sourceCode":"  async updateSubscriptionPlan (\n    ctx: MeasureContext,\n    subscriptionId: string,\n    newPlan: string,\n    workspaceUrl: string,\n    accountUuid: string\n  ): Promise<SubscriptionData | CheckoutResponse | null> {\n    // Get the current subscription to check if it's free\n    const currentSub = await this.stripe.getSubscription(ctx, subscriptionId)\n\n    // Check if subscription is free by checking if the price amount is 0\n    const price = currentSub.items.data[0]?.price\n    const isFreeSubscription = price?.unit_amount === 0 || price === undefined\n\n    // Get the Stripe price ID for the new plan (subscriptions updates are tier type)\n    const planKey = getPlanKey(SubscriptionType.Tier, newPlan)\n    const priceId = this.subscriptionPlans[planKey]\n    if (priceId === undefined) {\n      throw new Error(`No price configured for plan: ${planKey}`)\n    }\n\n    // If subscription is free, create a checkout instead of updating directly\n    if (isFreeSubscription) {\n      const successUrl = `${this.frontUrl}/workbench/${workspaceUrl}/setting/setting/billing/subscriptions?payment=success&checkout_id={CHECKOUT_SESSION_ID}`\n      const cancelUrl = `${this.frontUrl}/workbench/${workspaceUrl}/setting/setting/billing/subscriptions?payment=canceled`\n\n      const customerId = typeof currentSub.customer === 'string' ? currentSub.customer : currentSub.customer?.id\n      const metadata = currentSub.metadata ?? {}\n\n      const response = await this.stripe.createCheckout(ctx, {\n        priceId,\n        successUrl,\n        cancelUrl,\n        customerId,\n        subscriptionId: currentSub.id,\n        metadata: {\n          workspaceUuid: metadata.workspaceUuid,","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/payment/pod-payment/src/providers/stripe/provider.ts#L293-L329","documentation":"updateSubscriptionPlan resolves the Stripe price ID for the target plan by building the key `${newPlan}@tier` (subscription plan updates are always tier type) and indexing this.subscriptionPlans. If no price is configured for that key it throws this error before touching Stripe. Like error 810, this is a configuration gap between the requested plan and the payment service's subscriptionPlans config.","triggerScenarios":"Calling updateSubscriptionPlan with a newPlan value that has no '<plan>@tier:priceId' entry in the provider config (constructor only enforces common/rare/epic/legendary@tier); client requesting an unknown or renamed plan name during an upgrade/downgrade flow.","commonSituations":"New pricing tier added in the product but the payment service config was not updated; plan renamed on the frontend while the backend still uses the old key; user downgrading to a free/unknown plan name not present in the config.","solutions":["Add the missing '<newPlan>@tier:price_xxx' entry to the subscriptionPlans config and restart the service","Validate newPlan on the API/frontend against the configured plan list before calling updateSubscriptionPlan","Sync the plan catalog (model) with the Stripe price config so unknown plans are rejected upstream with a clearer message","Check for typos/case differences between the plan key sent and the one configured"],"exampleFix":"// before (client)\nawait updateSubscriptionPlan(ctx, workspaceId, SubscriptionType.Tier, 'premium') // no 'premium@tier' configured\n// after\nconst configuredPlans = ['common', 'rare', 'epic', 'legendary']\nif (!configuredPlans.includes(newPlan)) throw new ApiError(400, `Unknown plan: ${newPlan}`)\nawait updateSubscriptionPlan(ctx, workspaceId, SubscriptionType.Tier, newPlan)","handlingStrategy":"validation","validationCode":"const planKey = `${newPlan}@tier`\nif (!configuredPlanKeys.includes(planKey)) {\n  throw new ApiError(400, `Unknown plan '${newPlan}'. Available: ${configuredPlanKeys.join(', ')}`)\n}","typeGuard":"function isKnownPlan(plan: string, known: readonly string[]): plan is typeof known[number] {\n  return (known as readonly string[]).includes(plan)\n}","tryCatchPattern":"try {\n  await paymentProvider.updateSubscriptionPlan(ctx, workspaceId, SubscriptionType.Tier, newPlan)\n} catch (err) {\n  if ((err as Error).message.startsWith('No price configured for plan:')) {\n    throw new ApiError(400, `Cannot switch to plan '${newPlan}': it is not offered`) \n  }\n  throw err\n}","preventionTips":["Expose the configured plan list to the client (e.g. via a plans endpoint) and populate plan pickers from it","Never hardcode plan names in the frontend; use the shared plan catalog","Add new tiers to Stripe, config, and model in one deployment step","Validate plan names server-side before calling the provider"],"tags":["stripe","configuration","billing","plan-upgrade"],"backgroundTag":"missing-plan-price-mapping","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}