{"record":{"id":"2b23e9e08f03fd74","repo":"koala73/worldmonitor","slug":"invalid-checkout-product","errorCode":"INVALID_CHECKOUT_PRODUCT","errorMessage":"This product is not available for checkout.","messagePattern":"This product is not available for checkout\\.","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/checkout.ts","lineNumber":53,"sourceCode":"import { recordTerminalCheckoutRateLimit } from \"./checkoutRateLimitAlarm\";\n\n// MCP paid-funnel campaign marker (#6716). Imported, never re-declared: a\n// second copy of this normalisation is exactly the drift that produced the\n// display-vs-enforcement divergence documented in\n// docs/solutions/security-issues/mcp-quota-credential-class-vs-plan-family-scoping-bypass.md.\n// The Convex runtime imports from `shared/` elsewhere (convex/apiKeys.ts,\n// convex/companyMonitoring/*), so there is no module-boundary reason to fork it.\nimport { normalizeCheckoutAttributionSource as normalizeAttributionSource } from \"../../shared/mcp-attribution\";\n\nconst ACTIVE_SUBSCRIPTION_EXISTS = \"ACTIVE_SUBSCRIPTION_EXISTS\";\nconst PAYMENT_IN_PROGRESS = \"PAYMENT_IN_PROGRESS\";\n\nfunction requireCheckoutProduct(productId: string): void {\n  const allowed = Object.values(PRODUCT_CATALOG).some(\n    (entry) => entry.dodoProductId === productId && entry.currentForCheckout && entry.selfServe,\n  );\n  if (!allowed) {\n    throw new ConvexError({\n      code: \"INVALID_CHECKOUT_PRODUCT\",\n      message: \"This product is not available for checkout.\",\n    });\n  }\n}\n\n// RFC 5321 maximum forward-path length. A value beyond it is not an address we\n// could deliver to anyway, and it keeps the stamped metadata value small.\nconst MAX_LOGIN_EMAIL_LENGTH = 254;\n\n/**\n * Normalizes the authenticated login email for stamping into checkout metadata\n * (#6335).\n *\n * This is a shape guard, not a trust boundary — it keeps an unusable value out\n * of a field the webhook later hands to Resend as a recipient. What makes that\n * the right level: `createCheckout` reads the email from the Clerk JWT `email`\n * claim via `resolveUserIdentity`, and `internalCreateCheckout` receives it from","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/payments/checkout.ts#L35-L71","documentation":"requireCheckoutProduct validates that the requested Dodo product ID maps to a catalog entry that is currently offered (currentForCheckout) and purchasable self-serve (selfServe). Anything else — unknown product ID, retired plan, or internal/admin-only product — is rejected so checkout can never be created for an unintended product.","triggerScenarios":"Calling createCheckout or internalCreateCheckout with a productId that is not in PRODUCT_CATALOG, has dodoProductId mismatch, currentForCheckout=false (retired/superseded plan), or selfServe=false (comp/admin-only SKU).","commonSituations":"Client hardcodes an old dodoProductId after the plan was rotated; attempting to buy an enterprise/annual-only SKU through the self-serve checkout; typo'd product ID; stale cached pricing page pointing at a retired product.","solutions":["Fetch the current checkout-eligible product IDs from PRODUCT_CATALOG (or the pricing endpoint) and use one of those.","If the plan was superseded, switch to the replacement product's dodoProductId.","For non-self-serve products, use the internal/admin grant path instead of checkout.","Clear stale client caches/pricing pages that reference retired product IDs."],"exampleFix":"// before\ncreateCheckout({ productId: \"prod_old_pro_monthly\" });  // retired\n// after\nconst sku = await getCurrentSelfServeProductId(\"pro_monthly\");\ncreateCheckout({ productId: sku });","handlingStrategy":"validation","validationCode":"import { PRODUCT_CATALOG } from \"./catalog\";\nconst ok = Object.values(PRODUCT_CATALOG).some(p => p.dodoProductId === productId && p.currentForCheckout && p.selfServe);\nif (!ok) throw new Error(`product ${productId} is not checkout-eligible`);","typeGuard":null,"tryCatchPattern":"try {\n  await createCheckout({ productId });\n} catch (e) {\n  if (isConvexError(e) && e.data?.code === \"INVALID_CHECKOUT_PRODUCT\") {\n    refreshPricingAndPromptUserToRechoosePlan();\n  }\n}","preventionTips":["Never hardcode dodoProductId in clients; fetch current self-serve SKUs from the pricing endpoint.","Rotate plan IDs behind stable plan keys so clients survive catalog changes.","Verify product IDs per environment (staging vs production Dodo)."],"tags":["checkout","payments","product-catalog","validation"],"backgroundTag":"invalid-enum-value","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}