{"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":88,"sourceCode":"          retryAfterSeconds: Math.max(1, Math.ceil((row.windowStart + CHECKOUT_ADMISSION_WINDOW_MS - now) / 1000)),\n        };\n      }\n      await ctx.db.patch(row._id, { count: row.count + 1 });\n    } else if (row) {\n      await ctx.db.patch(row._id, { windowStart: now, count: 1 });\n    } else {\n      await ctx.db.insert(\"checkoutAdmissions\", { userId, windowStart: now, count: 1 });\n    }\n    return null;\n  },\n});\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":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/convex/payments/checkout.ts#L70-L106","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":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}