{"record":{"id":"4931f109c08e1e60","repo":"koala73/worldmonitor","slug":"userid-is-required","errorCode":null,"errorMessage":"userId is required","messagePattern":"userId is required","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/checkout.ts","lineNumber":494,"sourceCode":"// Internal action: called by /relay/create-checkout with trusted userId\n// ---------------------------------------------------------------------------\n\nexport const internalCreateCheckout = internalAction({\n  args: {\n    userId: v.string(),\n    email: v.optional(v.string()),\n    name: v.optional(v.string()),\n    productId: v.string(),\n    returnUrl: v.optional(v.string()),\n    discountCode: v.optional(v.string()),\n    referralCode: v.optional(v.string()),\n    attributionSource: v.optional(v.string()),\n    // See createCheckout — skips only the pending-payment guard (#4438).\n    bypassPendingGuard: v.optional(v.boolean()),\n  },\n  handler: async (ctx, args) => {\n    if (!args.userId) {\n      throw new ConvexError(\"userId is required\");\n    }\n    requireCheckoutProduct(args.productId);\n    if (args.bypassPendingGuard) {\n      // See createCheckout — audit the pending-guard bypass (#4438 review).\n      console.info(`[checkout] pending-payment guard bypassed user=${args.userId} product=${args.productId}`);\n    }\n    // Both guards concurrently (no shared data); subscription block still wins,\n    // bypass skips the pending query (#4438 review).\n    const [blocking, pending] = await Promise.all([\n      getCheckoutBlockingSubscription(ctx, args.userId, args.productId),\n      args.bypassPendingGuard\n        ? Promise.resolve(null)\n        : getCheckoutBlockingPendingPayment(ctx, args.userId, args.productId),\n    ]);\n    if (blocking) {\n      return buildBlockedCheckoutResponse(blocking);\n    }\n    if (pending) {","sourceCodeStart":476,"sourceCodeEnd":512,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/payments/checkout.ts#L476-L512","documentation":"The internal checkout mutation requires an explicit userId argument because it runs without a browser identity (internal functions have no auth context). An empty/missing args.userId makes the target user unattributable, so the mutation throws before validating the product.","triggerScenarios":"Calling the internal checkout mutation (the one with bypassPendingGuard in its args at checkout.ts:494) with userId undefined, null, or an empty string — e.g. from a webhook/cron where the user lookup failed.","commonSituations":"Webhook payload lacked the user reference used to resolve userId; caller code passes a variable that is undefined after a failed users-table lookup; cron job scheduling checkouts without resolving a user; omitting the field entirely from the function args object.","solutions":["Resolve and pass the Convex user's _id as userId before calling the mutation.","Fix the upstream lookup (email -> user mapping) that produced an undefined userId.","Skip checkout creation for requests with no attributable user instead of calling the mutation with a blank id."],"exampleFix":"// before\nawait ctx.runMutation(api.payments.checkout.internalCreateCheckout, { productId } as any);\n// after\nif (!user?._id) throw new Error(\"cannot checkout without user\");\nawait ctx.runMutation(api.payments.checkout.internalCreateCheckout, { userId: user._id, productId });","handlingStrategy":"validation","validationCode":"if (!userId) throw new Error(\"internalCreateCheckout requires a resolved userId\");\nawait runMutation(api.payments.checkout.internalCreateCheckout, { userId, productId });","typeGuard":"function hasUserId(u: { _id?: string } | null | undefined): u is { _id: string } {\n  return typeof u?._id === \"string\" && u._id.length > 0;\n}","tryCatchPattern":"try {\n  await internalCreateCheckout({ userId, productId });\n} catch (e) {\n  if (String(e?.message) === \"userId is required\") {\n    logUnattributableCheckoutRequest(context); // do not retry without a user\n  }\n}","preventionTips":["Type internal callers against a required userId parameter so TypeScript rejects omitted fields.","Resolve user identity upstream and fail fast when lookup yields nothing.","Never call internal checkout mutations from paths lacking a verified user reference (e.g. anonymous webhook events)."],"tags":["checkout","convex","validation","internal-mutation"],"backgroundTag":"missing-required-argument","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"}