{"record":{"id":"98c625128e49a1b8","repo":"hcengineering/platform","slug":"missing-priceid-for-plan-plankey","errorCode":null,"errorMessage":"Missing priceId for plan: ${planKey}","messagePattern":"Missing priceId for plan: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"services/payment/pod-payment/src/providers/stripe/provider.ts","lineNumber":105,"sourceCode":"      if (this.subscriptionPlans[plan] === undefined) {\n        throw new Error(`Missing plan in config: ${plan}`)\n      }\n    }\n  }\n\n  async createSubscription (\n    ctx: MeasureContext,\n    request: SubscribeRequest,\n    workspaceUuid: WorkspaceUuid,\n    workspaceUrl: string,\n    accountUuid: string\n  ): Promise<CheckoutResponse> {\n    ctx.info('Creating Stripe subscription', { type: request.type, plan: request.plan })\n\n    const planKey = getPlanKey(request.type, request.plan)\n    const priceId = this.subscriptionPlans[planKey]\n    if (priceId === undefined) {\n      throw new Error(`Missing priceId for plan: ${planKey}`)\n    }\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    const response = await this.stripe.createCheckout(ctx, {\n      priceId,\n      successUrl,\n      cancelUrl,\n      customerEmail: request.customerEmail,\n      customerName: request.customerName,\n      metadata: {\n        workspaceUuid,\n        subscriptionType: request.type,\n        subscriptionPlan: request.plan,\n        accountUuid\n      }\n    })\n\n    return {","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/payment/pod-payment/src/providers/stripe/provider.ts#L87-L123","documentation":"StripeProvider.createSubscription looks up a Stripe price ID in the this.subscriptionPlans map (parsed from the provider's semicolon-separated 'plan@type:priceId' config) using the key `${plan}@${type}` from the subscribe request. When no price ID is registered for that key, the provider cannot create a Stripe Checkout session and throws this error. It is a server configuration/request mismatch, not a Stripe API failure.","triggerScenarios":"Calling createSubscription (via the payment service subscribe endpoint) with a request whose (request.type, request.plan) combination produces a planKey like 'premium@tier' or 'common@yearly' that is absent from the subscriptionPlans config string. The constructor only hard-validates common/rare/epic/legendary@tier, so any other type (e.g. one-off/yearly) or plan name passes startup but fails here.","commonSituations":"Deploying with an incomplete StripeSubscriptionPlans config env value; typo in the plan name sent by the client; requesting a subscription type other than 'tier' (e.g. a yearly or one-off type) that was never configured; adding a new plan to the model without updating the payment service config.","solutions":["Add the missing 'plan@type:price_REDACTED_<priceId>' entry to the payment provider's subscriptionPlans configuration and restart the service","Fix the client/request to use a configured (type, plan) combination, e.g. plan 'common' with type 'tier'","Extend the constructor's mustHave list so misconfiguration fails at startup instead of at request time","Log or return the available planKeys in the error to ease diagnosis"],"exampleFix":"// config before\nStripeSubscriptionPlans=common@tier:price_abc;rare@tier:price_def;epic@tier:price_ghi;legendary@tier:price_jkl\n// after (adding the requested plan)\nStripeSubscriptionPlans=common@tier:price_abc;rare@tier:price_def;epic@tier:price_ghi;legendary@tier:price_jkl;premium@tier:price_mno","handlingStrategy":"validation","validationCode":"const planKey = `${request.plan}@${request.type}`\n// parse config the same way the provider does\nconst configured = new Map(config.StripeSubscriptionPlans.split(';').map(p => {\n  const [key, priceId] = p.split(':')\n  return [key, priceId]\n}))\nif (!configured.has(planKey)) {\n  throw new Error(`Plan not configured on payment provider: ${planKey}`)\n}","typeGuard":"function isConfiguredPlan(\n  plans: Record<string, string>,\n  type: string,\n  plan: string\n): plans is Record<string, string> & { [k: string]: string } {\n  return plans[`${plan}@${type}`] !== undefined\n}","tryCatchPattern":"try {\n  const checkout = await paymentProvider.createSubscription(ctx, request, wsUuid, wsUrl, accountUuid)\n  return checkout\n} catch (err) {\n  if ((err as Error).message.startsWith('Missing priceId for plan:')) {\n    throw new ApiError(400, `Plan '${request.plan}' (${request.type}) is not available for purchase`)\n  }\n  throw err\n}","preventionTips":["Keep the StripeSubscriptionPlans config in sync with the plan catalog in the model; add both in the same change","Enforce all plans at provider startup by extending the constructor's mustHave list","Validate requested (type, plan) pairs at the API boundary before reaching the provider","Test each configured plan end-to-end after deployment"],"tags":["stripe","configuration","billing","missing-config"],"backgroundTag":"missing-plan-price-mapping","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}